블록으로 페이지 만들기

이 페이지

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에 대해 컴포넌트를 하나씩 필수로 지정할 수 있습니다. 이 맵을 페이지 라우트의 <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 필드 레퍼런스를 참조하세요. 시드 충돌 및 내보내기 동작은 시드 파일을 참조하세요.