profound-logoProfound CMS
⌘K
Admin
Theme
DocsTutorialBlogPhilosophy
DocsTutorialBlogPhilosophy

Blog

Static Blog PostSetup Hybrid CMS ProjectTailwind TokenswitchOur TeamFallow and LLM-assisted cleanup
All Systems Operational
Powered Byprofound-logo
Theme

Setup Hybrid CMS Project

Project setup for the renderer

A renderer project is usually made of two route surfaces:

  • a production route for published pages
  • a preview route for draft previews, hover states, and live editing from the CMS admin panel

In a project, both routes pass a block registry into cms-renderer. The registry maps CMS block schema names, such as header, uisidebar, uicontent, and uifooter, to the React components that render them.


Install Renderer

Pull the renderer into your project with npm or bun:

npm install cms-renderer

or:

bun add cms-renderer

Project Structure

A simple Next.js renderer project can be organized like this:

src/
  app/
    [...slug]/
      page.tsx              # production pages
    cms-preview_/
      [...slug]/
        page.tsx            # draft preview / live editing route
      page.tsx              # preview index route, if needed
  components/
    NavbarBlock.tsx
    UISidebar.tsx
    UIContent.tsx
    UIFooter.tsx
  lib/
    cms-config.ts
    registry.ts

Block Registry

The registry tells the renderer which React component should render each CMS block type.

// lib/registry.ts
import NavbarBlock from '@/components/NavbarBlock';
import UIContent from '@/components/UIContent';
import UIFooter from '@/components/UIFooter';
import UISidebar from '@/components/UISidebar';
import type { BlockComponentRegistry } from 'cms-renderer/lib/types';

export const registry: Partial<BlockComponentRegistry> = {
  // biome-ignore lint/suspicious/noExplicitAny: block props are CMS-defined at runtime
  header: NavbarBlock as any,
  // biome-ignore lint/suspicious/noExplicitAny: block props are CMS-defined at runtime
  uisidebar: UISidebar as any,
  // biome-ignore lint/suspicious/noExplicitAny: block props are CMS-defined at runtime
  uicontent: UIContent as any,
  // biome-ignore lint/suspicious/noExplicitAny: block props are CMS-defined at runtime
  uifooter: UIFooter as any,
};

The keys must match the CMS schema names. The values are CMS-agnostic React components from your app or design system.


Production Route

Use ParametricRoutePage for the public route.

// [...slug]/page.tsx
import ParametricRoutePage from 'cms-renderer/lib/renderer'; // Profound CMS renderer primitives to allow rendering and live previews
import { registry } from '@/lib/registry'; // shared block registry
import { cmsConfig } from '@/lib/cms-config';

export const dynamic = 'force-static';

interface PageProps {
  params: Promise<{ slug: string[] }>;
}

export default async function Page({ params }: PageProps) {
  const { slug } = await params;

  return (
    <ParametricRoutePage
      registry={registry}
      apiKey={cmsConfig.apiKey}
      websiteId={cmsConfig.websiteId}
      cmsUrl={cmsConfig.cmsUrl}
      params={Promise.resolve({ slug })}
    />
  );
}

Preview Route

Use ParametricRoutePreviewPage for the CMS preview route.

// cms-preview_/[...slug]/page.tsx
import { ParametricRoutePreviewPage } from 'cms-renderer/lib/renderer';
import { registry } from '@/lib/registry'; // shared block registry
import { cmsConfig } from '@/lib/cms-config';

// Dynamic rendering - allows searchParams for edit_mode
export const dynamic = 'force-dynamic';

interface PageProps {
  params: Promise<{ slug: string[] }>;
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}

export default async function PreviewPage({ params, searchParams }: PageProps) {
  const { slug } = await params;

  return (
    <ParametricRoutePreviewPage
      registry={registry}
      apiKey={cmsConfig.apiKey}
      {...(cmsConfig.websiteId ? { websiteId: cmsConfig.websiteId } : {})}
      cmsUrl={cmsConfig.cmsUrl}
      params={Promise.resolve({ slug })}
      searchParams={searchParams}
    />
  );
}

Keeping Routes In Sync

Both production and preview routes must use the same registry. If the same registry object is copied into multiple files, remember to update every route whenever you add or rename a CMS block.

For larger projects, move the registry into a shared module such as src/lib/registry.ts and import it from both routes.


Next steps

  1. Add your custom block components to the registry.
  2. Create parametric pages with Components or UI Components in the admin panel.
  3. Set up the admin panel proxy for live editing.
  4. Set up Zod component pull so local component types stay in sync with the latest schema state from the admin panel.