npm install @flyo/nitro-nextCreate a flyo.config.tsx file to configure the library and your components.
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';
// Get configuration from environment variables
const accessToken = process.env.FLYO_ACCESS_TOKEN || '';
const liveEdit = process.env.FLYO_LIVE_EDIT === 'true';
const baseUrl = process.env.SITE_URL || 'http://localhost:3000';
export const flyoConfig = initNitro({
// API token for authenticating with the Flyo CMS
accessToken: accessToken,
// Language code for content retrieval
lang: 'en',
// Base URL for your site (used for sitemap generation, canonical URLs, etc.)
baseUrl: baseUrl,
// Enable live editing mode - when true, wraps your app with FlyoClientWrapper for real-time content updates
liveEdit: liveEdit,
// Server/CDN cache TTL in seconds (default: 1200 = 20 minutes)
serverCacheTtl: 1200,
// Client browser cache TTL in seconds (default: 900 = 15 minutes)
clientCacheTtl: 900,
// Map of CMS block types to React components - register all custom components here
components: {
HeroBanner: HeroBanner,
Text: Text
}
});
/**
* Pre-configured Flyo component
*
* This component initializes the Flyo Nitro CMS with your configuration.
* Wrap your app with this component in your root layout.
*/
export function Flyo({ children }: { children: ReactNode }) {
flyoConfig();
if (liveEdit) {
return <FlyoClientWrapper>{children}</FlyoClientWrapper>;
}
return children;
}Create a proxy.ts file in the src/ directory to handle cache control:
import { createProxy } from '@flyo/nitro-next/proxy';
import { flyoConfig } from './flyo.config';
export default createProxy(flyoConfig());
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)
Wrap your application with the provider in app/layout.tsx.
import { Flyo } from '@/flyo.config';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<Flyo>
<html>
<body>{children}</body>
</html>
</Flyo>
);
}Create a catch-all route in app/[[...slug]]/page.tsx to handle dynamic pages.
// Re-export the Nitro route handlers for a one-liner setup
export {
nitroPageRoute as default,
nitroPageGenerateMetadata as generateMetadata,
// 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
// - Makes the preview frame unusable (you won't see changes anymore)
//
// To enable in production only, use a conditional export:
// ...(process.env.FLYO_LIVE_EDIT !== 'true' && {
// generateStaticParams: nitroPageGenerateStaticParams
// })
//
// nitroPageGenerateStaticParams as generateStaticParams,
} from "@flyo/nitro-next/server";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.
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/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' }}
/>
);
}Then use it with the WYSIWYG component:
'use client';
import { FlyoWysiwyg } from '@flyo/nitro-next/client';
import CustomImage from './components/CustomImage';
export default function MyComponent({ block }) {
return (
<FlyoWysiwyg
json={block.content.json}
components={{
image: CustomImage
}}
/>
);
}The component will use your custom CustomImage component for all image nodes, and render all other nodes using the default WYSIWYG renderer.
When blocks contain nested blocks in slots, use the NitroSlot component to recursively render them. This is useful for container-like components that can hold other blocks.
import { NitroSlot } from '@flyo/nitro-next/server';
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} />
</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
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,
getNitroEntities,
type EntityResolver
} from "@flyo/nitro-next/server";
import type { Entity } from "@flyo/nitro-typescript";
type RouteParams = {
params: Promise<{ slug: string }>;
};
// Define how to resolve the entity from route params
const resolver: EntityResolver<{ slug: string }> = async (params) => {
const { slug } = await params;
return getNitroEntities().entityBySlug({
slug,
typeId: 123 // Your entity type ID from Flyo
});
};
// Generate metadata for SEO
export const generateMetadata = (props: RouteParams) =>
nitroEntityGenerateMetadata(props, { resolver });
// Render the page
export default function BlogPost(props: RouteParams) {
return nitroEntityRoute(props, {
resolver,
render: (entity: Entity) => (
return (
<article>
<h1>{entity.entity?.entity_title}</h1>
<p>{entity.entity?.entity_teaser}</p>
{/* Access all entity data here */}
</article>
);
)
});
}Create app/items/[uniqueid]/page.tsx:
import {
nitroEntityRoute,
nitroEntityGenerateMetadata,
getNitroEntities,
type Entity,
type EntityResolver
} from "@flyo/nitro-next/server";
type RouteParams = {
params: Promise<{ uniqueid: string }>;
};
const resolver: EntityResolver<{ uniqueid: string }> = async (params) => {
const { uniqueid } = await params;
return getNitroEntities().entityByUniqueid({ uniqueid });
};
export const generateMetadata = (props: RouteParams) =>
nitroEntityGenerateMetadata(props, { resolver });
export default function Item(props: RouteParams) {
return nitroEntityRoute(props, {
resolver,
render: (entity: Entity) => (
return (
<div>
<h1>{entity.entity?.entity_title}</h1>
</div>
)
)
});
}Works with any route parameter name - create app/products/[id]/page.tsx:
import {
nitroEntityRoute,
nitroEntityGenerateMetadata,
getNitroEntities,
type Entity,
type EntityResolver
} from "@flyo/nitro-next/server";
type RouteParams = {
params: Promise<{ id: string }>;
};
const resolver: EntityResolver<{ id: string }> = async (params) => {
const { id } = await params;
// Use the id parameter however you need
return getNitroEntities().entityBySlug({
slug: id,
typeId: 456
});
};
export const generateMetadata = (props: RouteParams) =>
nitroEntityGenerateMetadata(props, { resolver });
export default function Product(props: RouteParams) {
return nitroEntityRoute(props, {
resolver,
render: (entity: Entity) => (
return (
<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 a helper function to automatically generate a Next.js sitemap from your Flyo CMS content. The sitemap includes all pages and mapped entities.
First, ensure your flyo.config.tsx includes the baseUrl parameter:
export const flyoConfig = initNitro({
accessToken: process.env.FLYO_ACCESS_TOKEN || '',
lang: 'en',
baseUrl: process.env.SITE_URL || 'http://localhost:3000', // Required for sitemap
liveEdit: process.env.FLYO_LIVE_EDIT === 'true',
components: {
// your components
}
});Create app/sitemap.ts:
import { nitroSitemap } from '@flyo/nitro-next/server';
import { flyoConfig } from '../flyo.config';
export default async function sitemap() {
return nitroSitemap(flyoConfig());
}- Fetches all content: The
nitroSitemapfunction 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.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';
initNitro(config)– Create and cache the Flyo configuration the rest of the helpers rely on.import { initNitro } from '@flyo/nitro-next/server';
getNitroConfig()– Fetches and caches the Nitro configuration/metadata that describes the available pages.import { getNitroConfig } from '@flyo/nitro-next/server';
getNitroPages()– Factory for the pages API that lets you fetch Nitro page data (used byNitroPage).import { getNitroPages } from '@flyo/nitro-next/server';
getNitroEntities()– Factory for Nitro entities API (available via the Flyo Typescript SDK).import { getNitroEntities } from '@flyo/nitro-next/server';
nitroPageRoute(props)– Default page route handler for Nitro pages. Renders a page from Flyo CMS.import { nitroPageRoute } from '@flyo/nitro-next/server';
nitroPageGenerateMetadata(props)– Generate metadata for Nitro pages using page data from Flyo.import { nitroPageGenerateMetadata } from '@flyo/nitro-next/server';
nitroPageGenerateStaticParams()– Generate static params for all Nitro pages to enable SSG.import { nitroPageGenerateStaticParams } from '@flyo/nitro-next/server';
nitroEntityRoute(props, options)– Flexible entity detail page handler that works with any route param structure. Takes a custom resolver function.import { nitroEntityRoute } from '@flyo/nitro-next/server';
nitroEntityGenerateMetadata(props, options)– Generate metadata for entity detail pages using a custom resolver function.import { nitroEntityGenerateMetadata } from '@flyo/nitro-next/server';
nitroSitemap(state)– Generate a Next.js sitemap from Flyo Nitro content. Takes the Nitro state (fromflyoConfig()) as parameter.import { nitroSitemap } from '@flyo/nitro-next/server';
getNitro()– Access the current Nitro configuration state after initialization.import { getNitro } from '@flyo/nitro-next/server';
createProxy(state)– Create a Next.js middleware for cache control. Takes the Nitro state (fromflyoConfig()) as parameter.import { createProxy } from '@flyo/nitro-next/proxy';
NitroPage– Server component that renders a whole Nitro page by delegating toNitroBlockfor each block.import { NitroPage } from '@flyo/nitro-next/server';
NitroBlock– Low-level renderer that looks up and renders the registered component for a block, or shows a placeholder if missing.import { NitroBlock } from '@flyo/nitro-next/server';
NitroSlot– Renders nested blocks from a slot. Used for recursive block rendering when blocks contain slots with child blocks.import { NitroSlot } from '@flyo/nitro-next/server';
This is a workspace-based project using npm workspaces.
# Install dependencies
npm install
# run dev & start the playground
npm run dev
npm run playground