Project setup for the renderer
A renderer project is usually made of two route surfaces:
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.
Pull the renderer into your project with npm or bun:
npm install cms-renderer
or:
bun add cms-renderer
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
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.
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 })}
/>
);
}
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}
/>
);
}
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.tsand import it from both routes.