コンテンツにスキップ

コンテンツモデル

EmDashは、スキーマ定義がコードではなくデータベース内に存在するデータベースファーストのコンテンツモデルを採用しています。これは、ランタイムでのスキーマ変更と非開発者にも扱いやすいセットアップを可能にする根本的な設計上の選択です。

StrapiやKeystaticのような従来のCMSでは、スキーマをコードで定義する必要があります:

// Traditional approach - schema in code
const posts = collection({
fields: {
title: text({ required: true }),
content: richText(),
},
});

EmDashは同じ情報をデータベーステーブルに保存します:

-- _emdash_collections table
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);

どちらのアプローチも同じコンテンツ構造を定義します。違いは、その構造がどこに存在し、どのように変更できるかです。

なぜデータベースファーストなのか?

Section titled “なぜデータベースファーストなのか?”

ランタイム変更

コード変更やリビルドなしでコンテンツタイプを作成・編集できます。非開発者は管理UIを通じてコンテンツモデルを設計できます。

実際のSQLカラム

WordPressのEAV(Entity-Attribute-Value)モデルとは異なり、各フィールドは実際のカラムを取得します。適切なインデックス作成、外部キー、クエリ最適化が可能です。

自己文書化

データベースツールでスキーマを直接検査できます。コンテンツモデルを理解するためにコードを解析する必要はありません。

移行パス

スキーマをJSONとしてエクスポートしてバージョン管理できます。新しい環境でスキーマをインポートできます。

2つのシステムテーブルがコンテンツ構造を定義します:

CREATE TABLE _emdash_collections (
id TEXT PRIMARY KEY,
slug TEXT UNIQUE NOT NULL, -- "posts", "products"
label TEXT NOT NULL, -- "Blog Posts"
label_singular TEXT, -- "Post"
description TEXT,
icon TEXT, -- Lucide icon name
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- How it was created
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);

source フィールドは、コレクションがどのように作成されたかを追跡します:

SourceDescription
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, -- Column name: "title", "price"
label TEXT NOT NULL, -- Display label
type TEXT NOT NULL, -- Field type
column_type TEXT NOT NULL, -- SQLite type: TEXT, REAL, INTEGER, JSON
required INTEGER DEFAULT 0,
unique_field INTEGER DEFAULT 0,
default_value TEXT, -- JSON-encoded default
validation JSON, -- Validation rules
widget TEXT, -- Custom widget identifier
options JSON, -- Widget options
sort_order INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
UNIQUE(collection_id, slug)
);

各コレクションは ec_ プレフィックスを持つ独自のテーブルを取得します。タイトルと価格フィールドを持つ「products」コレクションを作成すると:

CREATE TABLE ec_products (
-- System columns (always present)
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, -- Soft delete
version INTEGER DEFAULT 1, -- Optimistic locking
-- コンテンツカラム(フィールド定義から)
title TEXT NOT NULL,
price REAL
);

管理UI経由でフィールドを追加すると、EmDashは:

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

SQLiteはこれらの ALTER TABLE 操作をランタイムでサポートします:

OperationSupported
Add columnYes
Rename columnYes
Drop columnYes (SQLite 3.35+)
Change column typeNo (requires table rebuild)

型変更の場合、EmDashはテーブルの再構築を透過的に処理します:新しいテーブル作成 → データコピー → 古いテーブル削除 → 新しいテーブル名変更。

EmDashは明確な分離を維持します:

ConcernLocationTables
SchemaSystem tables_emdash_collections, _emdash_fields
ContentPer-collection tablesec_posts, ec_products, etc.
MediaSeparate table + storagemedia table + R2/S3
SettingsOptions tableoptions with site: prefix

この分離は以下を意味します:

  • コンテンツなしでスキーマをエクスポート可能
  • コンテンツをスキーマ間で移行可能
  • システムテーブルがユーザーデータで散らかされることはない

EmDashは起動時にデータベースのフィールド定義からZodスキーマを構築します:

// Simplified example
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型を生成します:

Terminal window
# Fetch schema from database, generate types
npx emdash types

これにより .emdash/types.ts が生成されます:

// .emdash/types.ts (generated)
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 }>;
}

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

Section titled “開発者と非開発者のワークフロー”

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

Terminal window
# Fetch schema, generate types
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": "カテゴリー", "hierarchical": true }],
"menus": [{ "name": "primary", "label": "メインナビゲーション" }]
}

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

import { applySeed, validateSeed } from "emdash/seed";
import seedData from "../../concepts/.emdash/seed.json";
// 最初に検証
const { valid, errors } = validateSeed(seedData);
// 適用(冪等性あり - 再実行しても安全)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
ApproachSchema LocationRuntime ModificationType Safety
EmDashDatabaseYes (full)Generated from DB
WordPressPHP code + EAVLimited (meta fields)None
StrapiCode filesNo (rebuild required)Generated at build
SanityCode filesNo (schema must deploy)Built-in
DirectusDatabaseYes (full)Generated from DB

EmDashはDirectusモデルに従います:データベースファーストで、オプションの型生成を備えています。これにより、最大限の柔軟性を保ちつつ、必要に応じて型安全な開発をサポートします。