cms-renderer
v0.9.0
Published
Readme
cms-renderer
cms-renderer is a library for rendering CMS-powered routes inside a Next.js app.
It provides:
- A catch-all route component that fetches CMS routes and blocks from the CMS API and renders them with your React component registry
- A proxy helper for mounting the CMS admin and API behind your app
- Component-fetching utilities for headless document access
- Docs-oriented markdown rendering and styles
- Type exports for block registries and route params
It is meant to be embedded in an existing Next.js app. It is not a standalone website.
Install
npm install cms-rendererThis package is intended for React + Next.js applications that can reach a running CMS deployment.
Exported Entry Points
Main entry points:
cms-renderer/lib/rendererfor catch-all route renderingcms-renderer/lib/typesfor block and route typescms-renderer/lib/proxyfor proxying CMS admin and API trafficcms-renderer/lib/schemafor headless component-based readscms-renderer/lib/custom-schemasfor fetching custom component metadata and generating Zod codecms-renderer/lib/docs-markdownandcms-renderer/styles/docs-markdown.cssfor docs markdown renderingcms-renderer/lib/refresherfor client-side refresh on CMS updatescms-renderer/lib/ai-previewfor headless AI preview rendering
Additional utility exports:
cms-renderer/lib/block-renderercms-renderer/lib/block-toolbarcms-renderer/lib/client-editable-blockcms-renderer/lib/cms-apicms-renderer/lib/markdown-utilscms-renderer/lib/resultcms-renderer/lib/trpccms-renderer/lib/image/lazy-load
Core Usage
1. Render CMS Routes in a Catch-All Next.js Page
Use the default export from cms-renderer/lib/renderer inside a catch-all route such as app/[...slug]/page.tsx.
import ParametricRoutePage from 'cms-renderer/lib/renderer';
import type { BlockComponentRegistry } from 'cms-renderer/lib/types';
import HeaderBlock from '@/components/HeaderBlock';
const registry: Partial<BlockComponentRegistry> = {
header: HeaderBlock,
};
interface PageProps {
params: Promise<{ slug: string[] }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function Page({ params, searchParams }: PageProps) {
const { slug } = await params;
return (
<ParametricRoutePage
registry={registry}
cmsUrl={process.env.NEXT_PUBLIC_CMS_URL!}
apiKey={process.env.CMS_API_KEY}
websiteId={process.env.NEXT_PUBLIC_WEBSITE_ID}
params={Promise.resolve({ slug })}
searchParams={searchParams}
/>
);
}What the route component does:
- Resolves the incoming path from the catch-all slug
- Fetches the route definition from the CMS API
- Fetches each referenced block
- Maps each block to your registry by
schema_name - Renders built-in and custom block types
- Supports
edit_modeandai_previewquery params
2. Register Block Components
Block components receive content and optional routeParams.
import type { BlockComponentProps } from 'cms-renderer/lib/types';
type HeroProps = BlockComponentProps<{
headline: string;
subheadline?: string;
}>;
export default function HeroBlock({ content }: HeroProps) {
return (
<section>
<h1>{content.headline}</h1>
{content.subheadline ? <p>{content.subheadline}</p> : null}
</section>
);
}Useful types are exported from cms-renderer/lib/types:
BlockDataBlockComponentProps<T>BlockComponentRegistryResolvedRouteParams
3. Configure Environment
Required for route rendering:
NEXT_PUBLIC_CMS_URL=https://cms.example.com
NEXT_PUBLIC_WEBSITE_ID=00000000-0000-0000-0000-000000000000Optional:
CMS_API_KEY=your-api-key
ADMIN_UPSTREAM_ORIGIN=https://cms.example.com
NEXT_PUBLIC_CMS_API_URL=https://cms.example.comParametricRoutePage requires:
cmsUrl, passed as a prop- a website ID, passed as a prop or available through environment variables
websiteId can be passed explicitly to ParametricRoutePage, or it can be read from:
NEXT_PUBLIC_WEBSITE_IDWEBSITE_IDCMS_WEBSITE_ID
Use CMS_API_KEY when your CMS requires authenticated access.
The default schema / component exports from cms-renderer/lib/schema read NEXT_PUBLIC_CMS_API_URL on first use (lazy), not when the module loads — so importing only configureSchema never touches that env. If you call configureSchema(...) directly, pass cmsUrl explicitly instead.
If no website ID is available, ParametricRoutePage throws at runtime.
CMS Proxy
Use createCmsProxy when your app needs to proxy /admin, /api, /auth, and related CMS assets to an upstream CMS deployment.
Use it from proxy.ts or middleware.ts, depending on how your Next.js app is structured.
import { createCmsProxy, cmsProxyMatcher } from 'cms-renderer/lib/proxy';
import type { NextRequest } from 'next/server';
const cmsProxy = createCmsProxy({
upstream: process.env.ADMIN_UPSTREAM_ORIGIN,
});
export async function middleware(request: NextRequest) {
return cmsProxy(request);
}
export const config = {
matcher: cmsProxyMatcher,
};The proxy helper:
- Forwards
/admin,/api, and/auth - Can proxy additional custom path prefixes
- Rewrites redirects back to the current host when appropriate
- Rewrites cookies for the current deployment host
- Proxies admin-originated static asset requests
Headless Component Access
Use configureSchema or the default component export when you want published documents for a custom component outside the block renderer.
import { configureSchema } from 'cms-renderer/lib/schema';
const cms = configureSchema({
cmsUrl: process.env.NEXT_PUBLIC_CMS_URL!,
apiKey: process.env.CMS_API_KEY,
websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID,
});
const posts = await cms.name('post').fetchAll();
const post = await cms.name('post').fetchSingleById('document-id');
const frenchPosts = await cms.name('post').translation('fr').fetchAll();Available methods:
fetchAll()fetchSingle()fetchSingleById(id)translation(language)
If you use the default component export, it reads NEXT_PUBLIC_CMS_API_URL from the environment on first access. If you call configureSchema(...) directly, pass the CMS origin/base URL as cmsUrl.
Offloading reads to the dataset service
Pass datasetEndpoint to serve published document reads from the standalone dataset service (the Rust port of /api/schemas running on the translation_manager EC2 host) instead of the CMS app:
const cms = configureSchema({
datasetEndpoint: process.env.DATASET_ENDPOINT, // e.g. https://datasets.tryprofound.com
apiKey: process.env.CMS_API_KEY,
websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID,
});When set, fetch* calls hit ${datasetEndpoint}/dataset/{schema} (same query params and x-api-key header as the CMS route), and cmsUrl becomes optional. datasetEndpoint is part of the shared CmsConfig, so a single config object can be spread into both configureSchema and ParametricRoutePage.
ParametricRoutePage honors datasetEndpoint too: route resolution stays on tRPC (cmsUrl), but the published content of each resolved route-param document is read from the dataset service instead of the published_content bundled in the tRPC payload — moving that read to the dedicated pool. If the dataset read fails, it falls back to the bundled content, so the dataset service is never on the critical rendering path. Preview/draft routes always use cmsUrl, since the dataset service only serves published documents.
Custom Component Code Generation
Use cms-renderer/lib/custom-schemas to fetch custom component metadata and generate typed Zod component code for your app. The helper names in this module still use Schema for compatibility.
import {
fetchAllCustomSchemaFields,
saveZodSchemaCode,
} from 'cms-renderer/lib/custom-schemas';
const components = await fetchAllCustomSchemaFields({
cmsUrl: process.env.NEXT_PUBLIC_CMS_URL!,
apiKey: process.env.CMS_API_KEY,
websiteId: process.env.NEXT_PUBLIC_WEBSITE_ID!,
});
await saveZodSchemaCode(components, 'generated/cms-schemas.ts');Also exported:
fetchCustomSchemaFields(options, schemaName)buildZodSchemas(schemas)
Headless AI Preview
renderParametricRoute supports an ai_preview=<n> query param that pulls AI-generated block variants from the CMS API. When you want to render preview content headlessly — you already have the block content in hand (from a generation webhook, an agent, your own preview endpoint) and don't want a CMS round-trip, query params, or edit-mode overlays — use aiPreview from cms-renderer/lib/ai-preview.
It dispatches through the same component registry as the catch-all route, so previews match production exactly.
import { aiPreview } from 'cms-renderer/lib/ai-preview';
import type { BlockComponentRegistry } from 'cms-renderer/lib/types';
import HeroBlock from '@/components/HeroBlock';
const registry: Partial<BlockComponentRegistry> = {
'hero-block': HeroBlock,
};
// `blocks` is content you already have — e.g. the JSON an agent generated.
export default function AiPreviewPage({ blocks }) {
return aiPreview(blocks, { registry });
}aiPreview(content, options):
content— a single block or an array of blocks, each shaped as{ id?, type, content, layout? }.typeselects the component from the registry;contentand optional visuallayoutmetadata are passed straight to it.iddefaults to the block's index.options.registry— yourBlockComponentRegistry(required).options.routeParams— optional resolved route params, exposed to components asrouteParams.options.path— optional current path, enabling path-namespaced registry keys like"/{lang}/blog ArticleBlock".
Edit overlays are always disabled and nothing is fetched, so the output is plain, public-shaped markup. Unknown block types are skipped (with a development-only warning).
Docs Markdown
cms-renderer includes a docs-oriented markdown boundary for starter apps and docs sites.
import { DocsMarkdown } from 'cms-renderer/lib/docs-markdown';
import 'cms-renderer/styles/docs-markdown.css';
export default async function Page() {
return <DocsMarkdown content={'# Hello\n\nThis came from the CMS.'} />;
}DocsMarkdown supports:
- WASM-backed markdown parsing
- Syntax-highlighted code blocks
- Optional custom image rendering via
renderImage - Default styling through
styles/docs-markdown.css
Live Refresh
Use Refresher in client components to refresh the current route when CMS content changes.
You can also provide onInvalidate to run app-specific invalidation before router.refresh().
'use client';
import { Refresher } from 'cms-renderer/lib/refresher';
export function CmsLiveRefresh() {
return (
<Refresher
cmsUrl={process.env.NEXT_PUBLIC_CMS_URL!}
websiteId={process.env.NEXT_PUBLIC_WEBSITE_ID!}
apiKey={process.env.NEXT_PUBLIC_CMS_API_KEY}
/>
);
}Edit Mode
When the page is rendered with edit_mode=true or edit_mode=1, the renderer enables editable wrappers and block toolbar behavior for CMS iframe editing.
In normal public traffic, you do not need to do anything special for edit mode. This mode is intended for CMS-integrated editing flows, not for public pages.
Contributor Notes
Useful package scripts:
bun run build
bun run check-types
bun run lint
bun run test
bun run test:coveragedist/ is generated by tsup during build and publish.
