Jetzt mit KI-gestütztem Page Building über den MCP-Server

Layouts

Header und Footer sind Layout-Blöcke - im Seitenbaum vererbt und editierbar wie jeder andere Block.

Ein Header ist keine Komponente, die du fest in app/layout.tsx schreibst. In cmssy ist er ein Layout-Block: dieselbe Art Ding wie ein Hero oder eine Preistabelle - im CMS gespeichert, im Seiten-Editor bearbeitet und von einer Komponente aus deiner Registry gerendert.

Das ist eine Design-Entscheidung mit einer Konsequenz. Wäre der Header Markup, könnte eine Redaktion keinen Navigationslink ohne Deploy ändern. Weil er ein Block ist, kann sie es.

Positionen und Vererbung

Layout-Blöcke liegen an benannten Positionen - header und footer. Eine Seite besitzt entweder ihr eigenes Layout oder erbt es vom Elternknoten; den Header einmal auf der Wurzelseite zu setzen gibt ihn also dem ganzen Baum.

Überschreibe ihn nur dort, wo ein Bereich wirklich abweicht: eine Landingpage ohne Navigation, ein Checkout mit reduziertem Footer. Alles andere erbt - eine Bearbeitung aktualisiert damit jede Seite, die sich nicht abgemeldet hat.

Zwei Root-Layouts, nicht eines

Die öffentliche Route und die Edit-Route haben jeweils ein eigenes Root-Layout. Sie holen dieselben Layout-Gruppen und rendern sie durch verschiedene Komponenten - denn ein servergerenderter Header lässt sich nicht bearbeiten, und ein clientseitig gemounteter nicht statisch ausliefern.

Die Gruppen holst du selbst - eine Delivery-Query, die die Layout-Blöcke einer Seite liefert:

// services/layout.ts
export async function fetchChromeLayouts(
  pageSlug: string,
  previewSecret?: string,
): Promise<CmssyLayoutGroup[]> { /* PublicPageLayouts-Query */ }

Das öffentliche Layout

CmssyServerLayout aus @cmssy/react rendert die Gruppen serverseitig. Es braucht die Block-Registry, weil es Typen auf dem Server auflöst:

// app/[[...path]]/layout.tsx
import { CmssyServerLayout } from "@cmssy/react";

const { isEnabled: draft } = await draftMode();
const [locales, groups] = await Promise.all([
  resolveSiteLocales(),
  fetchChromeLayouts("/", draft ? cmssy.draftSecret : undefined),
]);

const slot = (position: "header" | "footer") => (
  <CmssyServerLayout
    groups={groups}
    blocks={blocks}
    position={position}
    locale={locale}
    defaultLocale={locales.defaultLocale}
    enabledLocales={locales.locales}
  />
);

Das Draft-Secret wird nur übergeben, wenn der Draft-Modus aktiv ist. Übergibst du es bedingungslos, liefert deine öffentliche Site unveröffentlichte Header-Änderungen aus.

Das Edit-Layout

Die Edit-Route rendert dieselben Gruppen stattdessen über deinen Client-Wrapper:

// app/cmssy-edit/[[...path]]/layout.tsx
const slot = (position: "header" | "footer") => (
  <EditableLayout
    groups={groups}
    position={position}
    locale={locale}
    defaultLocale={locales.defaultLocale}
    enabledLocales={locales.locales}
    edit={{ editorOrigin }}
  />
);

Beachte, was fehlt: kein blocks-Prop. Die Registry lädt der Wrapper selbst lazy auf dem Client:

// cmssy/editable-layout.tsx
"use client";
import { CmssyLazyLayout, type CmssyLazyLayoutProps } from "@cmssy/react/client";

export function EditableLayout(props: Omit<CmssyLazyLayoutProps, "load">) {
  return <CmssyLazyLayout {...props} load={() => import("./blocks")} />;
}

Lazy Loading ist hier keine Optimierung, sondern eine Grenze. Block-Module können Server-Loader enthalten, die Config lesen, sowie rein serverseitige Abhängigkeiten; die Registry eager aus einer Client-Komponente zu importieren zieht all das ins Browser-Bundle.

Beide Root-Layouts müssen <html lang> aus dem aufgelösten Locale setzen - auch das Edit-Layout, denn die Vorschau muss die Sprache deklarieren, in der sie tatsächlich rendert.

Warum genau das lautlos bricht

Lässt du editable weg, sieht trotzdem alles richtig aus. Die Site baut, Besucher sehen den korrekten Header, deine Tests laufen grün.

Aber im Editor ist der Header jetzt servergerendertes Markup. Er ist auswählbar und hat keine Felder - der Editor kann ihn markieren und nichts ändern. Kein Fehler, keine Warnung, kein roter Build.

Genau das prüft checkCmssyEditMode, und deshalb liest sich seine Erfolgsbedingung verkehrt herum: kein <header> im servergerenderten HTML einer Edit-Anfrage ist der bestandene Zustand. Siehe Testen.

Nächste Schritte