ブロックでページを構築する

このページ

blocks フィールドは、順序付きのページコンポジションを保存します。各アイテムには、ブロックタイプ、保持されているスキーマバージョン、安定したキー、そしてそのバージョンで宣言されたフィールドが記録されます。編集者はコンテンツエディターでブロックを追加し、並べ替えます。Astro のルートは、各ブロックタイプをコンポーネントにマッピングします。

ブロックタイプを定義する

ブロックタイプは、それを使用するコレクションフィールドより先に定義します。シードは、番号付きのすべてのバージョンと、アクティブな currentVersion ポインターを保持します。

次のシードは 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 が含まれるため、ブロックスキーマが変化したときに、コンポーネントは保持されているバージョンを判別して絞り込めます。

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 に対してコンポーネントを 1 つずつ必須にできます。そのマップをページルートの <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> は、コンテンツ、スキーマ、メディア、ネットワークのいずれのクエリも実行しません。渡された配列を保存された順序でレンダリングします。ブロックコンポーネントが他のデータを必要とする場合は、そのコンポーネントで明示的にアプリケーションのクエリを実行できます。

コンポーネントが見つからない場合の処理

開発中は、マッピングされていないタイプがあると、目に見えるプレースホルダーとコンソール警告が表示されます。プレースホルダーには _type が表示されますが、保存されたブロックの値は出力されません。

本番環境では、マッピングされていないタイプは、fallback コンポーネントが指定されていればそれをレンダリングします。指定されていない場合は何も出力しません。

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

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

本番コンテンツで使用されるブロックタイプを有効化またはアクティブ化する前に、レンダラーの対応をリリースしてください。

ブロックスキーマを変更する

互換性のある変更は、アクティブなバージョンを修正します。任意フィールドの追加、デフォルト値の追加、バリデーションの緩和では、同じバージョン番号が維持されます。保存済みのブロックは、次に書き込まれるときにデフォルト値を受け取ります。

破壊的変更は、非アクティブなバージョンを作成します。フィールドの削除、フィールドタイプの変更、必須フィールドの追加、バリデーションの厳格化は破壊的変更です。

  1. スキーマ API または MCP で破壊的なバージョンを作成します。非アクティブのままにしておきます。

  2. 保持されているバージョンと新しいバージョンの両方を処理できるようにレンダラーを更新します。レンダラーをデプロイします。

  3. 新しいバージョンをアクティブ化します。アクティブ化以降、新しいブロックはそのバージョンを使用します。

  4. migrateBlocks: true を指定して、保存済みのブロックを明示的に移行します。_version とバージョン固有のフィールドを変更する際も、各ブロックの _key は維持してください。

古いバージョンは、リビジョン、下書き、メディアの追跡、保存済みコンテンツのために引き続き利用できます。ブロックタイプと保持されているバージョンには、完全に削除する操作はありません。

サポートされるネストされたフィールド

ブロック定義では、string、text、url、number、integer、boolean、datetime、select、multiSelect、portableText、image、file、repeater をサポートしています。

リファレンス、JSON、スラッグ、ネストされたブロック、カスタムウィジェット、物理インデックス、一意性、サブフィールド単位のローカライズは、ブロック定義内ではサポートされていません。ブロックフィールド自体は、必須、一意、検索可能、インデックス付きに設定することはできず、カスタムフィールドウィジェットを割り当てることもできません。

フィールドのバリデーションと保存される値の正確なルールについては、blocks フィールドのリファレンスを参照してください。シードの競合とエクスポートの動作については、シードファイルを参照してください。