Components
How CMS component types map to React components, and how to add a new block to a site.
This page covers how the component registry works, what happens to unmatched components, and how to add a new block to the site.
The registry
In the site template, the only place that connects a CMS component to a React component is the COMPONENT_REGISTRY object in src/components/cms/DynamicComponent.tsx.
const COMPONENT_REGISTRY = {
"top-banner": () => Promise.resolve(null),
custom: () => import("./CustomComponent"),
"markdown-content": () => import("./MarkdownContent"),
form: () => import("./FormContainer"),
} as const;Each key is a component slug. Each value is an import() function that lazy-loads the component.
Matching order
DynamicComponent looks at the node's slug first. If there is no slug, it uses the lowercased componentType. Keys are compared in lowercase.
| Node | Key used |
|---|---|
{ "slug": "hero-banner", "componentType": "Custom" } | hero-banner |
{ "componentType": "Form" } | form |
Built-in blocks
| Slug | What it does |
|---|---|
markdown-content | Renders Markdown with GFM as a prose section. |
form | FormContainer: builds fields from the CMS form definition, validates, and submits. |
custom | Debug card. The fallback for unknown types. |
top-banner | Placeholder. Renders nothing. |
Props passed to the component
DynamicComponent passes every field of the node as props: children, data, uniqueId, slug, title, and the rest. It also adds pageData, which is the page response itself plus a shareUrl field. Ignore the fields you don't need.
Unmatched components
The fallback is visible on the live site
A slug with no registry entry falls back to CustomComponent, a blue debug card that lists every field. It shows up on the live site too, so register every block you use in production.
If a component's import() fails, a red Error loading component box is rendered in its place and the error is written to the server log.
Adding a new block
The whole contract is one registry line and one file under src/components/cms/. There is no other place to register anything.
Create the component in the panel
In the panel's component catalog, create a new component and give it a slug, for example hero-banner. Define its children: title, description, image.
Write the React component
Create src/components/cms/HeroBanner.tsx with a default export. Keep it a server component unless you need state or effects.
import type { ApiComponent } from "@/types/cms";
interface HeroBannerProps {
children?: ApiComponent[];
pageData?: Record<string, unknown>;
[key: string]: unknown; // ignore fields you don't use
}
const pick = (c: ApiComponent[] | undefined, slug: string) =>
c?.find((x) => x.slug === slug)?.data as string | undefined;
export default function HeroBanner({ children }: HeroBannerProps) {
const title = pick(children, "title");
if (!title) return null; // missing data: render nothing
const description = pick(children, "description");
return (
<section className="py-16">
<h1 className="text-4xl font-bold">{title}</h1>
{description && <p className="mt-4 text-muted-foreground">{description}</p>}
</section>
);
}Add it to the registry
const COMPONENT_REGISTRY = {
// …
"hero-banner": () => import("./HeroBanner"),
} as const;Place it on a page and check
Add the component to a page in the panel and open it on the preview site. You should see your component instead of the blue debug card.
Inspector support
On the preview site, the panel can select a component when you click it on the page. DynamicComponent already marks every top-level block. To make inner elements selectable too, spread cmpAttrs onto them:
import { cmpAttrs } from "@/lib/inspector";
// …
<li key={item.uniqueId} {...cmpAttrs(item.uniqueId, item.slug)}>
{/* … */}
</li>cmpAttrs returns an empty object when NEXT_PUBLIC_INSPECTOR_ENABLED is off, so no attributes reach the live site.
Tips
- Any field can be missing. Components carry their own defaults.
- Find children by
slug, not by position. - Use
uniqueIdas thekeyin lists, notid. - Add
"use client"only when you need state, effects, or a browser API.