Renderizado
La ruta catch-all, el componente de página, los metadatos, el layout, el enrutamiento localizado y la revalidación ISR.
La ruta catch-all
Un workspace de cmssy almacena el contenido; tu app lo renderiza. Una ruta catch-all opcional - app/[[...path]]/page.tsx - asigna cada slug publicado (y su prefijo de idioma) a una página. createCmssyPage obtiene la página para la ruta actual, asocia cada bloque a tu componente registrado y lo renderiza en el servidor.
// 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 });
}
La ruta catch-all es renderizable estáticamente: nunca lee
searchParamsniheaders(), así que el ISR sigue activo. La vista previa de borradores sigue funcionando por petición mediantedraftMode().
Componente de página y configuración
createCmssyPage(config, blocks, options?) devuelve un componente de servidor asíncrono. config viene de defineCmssyConfig; blocks es el array de definiciones defineBlock que el SDK asocia por type. Pasa { editor } para montar el editor visual en la ruta de vista previa, o { path } para fijar una ruta de una sola página.
// 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,
});
Metadatos y SEO
buildCmssyMetadata(config, path, options?) devuelve un objeto Metadata de Next.js completo a partir de los campos SEO de la página y el branding del workspace: título, descripción, palabras clave, canonical y las alternativas hreflang por idioma, además de las tarjetas Open Graph / Twitter. Llámalo en generateMetadata (ver la ruta de arriba).
Pasa la ruta catch-all tal como se enrutó - con el prefijo de idioma incluido. El prefijo es lo que le dice al SDK qué idioma renderizar; si lo quitas primero, cada traducción hereda el título del idioma por defecto y un canonical que apunta a la URL por defecto, lo que indica a los buscadores que la traducción es un duplicado.
buildCmssyMetadata(cmssy, path, {
image: "https://assets.cmssy.io/og.png",
ogType: "article",
});
Header y footer
Las regiones compartidas - header, footer - son bloques de layout. Rendérizalas en app/[[...path]]/layout.tsx: fetchLayouts obtiene los grupos de layout, CmssyServerLayout renderiza una position, y CmssyLocaleProvider expone el idioma activo a los bloques de cliente.
// 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>
);
}
Enrutamiento localizado
cmssy codifica el idioma como prefijo de ruta: /about es el idioma por defecto, /pl/about es polaco. La misma ruta catch-all gestiona ambos. splitCmssyLocale(config, path) lee los idiomas habilitados del workspace y separa el prefijo del resto - sin petición ni cabeceras, por lo que es seguro para estático.
const { locale, path: rest } = await splitCmssyLocale(cmssy, path);
// ["pl", "about"] -> locale "pl", rest ["about"]
// ["about"] -> locale "en" (default), rest ["about"]
ISR y revalidación bajo demanda
Las páginas publicadas se cachean y se regeneran mediante ISR con export const revalidate. Para refrescar al instante al publicar, añade una ruta bajo demanda y apunta a ella el webhook content.changed del workspace. cmssy envía por POST la path modificada; la ruta la revalida, o todo el sitio (ámbito layout) cuando no se envía ruta. Protégela con un secreto compartido.
// 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 ?? "/" });
}
Próximos pasos
- Funciones avanzadas de bloques - loaders de servidor y obtención de datos
- Formularios y creador de formularios
- Tema
- Autenticación de miembros