⚠️ Important: This library is designed exclusively for Next.js App Router. It requires server-side setup and is not compatible with the Pages Router. Make sure your Next.js project is using the App Router architecture.
npm install @flyo/nitro-nextCreate a flyo.config.tsx file. The initNitro() function returns a Flyo instance — an object that contains all the API methods and state for your app.
import type { ReactNode } from 'react';
import { initNitro } from '@flyo/nitro-next/server';
import { FlyoClientWrapper } from '@flyo/nitro-next/client';
import { HeroBanner } from './components/HeroBanner';
import { Text } from './components/Text';
const accessToken = process.env.FLYO_ACCESS_TOKEN || '';
const liveEdit = process.env.FLYO_LIVE_EDIT === 'true';
const baseUrl = process.env.SITE_URL || 'http://localhost:3000';
// Create the Flyo instance — import this wherever you need CMS access
export const flyo = initNitro({
accessToken,
lang: 'en',
baseUrl,
liveEdit,
serverCacheTtl: 1200,
clientCacheTtl: 900,
components: {
HeroBanner,
Text
}
});
// Optional but recommended wrapper for live editing support
export function FlyoProvider({ children }: { children: ReactNode }) {
if (liveEdit) {
return <FlyoClientWrapper>{children}</FlyoClientWrapper>;
}
return <>{children}</>;
}The flyo instance provides:
flyo.getNitroConfig()— Fetch the CMS configurationflyo.getNitroPages()— Get the Pages API clientflyo.getNitroEntities()— Get the Entities API clientflyo.getNitroSitemap()— Get the Sitemap API clientflyo.getNitroSearch()— Get the Search API clientflyo.pageResolveRoute(props)— Resolve a page from route paramsflyo.sitemap()— Generate the sitemapflyo.state— Access the configuration state
Create a proxy.ts file in the src/ directory to handle cache control:
import { createProxy } from '@flyo/nitro-next/proxy';
import { flyo } from './flyo.config';
export default createProxy(flyo);
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};The proxy middleware:
- Sets appropriate cache headers for CDN (s-maxage) and browser (max-age) based on your configuration
- Disables caching when live edit mode is enabled (development mode)
- Uses Next.js middleware to intercept all requests matching the configured pattern
- Reads cache TTL values from your Nitro configuration (
serverCacheTtlandclientCacheTtl)
Configuration options in initNitro():
liveEdit- Enables live edit mode (typically controlled via environment variable), disables caching (default: false)serverCacheTtl- CDN cache duration in seconds (default: 1200 = 20 min)clientCacheTtl- Browser cache duration in seconds (default: 900 = 15 min)
Use the Flyo instance in your app/layout.tsx:
import Link from 'next/link';
import { FlyoProvider, flyo } from '@/flyo.config';
import { NitroDebugInfo } from '@flyo/nitro-next/server';
export default async function RootLayout({ children }: { children: React.ReactNode }) {
const config = await flyo.getNitroConfig();
const navItems = config?.containers?.nav?.items ?? [];
return (
<FlyoProvider>
<html>
<body>
<header>
<nav>
<ul className="flex gap-6">
{navItems.map((item, index) => (
<li key={index}>
<Link
href={item.href}
target={item.target}
>
{item.label}
</Link>
</li>
))}
</ul>
</nav>
</header>
<NitroDebugInfo flyo={flyo} />
{children}
</body>
</html>
</FlyoProvider>
);
}The NitroDebugInfo component outputs debug information as an HTML comment, including:
- Live edit status
- Environment mode
- API version and last update date
- Token type (production/develop)
- Deployment ID and commit SHA (Vercel)
- Release version (if set)
Create a catch-all route in app/[[...slug]]/page.tsx. The factory functions take the flyo instance and return Next.js-compatible handlers:
import { nitroPageRoute, nitroPageGenerateMetadata, nitroPageGenerateStaticParams } from "@flyo/nitro-next/server";
import { flyo } from "@/flyo.config";
export default nitroPageRoute(flyo);
export const generateMetadata = nitroPageGenerateMetadata(flyo);
// NOTE: generateStaticParams is commented out by default!
//
// ⚠️ IMPORTANT: Only enable this in PRODUCTION builds!
// When enabled, Next.js will pre-render ALL pages at build time, which:
// - Disables dynamic caching completely
// - Prevents live preview updates in the Nitro CMS editor
//
// export const generateStaticParams = nitroPageGenerateStaticParams(flyo);If you need to access the page data for custom logic (e.g. reading page properties, adding conditional wrappers, passing data to other components), use flyo.pageResolveRoute():
// app/[[...slug]]/page.tsx
import { NitroPage, nitroPageGenerateMetadata } from '@flyo/nitro-next/server';
import { flyo } from '@/flyo.config';
export const generateMetadata = nitroPageGenerateMetadata(flyo);
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
const { page, path, cfg } = await flyo.pageResolveRoute(props);
// Access page data before rendering
// page - the full Page object (page.title, page.meta_json, page.json, etc.)
// path - the resolved URL path string
// cfg - the Flyo ConfigResponse
return (
<div>
<h1>{page.title}</h1>
{/* Render all blocks from the page */}
<NitroPage page={page} flyo={flyo} />
</div>
);
}The flyo.pageResolveRoute() function is React-cached — calling it in both generateMetadata and your page component will only trigger a single API request.
Create custom components for your Flyo blocks. Each component receives a block object containing the content from your CMS.
'use client';
import { Block } from "@flyo/nitro-typescript";
import { editable } from "@flyo/nitro-next/client";
export function HeroBanner({ block }: { block: Block }) {
return (
<section {...editable(block)} className="bg-gray-200 p-8 rounded-lg text-center">
<h2 className="text-3xl font-bold mb-4">
{block?.content?.title}
</h2>
<p className="text-lg mb-6">
{block?.content?.teaser}
</p>
<img
src={block?.content?.image?.source}
alt={block?.content?.image?.caption}
className="mx-auto mb-6"
/>
</section>
);
}The editable() helper function marks the component as editable in the Flyo CMS live editor. It spreads the necessary data attributes onto your component's root element to enable in-place editing.
Important:
editable()is a client-only function. Any component that useseditable()must have'use client'as the very first line of the file. Using it in a server component will cause a runtime error.
The FlyoWysiwyg component renders ProseMirror/TipTap JSON content. It handles standard nodes automatically and allows you to provide custom components for specific node types.
'use client';
import { FlyoWysiwyg } from '@flyo/nitro-next/client';
export default function MyComponent({ block }) {
return (
<FlyoWysiwyg json={block.content.json} />
);
}Create a custom component for specific node types:
// components/wysiwyg/CustomImage.tsx
'use client';
interface ImageNode {
node: {
attrs: {
src: string;
alt?: string;
title?: string;
};
};
}
export default function CustomImage({ node }: ImageNode) {
const { src, alt, title } = node.attrs;
return (
<img
src={src}
alt={alt}
title={title}
style={{ maxWidth: '100%', height: 'auto' }}
/>
);
}Recommended pattern: create a project-level AppWysiwyg wrapper once.
Register your custom node components there and keep a default class (for example className="wysiwyg") so you can reuse the same setup everywhere.
// components/wysiwyg/AppWysiwyg.tsx
'use client';
import { FlyoWysiwyg, type WysiwygJson } from '@flyo/nitro-next/client';
import CustomImage from './CustomImage';
export function AppWysiwyg({
json,
}: {
json: WysiwygJson;
}) {
return (
<FlyoWysiwyg
json={json}
className="wysiwyg"
components={{
image: CustomImage,
}}
/>
);
}Use your wrapper directly in pages/components:
'use client';
import { AppWysiwyg } from './components/wysiwyg/AppWysiwyg';
export default function MyComponent({ block }) {
return <AppWysiwyg json={block.content.json} />;
}This keeps custom WYSIWYG node registration centralized and consistent across your app.
You can still override styles per usage, for example:
<AppWysiwyg json={block.content.json} className="wysiwyg article-body" />.
The FlyoCdnLoader function provides automatic image optimization through Flyo's CDN. Use it with Next.js Image component for optimized image delivery with automatic format conversion and resizing.
'use client';
import Image from 'next/image';
import { FlyoCdnLoader } from '@flyo/nitro-next/client';
export default function MyComponent({ block }) {
return (
<Image
loader={FlyoCdnLoader}
src={block.content.image.source}
alt={block.content.image.caption}
width={800}
height={600}
/>
);
}The loader automatically:
- Adds the Flyo CDN host (
storage.flyo.cloud) if not already present - Applies width-based transformations
- Converts images to WebP format for optimal performance
When blocks contain nested blocks in slots, use the NitroSlot component to recursively render them. In v2, NitroSlot requires the flyo prop — import it from your config file:
import { NitroSlot } from '@flyo/nitro-next/server';
import { flyo } from '@/flyo.config';
import { Block } from '@flyo/nitro-typescript';
export function Container({ block }: { block: Block }) {
return (
<div className="container">
<h2>{block.content?.title}</h2>
{/* Render nested blocks from the slot */}
<NitroSlot slot={block.slots?.content} flyo={flyo} />
</div>
);
}Keep in mind that
NitroSlotcan only be used in server components, as it relies on server-side rendering of blocks.
The NitroSlot component automatically handles:
- Iterating over nested blocks
- Recursively rendering each block using
NitroBlock - Supporting unlimited nesting depth
Because editable() requires 'use client' and NitroSlot is server-only, you cannot use both in the same file. The EditableSection component solves this by acting as a thin client wrapper that applies the editable data attribute, while accepting server-rendered children (including NitroSlot) via the children prop.
This works because Next.js supports passing server-rendered React trees into client components through props like children.
// components/HeroBanner.tsx (server component – no 'use client' needed)
import { Block } from '@flyo/nitro-typescript';
import { NitroSlot } from '@flyo/nitro-next/server';
import { EditableSection } from '@flyo/nitro-next/client';
import { flyo } from '@/flyo.config';
export function HeroBanner({ block }: { block: Block }) {
return (
<EditableSection block={block} className="bg-gray-200 p-8 rounded-lg text-center">
<h2 className="text-3xl font-bold mb-4">
{block?.content?.title}
</h2>
<p className="text-lg mb-6">
{block?.content?.teaser}
</p>
<NitroSlot slot={block.slots?.content} flyo={flyo} />
</EditableSection>
);
}EditableSection accepts an optional as prop to change the wrapper element (defaults to <section>):
<EditableSection block={block} as="div" className="card">
{/* ... */}
</EditableSection>Why this works: Only the minimal wrapper becomes a client component. The heavy recursive slot rendering stays on the server. Props passed into a client component must be serializable — plain CMS JSON objects like
blockare fine.
Nitro provides flexible helpers for creating entity detail pages with any route structure. You define a resolver function that fetches the entity from your route params, and the library handles caching and rendering.
Create app/blog/[slug]/page.tsx:
import {
nitroEntityRoute,
nitroEntityGenerateMetadata,
NitroEntityJsonLd,
type EntityResolver
} from "@flyo/nitro-next/server";
import { flyo } from "@/flyo.config";
import { FlyoMetric } from "@flyo/nitro-next/client";
import type { Entity } from "@flyo/nitro-typescript";
// Define how to resolve the entity from route params
const resolver: EntityResolver<{ slug: string }> = async (params) => {
const { slug } = await params;
return flyo.getNitroEntities().entityBySlug({
slug,
typeId: 123 // Your entity type ID from Flyo
});
};
// Factory functions return Next.js-compatible handlers
export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });
export default nitroEntityRoute(flyo, {
resolver,
render: (entity: Entity) => (
<>
<NitroEntityJsonLd entity={entity} />
<FlyoMetric entity={entity} />
<article>
<h1>{entity.entity?.entity_title}</h1>
<p>{entity.entity?.entity_teaser}</p>
</article>
</>
)
});Create app/items/[uniqueid]/page.tsx:
import {
nitroEntityRoute,
nitroEntityGenerateMetadata,
NitroEntityJsonLd,
type EntityResolver
} from "@flyo/nitro-next/server";
import { flyo } from "@/flyo.config";
import { FlyoMetric } from "@flyo/nitro-next/client";
import type { Entity } from "@flyo/nitro-typescript";
const resolver: EntityResolver<{ uniqueid: string }> = async (params) => {
const { uniqueid } = await params;
return flyo.getNitroEntities().entityByUniqueid({ uniqueid });
};
export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });
export default nitroEntityRoute(flyo, {
resolver,
render: (entity: Entity) => (
<>
<NitroEntityJsonLd entity={entity} />
<FlyoMetric entity={entity} />
<div>
<h1>{entity.entity?.entity_title}</h1>
</div>
</>
)
});Works with any route parameter name - create app/products/[id]/page.tsx:
import {
nitroEntityRoute,
nitroEntityGenerateMetadata,
NitroEntityJsonLd,
type EntityResolver
} from "@flyo/nitro-next/server";
import { flyo } from "@/flyo.config";
import { FlyoMetric } from "@flyo/nitro-next/client";
import type { Entity } from "@flyo/nitro-typescript";
const resolver: EntityResolver<{ id: string }> = async (params) => {
const { id } = await params;
return flyo.getNitroEntities().entityBySlug({
slug: id,
typeId: 456
});
};
export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });
export default nitroEntityRoute(flyo, {
resolver,
render: (entity: Entity) => (
<>
<NitroEntityJsonLd entity={entity} />
<FlyoMetric entity={entity} />
<div>
<h1>{entity.entity?.entity_title}</h1>
<p>{entity.entity?.entity_teaser}</p>
</div>
</>
)
});- Type-safe params: Define your route params type to match your Next.js route structure
- Custom resolver: Write a function that takes the params and returns an entity
- Automatic caching: The resolver is automatically wrapped with React cache - it's called once per unique params
- Shared resolution: Both
nitroEntityRouteandnitroEntityGenerateMetadatause the same cached result - Flexible rendering: Provide a custom render function or use the default simple renderer
This pattern works with any route structure: [slug], [id], [uniqueid], [whatever] - you control the resolution logic!
Nitro provides automatic sitemap generation using the Flyo instance:
Ensure your config includes the baseUrl:
export const flyo = initNitro({
accessToken: process.env.FLYO_ACCESS_TOKEN || '',
baseUrl: process.env.SITE_URL || 'http://localhost:3000',
// ...
});Create app/sitemap.ts:
import { flyo } from '@/flyo.config';
export default async function sitemap() {
return flyo.sitemap();
}- Fetches all content: The
flyo.sitemap()method fetches all pages and entities from the Flyo Nitro sitemap endpoint - Uses configured baseUrl: It constructs full URLs using the
baseUrlfrom your Nitro configuration - Handles routes: Prioritizes the
routesobject from entities, falls back toentity_slug - Returns Next.js format: Outputs the standard
MetadataRoute.Sitemapformat that Next.js expects
Set the SITE_URL environment variable for production:
# .env.production
SITE_URL=https://yourdomain.comNext.js will automatically serve the sitemap at /sitemap.xml.
editable(block)– Returns thedata-flyo-uidattributes to wire blocks into the Flyo live editor. Client-only — must be used in components with the'use client'directive.import { editable } from '@flyo/nitro-next/client';
FlyoClientWrapper– Internal wrapper that mounts the Nitro bridge, watches for new editable nodes, and wires the click/highlight handlers.import { FlyoClientWrapper } from '@flyo/nitro-next/client';
FlyoWysiwyg– Renders Flyo ProseMirror/TipTap JSON with optional overrides for individual node types.import { FlyoWysiwyg } from '@flyo/nitro-next/client';
FlyoCdnLoader– Image loader for Next.js Image component that optimizes images through Flyo CDN with automatic format conversion and resizing.import { FlyoCdnLoader } from '@flyo/nitro-next/client';
FlyoMetric– Component for tracking entity metrics in production. Automatically sends a metric tracking request to the Flyo API when in production environment and the entity has a metric API URL configured.import { FlyoMetric } from '@flyo/nitro-next/client';
EditableSection– Thin client wrapper that applieseditable()to a root element while accepting server-rendered children (e.g.NitroSlot). Use this when you need botheditable()and slots in the same component. Accepts an optionalasprop to change the wrapper element (defaults to<section>).import { EditableSection } from '@flyo/nitro-next/client';
isProd– Constant that checks if the current environment is production (process.env.NODE_ENV === 'production').import { isProd } from '@flyo/nitro-next/client';
initNitro(config)– Create a Flyo instance with all API methods and state. Returns aFlyoInstance.import { initNitro } from '@flyo/nitro-next/server'; const flyo = initNitro({ accessToken: '...' });
nitroPageRoute(flyo)– Factory that returns a page route handler for Nitro pages.import { nitroPageRoute } from '@flyo/nitro-next/server'; export default nitroPageRoute(flyo);
nitroPageGenerateMetadata(flyo)– Factory that returns a metadata generator for Nitro pages.import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server'; export const generateMetadata = nitroPageGenerateMetadata(flyo);
nitroPageGenerateStaticParams(flyo)– Factory that returns a static params generator for SSG.import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server'; export const generateStaticParams = nitroPageGenerateStaticParams(flyo);
nitroEntityRoute(flyo, options)– Factory that returns an entity detail page handler. Takes a resolver function and optional render function.import { nitroEntityRoute } from '@flyo/nitro-next/server'; export default nitroEntityRoute(flyo, { resolver, render });
nitroEntityGenerateMetadata(flyo, options)– Factory that returns a metadata generator for entity detail pages.import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server'; export const generateMetadata = nitroEntityGenerateMetadata(flyo, { resolver });
createProxy(flyo)– Create a Next.js middleware for cache control.import { createProxy } from '@flyo/nitro-next/proxy'; export default createProxy(flyo);
NitroPage– Server component that renders a whole Nitro page by delegating toNitroBlockfor each block. Requiresflyoprop.import { NitroPage } from '@flyo/nitro-next/server'; <NitroPage page={page} flyo={flyo} />
NitroBlock– Low-level renderer that looks up and renders the registered component for a block. Requiresflyoprop.import { NitroBlock } from '@flyo/nitro-next/server'; <NitroBlock block={block} flyo={flyo} />
NitroSlot– Renders nested blocks from a slot. Used for recursive block rendering. Requiresflyoprop.import { NitroSlot } from '@flyo/nitro-next/server'; <NitroSlot slot={block.slots?.content} flyo={flyo} />
NitroEntityJsonLd– Server component that renders a JSON-LD<script>tag from an Entity'sjsonldfield for structured data / SEO. Safely escapes HTML to prevent XSS.import { NitroEntityJsonLd } from '@flyo/nitro-next/server';
NitroDebugInfo– Async server component that outputs debug information as an HTML comment. Requiresflyoprop.import { NitroDebugInfo } from '@flyo/nitro-next/server'; <NitroDebugInfo flyo={flyo} />
After calling initNitro(), the returned instance exposes:
| Method | Returns | Description |
|---|---|---|
flyo.getNitroConfig() |
Promise<ConfigResponse> |
Fetch the CMS config (React-cached) |
flyo.getNitroPages() |
PagesApi |
Get the Pages API client |
flyo.getNitroEntities() |
EntitiesApi |
Get the Entities API client |
flyo.getNitroSitemap() |
SitemapApi |
Get the Sitemap API client |
flyo.getNitroSearch() |
SearchApi |
Get the Search API client |
flyo.pageResolveRoute(props) |
Promise<{ page, path, cfg }> |
Resolve a page from route params (React-cached) |
flyo.sitemap() |
Promise<MetadataRoute.Sitemap> |
Generate the Next.js sitemap |
flyo.state |
NitroState |
Access the configuration state |
This is a workspace-based project using npm workspaces.
# Install dependencies
npm install
# run dev & start the playground
npm run dev
npm run playground