Layouty

Header i footer to bloki layoutu - dziedziczone w dół drzewa stron i edytowalne jak każdy inny blok.

Header to nie komponent, który wklepujesz na sztywno w app/layout.tsx. W cmssy to blok layoutu: to samo co hero albo tabela cennika - przechowywany w CMS-ie, edytowany w edytorze stron i renderowany przez komponent z Twojego rejestru.

To decyzja projektowa z konsekwencją. Gdyby header był markupem, redaktor nie zmieniłby linku w nawigacji bez deployu. Ponieważ jest blokiem - może.

Pozycje i dziedziczenie

Bloki layoutu żyją na nazwanych pozycjach - domyślnie header i footer, albo tych, które deklaruje Twoja strona (zobacz Regiony layoutu). Strona albo posiada własny layout, albo dziedziczy go po rodzicu, więc ustawienie headera raz na stronie głównej daje go całemu drzewu.

Nadpisuj tylko tam, gdzie sekcja naprawdę się różni: landing bez nawigacji, checkout z okrojonym footerem. Reszta dziedziczy, co oznacza, że jedna edycja aktualizuje każdą stronę, która się nie wypisała.

Dwa root layouty, nie jeden

Route publiczny i route edycyjny mają własne root layouty. Pobierają te same grupy layoutu i renderują je różnymi komponentami, bo header wyrenderowany na serwerze nie da się edytować, a zamontowany na kliencie nie da się zestatycznieć.

Grupy pobierasz sam - jedno zapytanie do API dostawczego, zwracające bloki layoutu dla strony:

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

Layout publiczny

CmssyServerLayout z @cmssy/react renderuje grupy po stronie serwera. Potrzebuje rejestru bloków, bo rozwiązuje typy na serwerze:

// 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}
  />
);

Draft secret przekazywany jest tylko gdy tryb roboczy jest włączony. Przekaż go bezwarunkowo, a Twój publiczny serwis zacznie serwować nieopublikowane zmiany headera.

Layout edycyjny

Route edycyjny renderuje te same grupy przez Twój wrapper kliencki:

// 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 }}
  />
);

Zwróć uwagę, czego brakuje: nie ma propa blocks. Rejestr ładuje leniwie na kliencie sam wrapper:

// 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")} />;
}

Leniwe ładowanie nie jest tu optymalizacją, tylko granicą. Moduły bloków mogą zawierać loadery serwerowe czytające konfigurację i zależności wyłącznie serwerowe; łapczywy import rejestru z komponentu klienckiego wciągnąłby to wszystko do bundla przeglądarki.

Oba root layouty muszą ustawiać <html lang> z rozwiązanego locale - w tym ten edycyjny, bo podgląd musi deklarować język, w którym faktycznie się renderuje.

Dlaczego to psuje się po cichu

Pomiń editable, a wszystko dalej wygląda poprawnie. Serwis się buduje, odwiedzający widzą właściwy header, testy przechodzą.

Ale w edytorze header jest teraz markupem wyrenderowanym na serwerze. Da się go zaznaczyć i nie ma żadnych pól - edytor podkreśli go i nie zmieni niczego. Bez błędu, bez ostrzeżenia, bez czerwonego builda.

Dokładnie to sprawdza checkCmssyEditMode i dlatego jego warunek sukcesu czyta się na opak: brak <header> w HTML-u z serwera dla żądania w trybie edycji to stan poprawny. Zobacz testowanie.

Następne kroki