Désormais avec création de pages par IA via le serveur MCP

Modèles pour les données, blocs pour la vue

Le contenu structuré vit dans les modèles de l'espace de travail et atteint un bloc via fields.relation - jamais sous forme de repeater d'enregistrements dans les props du bloc.

cmssy sépare le contenu d'une page en deux couches ayant deux propriétaires différents.

  • Les données sont CMS-first. Le contenu structuré et réutilisable - témoignages, entrées de FAQ, membres de l'équipe, bureaux - vit dans les modèles de l'espace de travail et leurs enregistrements. Les éditeurs, ou un client IA via MCP, créent le modèle, ajoutent des enregistrements, les réordonnent et les traduisent, sans déploiement.
  • La vue est code-first. Un bloc est un composant React typé dans votre dépôt. Son schéma props contient ce qui relève de la vue : textes, variantes, options de mise en page - et des références aux données via fields.relation.

Le pont est fields.relation. Le schéma du bloc déclare quel modèle il affiche, et le SDK remet au composant des enregistrements entièrement résolus au moment du rendu. Le bloc part avec le déploiement ; les enregistrements changent dès qu'un éditeur enregistre.

L'anti-motif : des enregistrements dans un repeater

Un fields.repeater contenant des éléments { quote, author, role } paraît plus rapide, et pendant une semaine environ il l'est. Ensuite il enferme les données dans une instance de bloc sur une seule page. Impossible de les réutiliser ailleurs, de les interroger, de les trier ou de les référencer - et chaque copie dérive de son côté.

Le critère est la propriété, pas la forme. Réservez le repeater à une structure locale à la vue : une liste de points qui n'existe nulle part ailleurs. Dès que les éléments sont du contenu doté d'une identité - des choses que vous ajouteriez, traduiriez ou réutiliseriez indépendamment de cette page - ce sont des enregistrements d'un modèle.

fields.relation

fields.relation({
  label: "Testimonials",
  model: "testimonial", // le slug du modèle (requis)
  mode: "all",          // lier tous les enregistrements ; omettre pour choix éditeur
  sort: "order_asc",    // <fieldKey>_asc / <fieldKey>_desc
  limit: 12,            // plafond de la collection (50 par défaut)
  multiple: true,       // mode choix : en sélectionner plusieurs
});

Il y a deux modes, et choisir entre eux revient à se demander qui décide de ce qui apparaît :

  • mode: "all" - le champ est lié à la liste d'enregistrements du modèle. Rien à sélectionner dans l'éditeur ; sort et limit façonnent la liste. Pour les sections « tout afficher » : FAQ, logos, témoignages.
  • choix (par défaut) - l'éditeur sélectionne un enregistrement, ou plusieurs avec multiple: true. Quand c'est la page qui décide : une étude de cas mise en avant, les trois offres d'une page tarifs.

Ce que reçoit le composant

La valeur stockée est un identifiant d'enregistrement, ou un tableau d'identifiants. Mais le SDK résout les relations côté serveur, avant le rendu du composant : votre composant ne voit donc jamais d'identifiant :

  • mode: "all" ou multipleCmssyModelRecord[]
  • choix unique → CmssyModelRecord ou undefined

Les champs d'un enregistrement vivent sous record.data, une map fieldKey → valeur typée Record<string, unknown>. Restreignez chaque valeur avant l'affichage et utilisez record.id comme clé React.

Un choix unique vaut undefined quand la référence pend dans le vide - l'enregistrement a été supprimé. Aucun drapeau required ne peut l'exclure : le composant doit donc s'afficher correctement sans lui.

Comment fonctionne la résolution

La résolution est une passe groupée par page, avant l'exécution de tout loader serveur. Les identifiants choisis passent par public.model.recordsByIds ; les champs mode: "all" par public.model.records, dédoublonnés par modèle, tri et limite. Les deux portent la locale de la page.

Une relation ne casse jamais un rendu. Un échec de récupération ou un identifiant orphelin dégrade vers une liste vide ou une valeur absente, et journalise [cmssy] relation resolution failed côté serveur. Le canevas de l'éditeur s'affiche sans la résolution serveur : la valeur y a donc aussi la forme dégradée - une raison de plus pour que le composant ne présuppose pas la présence des données.

De bout en bout : les témoignages

Un modèle, un bloc, aucun loader.

1. Le modèle, CMS-first. Dans l'admin - ou via MCP avec create_model - créez testimonial avec les champs quote (textarea), author (text), role (text), order (number). Ajoutez des enregistrements. C'est de la saisie, pas un déploiement.

2. Le bloc, code-first. Le schéma déclare la relation ; le composant affiche les enregistrements qui arrivent :

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

Ajouter un témoignage est désormais de la saisie. Pas de pull request, pas de déploiement, pas de développeur. C'est tout l'intérêt de la séparation.

Étapes suivantes