Rendering
The catch-all route, page component, metadata, layout, localized routing and ISR revalidation.
The catch-all route
A cmssy workspace stores content; your app renders it. One optional catch-all route - app/[[...path]]/page.tsx - maps every published slug (and its locale prefix) to a page. createCmssyPage fetches the page for the current path, matches each block to your registered component and renders it on the server.
// app/[[...path]]/page.tsx
import { buildCmssyMetadata, createCmssyPage } from "@cmssy/next/server";
import { cmssy } from "@/cmssy/config";
import { blocks } from "@/cmssy/blocks";
export const revalidate = 3600;
export const dynamicParams = true;
const renderPage = createCmssyPage(cmssy, blocks);
type PageProps = { params: Promise<{ path?: string[] }> };
export async function generateMetadata({ params }: PageProps) {
return buildCmssyMetadata(cmssy, (await params).path);
}
export default function Page({ params }: PageProps) {
return renderPage({ params });
}
The catch-all is statically renderable: it never reads
searchParamsorheaders(), so ISR stays on. Draft preview still works per-request throughdraftMode().
Page component & config
createCmssyPage(config, blocks, options?) returns an async server component. config comes from defineCmssyConfig; blocks is the array of defineBlock definitions the SDK matches by type. Pass { editor } to mount the visual editor on the preview route, or { path } to pin a single-page route.
// cmssy/config.ts
import { defineCmssyConfig } from "@cmssy/next";
export const cmssy = defineCmssyConfig({
apiUrl: process.env.CMSSY_API_URL,
org: process.env.CMSSY_ORG_SLUG,
workspaceSlug: process.env.CMSSY_WORKSPACE_SLUG,
draftSecret: process.env.CMSSY_DRAFT_SECRET,
});
Metadata & SEO
buildCmssyMetadata(config, path, options?) returns a complete Next.js Metadata object from the page's SEO fields and workspace branding: title, description, keywords, canonical plus per-locale hreflang alternates, and Open Graph / Twitter cards. Call it from generateMetadata (see the route above).
Pass the catch-all path as routed - locale prefix and all. The prefix is what tells the SDK which language to render; strip it first and every translation inherits the default language's title and a canonical pointing at the default URL, which tells search engines the translation is a duplicate.
buildCmssyMetadata(cmssy, path, {
image: "https://assets.cmssy.io/og.png",
ogType: "article",
});
Header & footer
Shared regions - header, footer - are layout blocks. Render them in app/[[...path]]/layout.tsx: fetchLayouts pulls the layout groups, CmssyServerLayout renders one position, and CmssyLocaleProvider exposes the active locale to client blocks.
// app/[[...path]]/layout.tsx
import { fetchLayouts, resolveSiteLocales, CmssyServerLayout } from "@cmssy/react";
import { splitCmssyLocale } from "@cmssy/core";
import { CmssyLocaleProvider } from "@cmssy/next/client";
import { blocks } from "@/cmssy/blocks";
import { cmssy } from "@/cmssy/config";
export default async function Layout({
children,
params,
}: {
children: React.ReactNode;
params: Promise<{ path?: string[] }>;
}) {
const { path } = await params;
const [{ locale }, siteLocales, groups] = await Promise.all([
splitCmssyLocale(cmssy, path),
resolveSiteLocales(cmssy),
fetchLayouts(cmssy, "/"),
]);
const slot = (position: "header" | "footer") => (
<CmssyServerLayout
groups={groups}
blocks={blocks}
position={position}
locale={locale}
defaultLocale={siteLocales.defaultLocale}
enabledLocales={siteLocales.locales}
/>
);
return (
<html lang={locale}>
<body>
<CmssyLocaleProvider
value={{
current: locale,
default: siteLocales.defaultLocale,
enabled: siteLocales.locales,
}}
>
{slot("header")}
{children}
{slot("footer")}
</CmssyLocaleProvider>
</body>
</html>
);
}
Localized routing
cmssy encodes locale as a path prefix: /about is the default language, /pl/about is Polish. The same catch-all handles both. splitCmssyLocale(config, path) reads the workspace's enabled locales and splits the prefix from the rest - no request or headers, so it stays static-safe.
const { locale, path: rest } = await splitCmssyLocale(cmssy, path);
// ["pl", "about"] -> locale "pl", rest ["about"]
// ["about"] -> locale "en" (default), rest ["about"]
ISR & on-demand revalidation
Published pages are cached and re-generated by ISR via export const revalidate. To refresh instantly on publish, add an on-demand route and point the workspace's content.changed webhook at it. cmssy POSTs the changed path; the route revalidates it, or the whole site (layout scope) when no path is sent. Guard it with a shared secret.
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
import { NextResponse, type NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const secret = process.env.CMSSY_REVALIDATE_SECRET;
const provided =
request.nextUrl.searchParams.get("secret") ??
request.headers.get("x-revalidate-secret");
if (!secret || provided !== secret) {
return NextResponse.json({ revalidated: false }, { status: 401 });
}
const { path } = (await request.json().catch(() => ({}))) as { path?: string };
if (path) revalidatePath(path);
else revalidatePath("/", "layout");
return NextResponse.json({ revalidated: true, path: path ?? "/" });
}
Next steps
- Advanced Block Features - server loaders and data fetching
- Forms & Form Builder
- Theming
- Member Authentication