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

Modelle für Daten, Blöcke für die Ansicht

Strukturierte Inhalte leben in Workspace-Modellen und erreichen einen Block über fields.relation - nie als Repeater von Records in Block-Props.

cmssy teilt den Inhalt einer Seite in zwei Schichten mit zwei verschiedenen Eigentümern.

  • Daten sind CMS-first. Strukturierte, wiederverwendbare Inhalte - Testimonials, FAQ-Einträge, Teammitglieder, Standorte - leben in Workspace-Modellen und deren Records. Redaktionen oder ein KI-Client über MCP legen das Modell an, ergänzen Records, sortieren und übersetzen sie - ohne Deploy.
  • Die Ansicht ist code-first. Ein Block ist eine typisierte React-Komponente in deinem Repo. Sein props-Schema enthält, was zur Ansicht gehört: Texte, Varianten, Layout-Schalter - und Referenzen auf Daten via fields.relation.

Die Brücke ist fields.relation. Das Schema des Blocks deklariert, welches Modell er rendert, und das SDK reicht der Komponente beim Rendern vollständig aufgelöste Records. Der Block geht mit dem Deploy raus; die Records ändern sich, sobald jemand speichert.

Das Anti-Pattern: Records in einem Repeater

Ein fields.repeater mit { quote, author, role }-Einträgen wirkt schneller, und ungefähr eine Woche lang ist er das auch. Dann sperrt er die Daten in eine Block-Instanz auf einer Seite. Sie lassen sich nicht auf einer anderen Seite wiederverwenden, nicht abfragen, nicht sortieren, nicht referenzieren - und jede Kopie driftet für sich.

Der Test ist die Eigentümerschaft, nicht die Form. Greif zum Repeater nur für ansichtslokale Struktur: eine Liste von Feature-Punkten, die es sonst nirgends gibt. Sobald Einträge Inhalt mit Identität sind - Dinge, die du unabhängig von dieser Seite ergänzen, übersetzen oder wiederverwenden würdest - sind es Records eines Modells.

fields.relation

fields.relation({
  label: "Testimonials",
  model: "testimonial", // Slug des Modells (erforderlich)
  mode: "all",          // alle Records binden; weglassen für Editor-Auswahl
  sort: "order_asc",    // <fieldKey>_asc / <fieldKey>_desc
  limit: 12,            // Obergrenze der Sammlung (Standard 50)
  multiple: true,       // Auswahlmodus: mehrere statt eines
});

Es gibt zwei Modi, und die Wahl dazwischen ist eine Frage danach, wer entscheidet, was erscheint:

  • mode: "all" - das Feld ist an die Record-Liste des Modells gebunden. Im Editor gibt es nichts auszuwählen; sort und limit formen die Liste. Nutze es für "alle rendern"-Sektionen: FAQ, Logos, Testimonials.
  • Auswahl (Standard) - die Redaktion wählt einen Record, oder mehrere mit multiple: true. Nutze es, wenn die Seite entscheidet: eine hervorgehobene Case Study, die drei Pläne auf einer Preisseite.

Was die Komponente erhält

Gespeichert wird eine Record-ID oder ein Array davon. Doch das SDK löst Relationen serverseitig auf, bevor die Komponente rendert - deine Komponente sieht also nie eine ID:

  • mode: "all" oder multipleCmssyModelRecord[]
  • einzelne Auswahl → CmssyModelRecord oder undefined

Die Felder eines Records liegen unter record.data als fieldKey → Wert-Map vom Typ Record<string, unknown>. Verenge jeden Wert vor dem Rendern und nutze record.id als React-Key.

Eine einzelne Auswahl ist undefined, wenn die Referenz ins Leere zeigt - der Record wurde gelöscht. Kein required-Flag schließt das aus, die Komponente muss also auch ohne ihn sinnvoll rendern.

Wie die Auflösung funktioniert

Die Auflösung ist ein gebatchter Durchlauf pro Seite, bevor irgendein Server-Loader läuft. Ausgewählte IDs gehen über public.model.recordsByIds; mode: "all"-Felder über public.model.records, dedupliziert je Modell plus Sortierung plus Limit. Beide tragen das Locale der Seite.

Eine Relation bricht nie ein Rendering. Ein Fetch-Fehler oder eine ins Leere zeigende ID degradiert zu einer leeren Liste oder einem fehlenden Wert und loggt serverseitig [cmssy] relation resolution failed. Die Editor-Leinwand rendert ohne die serverseitige Auflösung, dort hat der Wert also ebenfalls die degradierte Form - ein weiterer Grund, warum die Komponente Daten nicht voraussetzen darf.

Durchgespielt: Testimonials

Ein Modell, ein Block, kein Loader.

1. Das Modell, CMS-first. Im Admin - oder über MCP mit create_model - lege testimonial an, mit den Feldern quote (textarea), author (text), role (text), order (number). Füge Records hinzu. Das ist Dateneingabe, kein Deploy.

2. Der Block, code-first. Das Schema deklariert die Relation; die Komponente rendert die Records, die ankommen:

// 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. Ein Testimonial hinzufügen ist jetzt Dateneingabe. Kein Pull Request, kein Deploy, keine Entwicklerin. Genau darum geht es bei der Trennung.

Nächste Schritte