Crear páginas con bloques

En esta página

Un campo blocks almacena una composición de página ordenada. Cada elemento registra un tipo de bloque, una versión de esquema conservada, una clave estable y los campos declarados por esa versión. Los editores añaden y reordenan bloques en el editor de contenido. La ruta de Astro asigna cada tipo de bloque a un componente.

Definir los tipos de bloque

Define los tipos de bloque antes del campo de colección que los utiliza. Un seed conserva cada versión numerada y el puntero activo currentVersion.

El siguiente seed define los bloques Hero y Feature grid, y luego los pone a disposición en un campo layout de la colección Pages:

{
  "$schema": "https://emdashcms.com/seed.schema.json",
  "version": "1",
  "blockTypes": [
    {
      "slug": "hero",
      "label": "Hero",
      "category": "Layout",
      "currentVersion": 1,
      "versions": [
        {
          "version": 1,
          "fields": [
            { "slug": "heading", "label": "Heading", "type": "string", "required": true },
            { "slug": "body", "label": "Body", "type": "portableText" },
            { "slug": "image", "label": "Image", "type": "image" },
            { "slug": "link_label", "label": "Link label", "type": "string" },
            { "slug": "link_url", "label": "Link URL", "type": "url" }
          ]
        }
      ]
    },
    {
      "slug": "feature_grid",
      "label": "Feature grid",
      "category": "Layout",
      "currentVersion": 1,
      "versions": [
        {
          "version": 1,
          "fields": [
            { "slug": "heading", "label": "Heading", "type": "string" },
            {
              "slug": "items",
              "label": "Items",
              "type": "repeater",
              "validation": {
                "subFields": [
                  { "slug": "title", "label": "Title", "type": "string", "required": true },
                  { "slug": "description", "label": "Description", "type": "text" }
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "collections": [
    {
      "slug": "pages",
      "label": "Pages",
      "fields": [
        { "slug": "title", "label": "Title", "type": "string", "required": true },
        {
          "slug": "layout",
          "label": "Layout",
          "type": "blocks",
          "validation": {
            "allowedTypes": ["hero", "feature_grid"],
            "maxItems": 20
          }
        }
      ]
    }
  ]
}

El orden de allowedTypes controla el selector de bloques. Si más adelante eliminas un tipo permitido, EmDash lo mueve a la lista retiredTypes gestionada por el servidor. Los bloques existentes siguen siendo editables, pero los editores no pueden añadir ni duplicar ese tipo.

Crear los componentes de Astro

Cada componente recibe value, index y blockKey. El valor incluye _type, _version y _key, de modo que un componente puede acotar las versiones conservadas cuando el esquema de su bloque evoluciona.

El componente Hero lee cada valor mostrado del bloque almacenado:

---
import { sanitizeHref } from "emdash";
import { Image, PortableText, type BlockComponentProps } from "emdash/ui";
import type { PageLayoutBlock } from "../../../emdash-env";

type HeroBlock = Extract<PageLayoutBlock, { _type: "hero" }>;
type Props = BlockComponentProps<HeroBlock>;

const { value } = Astro.props;
---

<section class="hero">
  <div>
    <h1>{value.heading}</h1>
    {value.body && <PortableText value={value.body} />}
    {value.link_url && <a href={sanitizeHref(value.link_url)}>{value.link_label}</a>}
  </div>
  {value.image && <Image image={value.image} />}
</section>

Crea un componente para cada tipo permitido. El componente controla el marcado y los estilos; el valor del bloque aporta el contenido y los medios.

Renderizar la composición

Usa defineBlockComponents para exigir un componente por cada _type de la unión de campos generada. Pasa ese mapa a <Blocks> en la ruta de la página.

---
import { decodeSlug, getEmDashEntry } from "emdash";
import { Blocks, defineBlockComponents } from "emdash/ui";
import type { PageLayoutBlock } from "../../../emdash-env";
import FeatureGrid from "../../components/blocks/FeatureGrid.astro";
import Hero from "../../components/blocks/Hero.astro";

const slug = decodeSlug(Astro.params.slug);
if (!slug) return Astro.redirect("/404");

const { entry: page, cacheHint } = await getEmDashEntry("pages", slug);
if (!page) return Astro.redirect("/404");
Astro.cache.set(cacheHint);

const components = defineBlockComponents<PageLayoutBlock>({
  hero: Hero,
  feature_grid: FeatureGrid,
});
---

<Blocks value={page.data.layout} components={components} />

<Blocks> no realiza consultas de contenido, esquema, medios ni red. Renderiza el array proporcionado en el orden almacenado. Un componente de bloque puede hacer una consulta explícita de la aplicación cuando necesite otros datos.

Gestionar un componente que falta

Durante el desarrollo, un tipo sin asignar produce un marcador de posición visible y una advertencia en la consola. El marcador de posición indica el _type, pero no muestra el valor almacenado del bloque.

En producción, un tipo sin asignar renderiza el componente fallback si se proporciona. De lo contrario, no genera ninguna salida:

---
import MissingBlock from "../../components/blocks/MissingBlock.astro";
---

<Blocks value={page.data.layout} components={components} fallback={MissingBlock} />

Publica el soporte del renderizador antes de habilitar o activar un tipo de bloque utilizado por contenido de producción.

Cambiar el esquema de un bloque

Los cambios compatibles modifican la versión activa. Añadir un campo opcional, añadir un valor predeterminado o relajar la validación mantiene el mismo número de versión. Los bloques almacenados reciben los valores predeterminados la próxima vez que se escriben.

Un cambio incompatible crea una versión inactiva. Eliminar un campo, cambiar el tipo de un campo, añadir un campo obligatorio o restringir la validación es un cambio incompatible.

  1. Crea la versión incompatible mediante la API de esquemas o MCP. Mantenla inactiva.

  2. Actualiza el renderizador para que gestione tanto la versión conservada como la nueva. Despliega el renderizador.

  3. Activa la nueva versión. Los bloques nuevos la usan después de la activación.

  4. Migra explícitamente los bloques almacenados con migrateBlocks: true. Conserva el _key de cada bloque mientras cambias _version y sus campos específicos de la versión.

Las versiones antiguas siguen disponibles para revisiones, borradores, seguimiento de medios y contenido almacenado. Los tipos de bloque y las versiones conservadas no tienen una operación de eliminación definitiva.

Campos anidados compatibles

Las definiciones de bloque admiten string, text, url, number, integer, boolean, datetime, select, multiSelect, portableText, image, file y repeater.

Las referencias, JSON, slugs, bloques anidados, widgets personalizados, índices físicos, unicidad y localización por subcampo no se admiten dentro de una definición de bloque. Un campo de bloques en sí no puede ser obligatorio, único, buscable ni indexado, ni tener asignado un widget de campo personalizado.

Para conocer las reglas exactas de validación de campos y de valores almacenados, consulta la referencia del campo blocks. Para el comportamiento de conflictos y exportación de seeds, consulta Archivos seed.