跳转到内容

内容模型

EmDash 采用数据库优先的内容模型,架构定义存储在数据库中,而非代码中。这是一个根本性的设计选择,它支持运行时架构修改和面向非开发人员的设置。

传统的 CMS(如 Strapi 或 Keystatic)要求你在代码中定义架构:

// 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 table
INSERT INTO _emdash_fields (collection_id, slug, type, required)
VALUES
('coll_abc', 'title', 'string', true),
('coll_abc', 'content', 'portableText', false);

两种方法定义了相同的内容结构。区别在于该结构存储的位置以及如何修改它。

运行时修改

无需更改代码或重新构建即可创建和编辑内容类型。非开发人员可以通过管理界面设计其数据模型。

真实的 SQL 列

与 WordPress 的 EAV(实体-属性-值)模型不同,每个字段都对应一个真实的列。支持适当的索引、外键和查询优化。

自文档化

数据库工具可以直接检查架构。无需解析代码即可理解数据模型。

迁移路径

将架构导出为 JSON 以便进行版本控制。在新环境中导入架构。

两个系统表定义了你的内容结构:

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 字段追踪集合的创建方式:

来源描述
manual通过管理界面创建
template:blog由模板的种子文件创建
import:wordpress从 WordPress 导入
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
-- Content columns (from field definitions)
title TEXT NOT NULL,
price REAL
);

当你通过管理界面添加字段时,EmDash 会:

  1. 向 _emdash_fields 表中插入一条记录 2. 执行 ALTER TABLE ec_collection ADD COLUMN column_name TYPE 3. 重新生成用于验证的 Zod 架构

SQLite 在运行时支持以下 ALTER TABLE 操作:

操作是否支持
添加列是
重命名列是
删除列是 (SQLite 3.35+)
更改列类型否 (需要重建表)

对于类型更改,EmDash 会透明地处理表重建:创建新表 → 复制数据 → 删除旧表 → 重命名新表。

EmDash 保持清晰的分离:

关注点位置表
架构系统表_emdash_collections, _emdash_fields
内容每个集合的表ec_posts, ec_products 等
媒体单独的表 + 存储media 表 + R2/S3
设置选项表带有 site: 前缀的 options 表

这种分离意味着:

  • 架构可以独立于内容导出
  • 内容可以在不同架构之间迁移
  • 系统表永远不会被用户数据污染

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;
}
// Typed overloads for query functions
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:

Terminal window
# Fetch schema, generate types
npx emdash types
# Export schema as JSON
npx emdash export-seed > seed.json

非开发者可以完全使用管理界面:

  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";
// Validate first
const { valid, errors } = validateSeed(seedData);
// Apply (idempotent - safe to re-run)
await applySeed(db, seedData, {
includeContent: true,
onConflict: "skip", // 'skip' | 'update' | 'error'
});
方法架构位置运行时修改类型安全
EmDash数据库是 (完整)从数据库生成
WordPressPHP 代码 + EAV有限 (元字段)无
Strapi代码文件否 (需要重新构建)在构建时生成
Sanity代码文件否 (架构必须部署)内置
Directus数据库是 (完整)从数据库生成

EmDash 遵循 Directus 模型:数据库优先,可选类型生成。这提供了最大的灵活性,同时在需要时仍支持类型安全的开发。