Loaders de servidor
Carga los datos de un bloque durante el renderizado en servidor: indexable, sin parpadeo de carga y sin dependencias de servidor en el bundle del cliente.
El loader de un bloque se ejecuta en el servidor durante el SSR y pasa su resultado al componente como la prop data. Úsalo para traer contenido, ejecutar transformaciones pesadas o llamar a la API de entrega antes de que la página llegue al navegador, en lugar de cargar en el cliente dentro de un useEffect.
De ahí se derivan tres cosas:
- SEO: el contenido está en el HTML renderizado en servidor, así que los crawlers lo ven.
- Sin parpadeo: el bloque se renderiza ya relleno en el primer pintado. Sin esqueletos.
- Bundle de cliente más pequeño: las dependencias solo de servidor, como un resaltador de sintaxis o un saneador de HTML, nunca llegan al navegador.
El contrato
import { defineBlock } from "@cmssy/react";
defineBlock({
type: "my-block",
loader: async ({ content, context }) => {
// solo en servidor durante el SSR; devuelve datos serializables por RSC
return { html: "" };
},
component: MyBlock, // recibe { content, context, data }
});Lo rigen tres reglas:
- El loader corre durante el SSR. No corre en el editor: allí el componente recibe
data: undefined. Renderiza siempre un fallback sensato. - El valor devuelto cruza la frontera servidor-cliente, así que debe ser serializable por RSC: objetos planos, arrays y primitivos. Sin funciones ni instancias de clase.
contentes el contenido resuelto del bloque;contextes el contexto de bloque (locale,isPreview,forms).
Tipa data como opcional y degrada cuando falte:
function MyBlock({
content,
data,
}: {
content: Record<string, unknown>;
data?: { html?: string };
}) {
if (!data?.html) return <pre>{String(content.code ?? "")}</pre>; // fallback del editor
return <div dangerouslySetInnerHTML={{ __html: data.html }} />;
}Mantén el código de servidor fuera del bundle del cliente
Un módulo de bloque también es alcanzable desde el bundle cliente del editor. Si tu loader importa estáticamente una dependencia pesada o solo de servidor, esa dependencia acaba también en el bundle del navegador. Dos salvaguardas lo evitan:
import()dinámico dentro del loader: la dependencia solo se carga cuando el loader realmente se ejecuta.- Una guarda de
windowen tiempo de ejecución en cualquier helper de servidor compartido: un fallo duro si alguna vez se alcanza en el cliente.
// block.ts
loader: async ({ content }) => {
const code = typeof content.code === "string" ? content.code : "";
if (!code) return { html: "" };
const { codeToHtml } = await import("shiki"); // solo servidor, perezoso
return { html: await codeToHtml(code, { lang: "ts", theme: "github-light" }) };
},// load-posts.ts - helper solo de servidor, alcanzado por import() dinámico
import { createCmssyClient } from "@cmssy/react";
import { cmssy } from "@/cmssy/config";
const client = createCmssyClient(cmssy);
export async function loadPosts(vars: { parentSlug: string; limit: number }) {
if (typeof window !== "undefined") {
throw new Error("loadPosts must only run on the server");
}
return client.queryScoped(PUBLIC_PAGES_QUERY, vars);
}El loader se mantiene diminuto; el helper solo se carga en el servidor:
loader: async ({ content }) => {
const parentSlug = resolveParentSlug(content);
if (!parentSlug) return null;
const { loadPosts } = await import("./load-posts");
return loadPosts({ parentSlug, limit: Number(content.postsPerPage) || 9 });
},Llamar a la API de entrega
Usa createCmssyClient(...).queryScoped(...) o graphqlRequest de @cmssy/react.
queryScoped inyecta automáticamente el id del workspace: si tu consulta declara $workspaceId y no lo pasas, el SDK lo resuelve desde tu workspaceSlug y añade tanto la variable como la cabecera x-workspace-id. Nunca gestionas el id del workspace tú mismo.
Híbrido: SSR la primera página, luego cliente
Un loader no tiene que poseer los datos para siempre. Un patrón habitual carga la primera página en el servidor, siembra el estado inicial del hook cliente desde data y deja búsqueda, paginación y filtros al cliente:
function BlogPosts({ content, context, data }) {
// estado inicial desde el `data` del SSR; los efectos de cliente hacen el resto
const state = useBlogPosts(content, context, data);
// ...
}Cuando data está presente, el cliente se salta por completo su primera carga. Solo las acciones del usuario -escribir una búsqueda, desplazarse por más- tocan la red.
Ejemplos reales
Tres bloques de la app de referencia cmssy-web usan loaders, cada uno por un motivo distinto:
docs-code-block: resaltado de sintaxis en servidor haciadata.html. Dependencia de servidor:shiki.legal: sanea el HTML escrito en el CMS haciadata.sections. Dependencia de servidor:sanitize-html.blog-posts: trae la primera página de entradas haciadata.items. Dependencia de servidor: la API de entrega.
Cuando un loader lanza
Un loader que lanza no tumba la página. El SDK lo captura y contiene el bloque:
- En tu sitio: el bloque no renderiza nada. El resto de la página queda intacto.
- En el editor: una tarjeta de diagnóstico en el lugar del bloque, etiquetada loader failed, con el mensaje de error y el id del bloque.
La asimetría es deliberada. Un visitante no debería encontrarse nunca una traza, y quien edita no debería adivinar por qué un bloque se quedó en blanco.
También significa que un loader que falla es fácil de pasar por alto en producción: la página simplemente tiene una sección menos. Si falta un bloque en una página en vivo y el contenido parece correcto, abre esa página en el editor antes de sospechar del contenido.
Checklist
- El loader devuelve datos serializables por RSC: objetos planos, arrays, primitivos.
- El componente renderiza un fallback cuando
dataesundefined, para que el editor siga funcionando. - Las dependencias de servidor están tras un
import()dinámico. - Los helpers de servidor compartidos comprueban
typeof window !== "undefined". - Las llamadas de entrega pasan por
queryScopedographqlRequest, con el workspace ya acotado.
Siguientes pasos
- Desarrollo de bloques:
defineBlockde principio a fin. - Funciones avanzadas de bloques: contexto, comportamiento en vista previa, casos límite.
- API e IA: qué expone la API de entrega.