event-manual / chapter 12

JSON-as-DB コンテンツモデル

@event/cms

イベントの公開コンテンツは DB ではなく content/*.json に置く。git がバージョン管理と監査ログを兼ね、編集は commit(第14章)、反映は rebuild / ISR で行う。読み出しの正は @event/cmscreateContentReader である。

createContentReader — real + dev sample overlay

import { createContentReader } from "@event/cms";

const reader = createContentReader(); // 既定 <cwd>/content
const news = await reader.readContent<NewsItem[]>("news");   // sample merge 済み
const raw  = await reader.readReal<NewsItem[]>("news");      // 素の real のみ

ファイル規約:

パス役割
content/<name>.jsonreal データ(git 管理・本番の正)
content/sample/<name>.jsonチーム共有の sample(git 管理)
content/sample/<name>.local.jsongitignored の個人 override。sample を丸ごと置換する

merge 規則:

書き込み系(createCmsActions / createCmsRouteHandlers)は必ず readReal を基準にする。sample がマージされた値を保存すると dev のダミーが real に固定化されるためである。

createResourceRegistry — 三重宣言の廃止

spikes では RESOURCES 配列 / SINGLETONS Set / ResourceSchema.shape の3箇所を手動同期していた(ドリフト源)。registry は schema 配列だけを宣言し、残りを全て派生させる:

import { createResourceRegistry } from "@event/cms";

export const registry = createResourceRegistry([NEWS_SCHEMA, SETTINGS_SCHEMA]);

registry.resources;            // ["news", "settings"]
registry.isSingleton("news");  // false — schema.shape から派生
registry.getSchema("news");    // ResourceSchema | null
registry.visible;              // hidden: true を除いた admin ナビ用一覧

schema 名の重複は生成時に throw する(黙って上書きしない)。

projections — publicFields allowlist

非公開フィールド(バイヤー私書・原価・連絡先メモ等)を public render に渡さない防御は allowlist で行う。denylist(Omit)は新フィールドの追加時に黙って漏れるが、allowlist は漏れない。

// schema 側で公開フィールドを宣言
const BUYERS_SCHEMA: ResourceSchema = {
  name: "buyers", label: "バイヤー", shape: "list",
  publicFields: ["name", "logoUrl", "industry", "summary"],
  fields: [/* private フィールドも全部ここに定義してよい */],
};

// LP の server component で
import { pickPublicList } from "@event/cms";
const items = pickPublicList(BUYERS_SCHEMA, await reader.readContent("buyers") ?? []);

pickPublicFieldsidField(既定 "id")を常に含め、publicFields 未定義の schema では素通しする(公開のみのリソースに宣言を強いない)。

サーバサイド default 注入

リソース固有の自動値(news の publishedAt 等)は createCmsActionsserverDefaults フックで注入する。client からの値に依存しないため、時刻偽装や欠落が起きない:

export const cms = createCmsActions({
  registry, store, reader, requireAuth: requireAdmin,
  serverDefaults: (name, values) =>
    name === "news" && !values.publishedAt
      ? { ...values, publishedAt: new Date().toISOString() }
      : values,
});

なお docs/DB_MCP_DESIGN.md の二層原則(固定コア + 拡張レイヤ)はこの JSON モデルにも適用される。4S 所有の LOCKED フィールド(@event/schemaLOCKED_SESSION_FIELDS)へは CMS からもここの save 経路からも書き込めない(第13章の locked guard)。