コンテンツモデル

このページ

EmDash は データベース優先のコンテンツモデル を採用し、スキーマ定義はコードではなくデータベースに置かれます。ランタイムでのスキーマ変更と、開発者以外にも扱いやすいセットアップを可能にする根本的な設計です。

スキーマをデータとして

Strapi や Keystatic のような従来の CMS はコードでスキーマを定義します。

// 従来: コード内スキーマ
const posts = collection({
	fields: {
		title: text({ required: true }),
		content: richText(),
	},
});

EmDash は同じ情報をテーブルに保存します。

-- _emdash_collections テーブル
INSERT INTO _emdash_collections (slug, label)
VALUES ('posts', 'Blog Posts');

-- _emdash_fields テーブル
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
  ('coll_abc', 'title', 'string', true),
  ('coll_abc', 'content', 'portableText', false);

どちらも同じコンテンツ構造を表します。違いはその構造の置き場所と変更方法です。

なぜデータベース優先か

ランタイム変更

コード変更や再ビルドなしでコンテンツタイプを作成・編集。開発者以外も管理 UI からデータモデルを設計できます。

実際の SQL 列

WordPress の EAV とは異なり各フィールドは実列を持ちます。インデックス、外部キー、クエリ最適化が適切に行えます。

自己文書化

DB ツールで直接スキーマを確認できます。コードを解析する必要はありません。

移行パス

バージョン管理用にスキーマを JSON エクスポート。新環境へインポート。

スキーマ用テーブル

システムテーブルが 2 つ、構造を定義します。

コレクションテーブル

CREATE TABLE _emdash_collections (
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE NOT NULL,
  label TEXT NOT NULL,
  label_singular TEXT,
  description TEXT,
  icon TEXT,
  supports JSON,
  source TEXT,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  updated_at TEXT
);

source はコレクションの作成方法を示します。

説明
manual管理 UI から作成
template:blogテンプレートのシードから
import:wordpressWordPress からインポート
discovered既存データから自動検出

フィールドテーブル

CREATE TABLE _emdash_fields (
  id TEXT PRIMARY KEY,
  collection_id TEXT REFERENCES _emdash_collections(id),
  slug TEXT NOT NULL,
  label TEXT NOT NULL,
  type TEXT NOT NULL,
  column_type TEXT NOT NULL,
  required INTEGER DEFAULT 0,
  unique_field INTEGER DEFAULT 0,
  default_value TEXT,
  validation JSON,
  widget TEXT,
  options JSON,
  sort_order INTEGER,
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(collection_id, slug)
);

コンテンツテーブル

各コレクションは ec_ プレフィックスのテーブルを持ちます。

CREATE TABLE ec_products (
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE,
  status TEXT DEFAULT 'draft',
  author_id TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  published_at TEXT,
  deleted_at TEXT,
  version INTEGER DEFAULT 1,
  title TEXT NOT NULL,
  price REAL
);

ランタイムのスキーマ変更

管理 UI でフィールドを追加すると EmDash は次を行います。

  1. _emdash_fields にレコードを挿入
  2. ALTER TABLE ec_<collection> ADD COLUMN を実行
  3. 検証用の Zod スキーマを再生成

SQLite がランタイムでサポートする ALTER TABLE 操作:

操作対応
列追加はい
列名変更はい
列削除はい(SQLite 3.35+)
型変更いいえ(テーブル再構築が必要)

型変更時は EmDash が透過的に再構築します(新テーブル作成 → コピー → 旧削除 → リネーム)。

スキーマとコンテンツの分離

関心場所テーブル
スキーマシステムテーブル_emdash_collections, _emdash_fields
コンテンツコレクションごとのテーブルec_posts, ec_products など
メディア別テーブル + ストレージmedia テーブル + R2/S3
設定オプションテーブルsite: プレフィックスの options

この分離により:

  • スキーマをコンテンツなしでエクスポート可能
  • コンテンツをスキーマ間で移行可能
  • システムテーブルにユーザーデータが混在しない

ランタイム検証

起動時に DB のフィールド定義から Zod スキーマを構築し、作成・更新のたびに検証します。

function buildSchema(fields: Field[]): ZodSchema {
	const shape: Record<string, ZodType> = {};
	for (const field of fields) {
		let zodType = fieldTypeToZod(field.type);
		if (field.required) zodType = zodType.required();
		if (field.validation?.min !== undefined) zodType = zodType.min(field.validation.min);
		shape[field.slug] = zodType;
	}
	return z.object(shape);
}

TypeScript 連携

データベーススキーマから TypeScript 型を生成します:

# スキーマを取得し型を生成
npx emdash types

.emdash/types.ts が生成されます:

// .emdash/types.ts(生成)
export interface Post {
	title: string;
	content: PortableTextBlock[];
	excerpt?: string;
	featuredImage?: string;
}

export interface Product {
	title: string;
	price: number;
	quantity: number;
}

// クエリ関数の型付きオーバーロード
declare module "emdash" {
	export function getEmDashCollection(
		type: "posts",
	): Promise<{ entries: ContentEntry<Post>[]; error?: Error }>;

	export function getEmDashEntry(
		type: "products",
		id: string,
	): Promise<{ entry: ContentEntry<Product> | null; error?: Error; isPreview: boolean }>;
}

開発者と非開発者のワークフロー

開発者 は CLI を使用できます:

# スキーマ取得・型生成
npx emdash types

# スキーマを JSON でエクスポート
npx emdash export-seed > seed.json

非開発者 は管理 UI のみで操作します:

  1. 管理パネルの コンテンツタイプ を開く
  2. コレクション追加 をクリック
  3. ビジュアルビルダーでフィールドを定義
  4. すぐにコンテンツ作成を開始

どちらのアプローチも同じデータベーステーブルを更新します。

シードファイル

テンプレートとエクスポートでは、ポータブルなスキーマ定義に JSON シードファイルを使います:

{
	"version": "1",
	"collections": [
		{
			"slug": "posts",
			"label": "Blog Posts",
			"labelSingular": "Post",
			"supports": ["drafts", "revisions", "preview"],
			"fields": [
				{ "slug": "title", "type": "string", "required": true },
				{ "slug": "content", "type": "portableText" },
				{ "slug": "featuredImage", "type": "image" }
			]
		}
	],
	"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
	"menus": [{ "name": "primary", "label": "Primary Navigation" }]
}

プログラムからシードファイルを適用します:

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "./.emdash/seed.json";

// まずバリデーション
const { valid, errors } = validateSeed(seedData);

// 適用(冪等 — 再実行しても安全)
await applySeed(db, seedData, {
	includeContent: true,
	onConflict: "skip", // 'skip' | 'update' | 'error'
});

他方式との比較

方式スキーマ場所ランタイム変更
EmDashDBあり(完全)DB から生成
WordPressPHP + EAV限定的(メタ)なし
Strapiコードなし(再ビルド)ビルド時生成
Sanityコードなし(デプロイ必須)組み込み
DirectusDBあり(完全)DB から生成

EmDash は Directus と同じモデル(データベース優先 + オプションの型生成)を採用しています。型安全な開発をサポートしつつ、最大限の柔軟性を提供します。

次のステップ