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
propstrzyma to, co należy do widoku: tekst, warianty, przełączniki układu - oraz referencje do danych przezfields.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;sortilimitkształ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"albomultiple→CmssyModelRecord[]- pojedynczy wybór →
CmssyModelRecordalboundefined
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
- Schemat bloku i typy pól - wszystkie typy pól, w tym
relation. - API dostawcze GraphQL - odpytywanie rekordów samodzielnie, gdy relacja nie wystarcza.
- Loadery serwerowe - do danych, których relacje nie wyrażą.