Skip to content

flyocloud/nitro-next

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

39 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flyo Nitro for Next.Js

Flyo      Next.js

Usage

1. Installation

npm install @flyo/nitro-next

2. Configuration

Create 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;
}

3. Setup Proxy

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 (serverCacheTtl and clientCacheTtl)

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)

4. Setup Layout

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>
  );
}

5. Create Page

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";

6. Create Custom Components

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.

7. WYSIWYG Component

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.

8. Nested Blocks (Slots)

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 NitroSlot can 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

9. Entity Detail Pages

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.

Example 1: Entity by Slug

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>
      );
    )
  });
}

Example 2: Entity by Unique ID

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>
      )
    )
  });
}

Example 3: Custom Route Parameter Name

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>
      )
    )
  });
}

How it Works

  1. Type-safe params: Define your route params type to match your Next.js route structure
  2. Custom resolver: Write a function that takes the params and returns an entity
  3. Automatic caching: The resolver is automatically wrapped with React cache - it's called once per unique params
  4. Shared resolution: Both nitroEntityRoute and nitroEntityGenerateMetadata use the same cached result
  5. 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!

10. Sitemap Generation

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.

Setup

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 Sitemap File

Create app/sitemap.ts:

import { nitroSitemap } from '@flyo/nitro-next/server';
import { flyoConfig } from '../flyo.config';

export default async function sitemap() {
  return nitroSitemap(flyoConfig());
}

How it Works

  1. Fetches all content: The nitroSitemap function fetches all pages and entities from the Flyo Nitro sitemap endpoint
  2. Uses configured baseUrl: It constructs full URLs using the baseUrl from your Nitro configuration
  3. Handles routes: Prioritizes the routes object from entities, falls back to entity_slug
  4. Returns Next.js format: Outputs the standard MetadataRoute.Sitemap format that Next.js expects

Environment Variables

Set the SITE_URL environment variable for production:

# .env.production
SITE_URL=https://yourdomain.com

Next.js will automatically serve the sitemap at /sitemap.xml.

API Reference

Client Exports

  • editable(block) – Returns the data-flyo-uid attributes 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';

Server Exports

  • 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 by NitroPage).
    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 (from flyoConfig()) 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 (from flyoConfig()) as parameter.
    import { createProxy } from '@flyo/nitro-next/proxy';
  • NitroPage – Server component that renders a whole Nitro page by delegating to NitroBlock for 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';

Development

This is a workspace-based project using npm workspaces.

# Install dependencies
npm install

# run dev & start the playground
npm run dev
npm run playground