Teraz z AI - twórz strony przez serwer MCP

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 - header i footer. 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