Modelos para los datos, bloques para la vista
El contenido estructurado vive en modelos del workspace y llega a un bloque mediante fields.relation, nunca como un repeater de registros en las props del bloque.
cmssy divide el contenido de una página en dos capas con dos dueños distintos.
- Los datos son CMS-first. El contenido estructurado y reutilizable -testimonios, preguntas frecuentes, miembros del equipo, oficinas- vive en modelos del workspace y sus registros. Los editores, o un cliente de IA vía MCP, crean el modelo, añaden registros, los reordenan y los traducen, sin desplegar.
- La vista es code-first. Un bloque es un componente de React tipado en tu repositorio. Su esquema
propsguarda lo que pertenece a la vista: textos, variantes, interruptores de maquetación, y referencias a datos víafields.relation.
El puente es fields.relation. El esquema del bloque declara qué modelo renderiza, y el SDK entrega al componente registros totalmente resueltos en el renderizado. El bloque sale con el despliegue; los registros cambian cuando alguien guarda.
El antipatrón: registros dentro de un repeater
Un fields.repeater con elementos { quote, author, role } parece más rápido, y durante una semana lo es. Luego atrapa los datos dentro de una instancia de bloque en una página. No se pueden reutilizar en otra página, ni consultar, ni ordenar, ni referenciar, y cada copia deriva por su cuenta.
La prueba es la propiedad, no la forma. Recurre al repeater solo para estructura local de la vista: una lista de viñetas que no existe en ningún otro sitio. En el momento en que los elementos son contenido con identidad -cosas que añadirías, traducirías o reutilizarías al margen de esta página- son registros de un modelo.
fields.relation
fields.relation({
label: "Testimonials",
model: "testimonial", // el slug del modelo (obligatorio)
mode: "all", // vincular todos los registros; omítelo para elección manual
sort: "order_asc", // <fieldKey>_asc / <fieldKey>_desc
limit: 12, // tope de la colección (50 por defecto)
multiple: true, // modo elección: elegir varios en vez de uno
});Hay dos modos, y elegir entre ellos es preguntarse quién decide qué aparece:
mode: "all": el campo queda ligado a la lista de registros del modelo. No hay nada que elegir en el editor;sortylimitdan forma a la lista. Úsalo para secciones de "renderizarlos todos": FAQ, logos, testimonios.- elección (el valor por defecto): el editor elige un registro, o varios con
multiple: true. Úsalo cuando decide la página: un caso destacado, los tres planes de una página de precios.
Qué recibe el componente
El valor almacenado es un id de registro, o un array de ellos. Pero el SDK resuelve las relaciones en el servidor, antes de que el componente renderice, así que tu componente nunca ve un id:
mode: "all"omultiple→CmssyModelRecord[]- elección única →
CmssyModelRecordoundefined
Los campos de un registro viven bajo record.data como un mapa fieldKey → valor tipado Record<string, unknown>. Estrecha cada valor antes de renderizar y usa record.id como clave de React.
Una elección única es undefined cuando la referencia queda colgando: el registro se borró. Ninguna marca required puede descartarlo, así que el componente debe renderizar con sensatez sin él.
Cómo funciona la resolución
La resolución es una pasada agrupada por página, antes de que corra ningún loader de servidor. Los ids elegidos pasan por public.model.recordsByIds; los campos mode: "all" por public.model.records, deduplicados por modelo más orden más límite. Ambos llevan el locale de la página.
Una relación nunca rompe un renderizado. Un fallo de fetch o un id colgante degrada a lista vacía o valor ausente, y registra [cmssy] relation resolution failed en el servidor. El lienzo del editor renderiza sin la resolución de servidor, así que allí el valor también tiene la forma degradada: otra razón para que el componente no dé por supuestos los datos.
De principio a fin: testimonios
Un modelo, un bloque, sin loader.
1. El modelo, CMS-first. En el panel -o vía MCP con create_model- crea testimonial con los campos quote (textarea), author (text), role (text), order (number). Añade registros. Esto es introducir datos, no desplegar.
2. El bloque, code-first. El esquema declara la relación; el componente renderiza los registros que lleguen:
// 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,
});Añadir un testimonio es ahora introducir datos. Sin pull request, sin despliegue, sin desarrollador. De eso trata la separación.
Siguientes pasos
- Esquema de bloque y tipos de campo: todos los tipos, incluido
relation. - API de entrega GraphQL: consultar registros tú mismo cuando una relación no basta.
- Loaders de servidor: para datos que las relaciones no expresan.