使用区块构建页面

本页内容

blocks 字段存储一个有序的页面组合。每个条目都记录了区块类型、保留的 schema 版本、一个稳定的键,以及该版本声明的字段。编辑者在内容编辑器中添加区块并调整其顺序。Astro 路由将每种区块类型映射到一个组件。

定义区块类型

先定义区块类型,再定义使用它们的集合字段。seed 会保留每个带编号的版本以及当前生效的 currentVersion 指针。

以下 seed 定义了 Hero 和 Feature grid 两种区块,然后让它们可以在 Pages 集合的 layout 字段中使用:

{
  "$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
          }
        }
      ]
    }
  ]
}

allowedTypes 的顺序决定了区块选择器中的排列顺序。如果之后移除了某个允许的类型,EmDash 会把它移到由服务器管理的 retiredTypes 列表中。已有的区块仍然可以编辑,但编辑者无法再添加或复制该类型的区块。

创建 Astro 组件

每个组件都会接收 value、index 和 blockKey。value 中包含 _type、_version 和 _key,因此当区块 schema 演进时,组件可以据此区分保留的各个版本。

Hero 组件从已存储的区块中读取所有要显示的值:

---
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>

为每种允许的类型创建一个组件。组件负责标记和样式;区块的值提供内容和媒体。

渲染页面组合

使用 defineBlockComponents 可以要求为生成的字段联合类型中的每个 _type 都提供一个组件。在页面路由中把这个映射传给 <Blocks>。

---
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> 不会执行任何内容、schema、媒体或网络查询。它按存储顺序渲染传入的数组。如果某个区块组件需要其他数据,可以在该组件中显式执行应用查询。

处理缺失的组件

在开发环境中,未映射的类型会显示一个可见的占位符,并在控制台输出警告。占位符会显示 _type,但不会打印已存储的区块值。

在生产环境中,如果提供了 fallback 组件,未映射的类型会渲染该组件;否则不输出任何内容:

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

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

在启用或激活生产内容所使用的区块类型之前,请先发布渲染器对它的支持。

修改区块 schema

兼容的变更会修订当前生效的版本。添加可选字段、添加默认值或放宽校验都会保持相同的版本号。已存储的区块会在下次写入时获得默认值。

破坏性变更会创建一个未激活的版本。删除字段、更改字段类型、添加必填字段或收紧校验都属于破坏性变更。

  1. 通过 schema API 或 MCP 创建破坏性版本,并保持其未激活状态。

  2. 更新渲染器,使其同时处理保留的旧版本和新版本,然后部署渲染器。

  3. 激活新版本。激活后,新建的区块将使用该版本。

  4. 使用 migrateBlocks: true 显式迁移已存储的区块。在更改 _version 及其版本特定字段时,保留每个区块的 _key。

旧版本仍可用于修订版本、草稿、媒体追踪和已存储的内容。区块类型和保留的版本没有永久删除操作。

支持的嵌套字段

区块定义支持 string、text、url、number、integer、boolean、datetime、select、multiSelect、portableText、image、file 和 repeater。

区块定义内不支持引用、JSON、slug、嵌套区块、自定义组件、物理索引、唯一性约束以及按子字段本地化。区块字段本身不能设为必填、唯一、可搜索或已索引,也不能指定自定义字段组件。

关于字段校验和存储值的确切规则,请参阅 blocks 字段参考。关于 seed 冲突和导出行为,请参阅 Seed 文件。