Teraz z AI - twórz strony przez serwer MCP

Modele na dane, bloki na widok

Treść strukturalna żyje w modelach workspace'u i trafia do bloku przez fields.relation - nigdy jako repeater rekordów w propsach bloku.

cmssy dzieli treść strony na dwie warstwy o dwóch różnych właścicielach.

  • Dane są CMS-first. Strukturalna, wielokrotnego użytku treść - opinie, pytania FAQ, członkowie zespołu, biura - żyje w modelach workspace'u i ich rekordach. Redaktorzy albo klient AI przez MCP tworzą model, dodają rekordy, zmieniają kolejność i tłumaczą - bez deployu.
  • Widok jest code-first. Blok to typowany komponent React w Twoim repo. Jego schemat props trzyma to, co należy do widoku: tekst, warianty, przełączniki układu - oraz referencje do danych przez fields.relation.

Mostem jest fields.relation. Schemat bloku deklaruje, który model renderuje, a SDK podaje komponentowi w pełni rozwiązane rekordy w czasie renderowania. Blok jedzie z deployem; rekordy zmieniają się, kiedy redaktor zapisze.

Antywzorzec: rekordy w repeaterze

fields.repeater trzymający elementy { quote, author, role } wygląda szybciej i przez jakiś tydzień faktycznie jest. Potem zamyka dane w jednej instancji bloku na jednej stronie. Nie da się ich użyć na innej stronie, odpytać, posortować ani zreferencjonować - a każda kopia dryfuje osobno.

Testem jest własność, nie kształt. Sięgaj po repeater wyłącznie dla struktury lokalnej dla widoku: listy punktów, która nie istnieje nigdzie indziej. W momencie, gdy elementy są treścią z tożsamością - czymś, co dodałbyś, przetłumaczył albo użył ponownie niezależnie od tej strony - to są rekordy modelu.

fields.relation

fields.relation({
  label: "Testimonials",
  model: "testimonial", // slug modelu (wymagany)
  mode: "all",          // zwiąż wszystkie rekordy; pomiń dla wyboru w edytorze
  sort: "order_asc",    // <fieldKey>_asc / <fieldKey>_desc
  limit: 12,            // limit kolekcji (domyślnie 50)
  multiple: true,       // tryb wyboru: wybierz wiele zamiast jednego
});

Są dwa tryby, a wybór między nimi to pytanie o to, kto decyduje, co się pojawi:

  • mode: "all" - pole jest związane z listą rekordów modelu. W edytorze nie ma nic do wyboru; sort i limit kształtują listę. Użyj dla sekcji typu "wyrenderuj wszystkie": FAQ, logotypy, opinie.
  • wybór (domyślny) - redaktor wybiera jeden rekord albo kilka przy multiple: true. Użyj, gdy to strona decyduje: wyróżnione case study, trzy plany na cenniku.

Co dostaje komponent

Przechowywana wartość to id rekordu albo ich tablica. Ale SDK rozwiązuje relacje po stronie serwera, zanim komponent się wyrenderuje, więc Twój komponent nigdy nie widzi id:

  • mode: "all" albo multipleCmssyModelRecord[]
  • pojedynczy wybór → CmssyModelRecord albo undefined

Pola rekordu żyją pod record.data jako mapa fieldKey → wartość typowana Record<string, unknown>. Zawęż każdą wartość przed renderem i użyj record.id jako klucza Reacta.

Pojedynczy wybór jest undefined, gdy referencja wisi - rekord został usunięty. Żadna flaga required tego nie wykluczy, więc komponent musi renderować się sensownie bez niego.

Jak działa rozwiązywanie

Rozwiązywanie to jeden zbiorczy przebieg na stronę, przed uruchomieniem jakiegokolwiek loadera serwerowego. Wybrane id idą przez public.model.recordsByIds; pola mode: "all" przez public.model.records, deduplikowane per model plus sort plus limit. Oba niosą locale strony.

Relacja nigdy nie psuje renderu. Błąd pobrania albo wiszące id degraduje się do pustej listy lub braku wartości i loguje [cmssy] relation resolution failed na serwerze. Płótno edytora renderuje się bez rozwiązywania serwerowego, więc tam wartość też ma zdegradowany kształt - kolejny powód, żeby komponent nie zakładał obecności danych.

Od początku do końca: opinie

Jeden model, jeden blok, zero loadera.

1. Model, CMS-first. W panelu - albo przez MCP narzędziem create_model - utwórz testimonial z polami quote (textarea), author (text), role (text), order (number). Dodaj rekordy. To wprowadzanie danych, nie deploy.

2. Blok, code-first. Schemat deklaruje relację; komponent renderuje te rekordy, które przyjdą:

// blocks/testimonials/block.ts
import { defineBlock, fields } from "@cmssy/react";
import Testimonials from "./Testimonials";

export const testimonialsProps = {
  heading: fields.text({ label: "Heading" }),
  items: fields.relation({
    label: "Testimonials",
    model: "testimonial",
    mode: "all",
    sort: "order_asc",
    limit: 12,
  }),
};

export const testimonialsBlock = defineBlock({
  type: "testimonials",
  label: "Testimonials",
  component: Testimonials,
  props: testimonialsProps,
});

3. Dodanie opinii to teraz wprowadzanie danych. Żadnego pull requesta, żadnego deployu, żadnego programisty. O to w tym podziale chodzi.

Następne kroki