本页面面向参与 EmDash 自身开发的人员,而非使用 EmDash 构建站点的人员。它解释了数据库布局、Astro 集成、请求路径、管理应用程序、媒体流程和导入系统。如果您正在构建站点,请改为阅读架构和内容模型。
Astro 集成
EmDash 作为来自 emdash 包的 Astro 集成运行。在构建时:
-
使用 Astro 的
injectRouteAPI 注入管理应用程序和 REST API 路由。不会向用户项目复制任何内容。主要路由族包括:路径模式 用途 /_emdash/admin/[...path]管理面板 SPA /_emdash/api/manifest管理清单(集合、插件) /_emdash/api/content/[collection]/...内容条目操作 /_emdash/api/media/...媒体库操作 /_emdash/api/schema/...模式管理 /_emdash/api/settings/...站点设置 /_emdash/api/menus/...导航菜单 /_emdash/api/taxonomies/...分类、标签、自定义分类法 /_emdash/api/plugins/[pluginId]/[...path]插件定义的 API 路由 路由注入器是完整的清单,包括认证、评论、搜索、导入、小部件和其他路由族。
-
生成虚拟模块,以便打包器可以解析配置和扩展代码:
模块 用途 virtual:emdash/config数据库、存储和站点配置 virtual:emdash/dialect数据库方言工厂 virtual:emdash/admin-registry插件管理界面的静态导入 virtual:emdash/plugins已配置的插件实现 virtual:emdash/media-providers已配置的外部媒体提供者 virtual-modules.ts定义了其余的运行时帮助程序和生成的模块内容。 -
提供 Live Content Collections 加载器并注册运行时中间件。在请求时,中间件打开已配置的数据库和存储连接,并在路由使用之前应用任何待处理的迁移。
数据库优先模式
模式定义存在于数据库中,而不是静态配置文件中。_emdash_collections 每个集合存储一行。其核心列描述了集合以及运行时和管理公开的功能:
| 列 | 用途 |
|---|---|
id, slug | 稳定的集合标识 |
label, label_singular, description, icon | 向编辑者显示的名称和指导 |
supports, has_seo, comments_enabled, edit_locking | 可选的集合功能 |
title_field, date_field, admin_config, hidden, sort_order | 管理列表和导航行为 |
url_pattern, routable | 公开 URL 和 slug 行为 |
source | 集合的创建方式 |
source 值记录来源,如 manual、seed、template:<name>、import:<name> 或 discovered。额外设置来自注册的迁移,因此 database/types.ts 和迁移是当前的列清单。
_emdash_fields 存储与每个集合关联的字段:
| 列 | 用途 |
|---|---|
id, collection_id, slug | 字段标识和所属集合 |
label, type, column_type | 编辑器标签、EmDash 字段类型和 SQL 存储类型 |
required, unique, default_value, validation | 内容约束和默认值 |
widget, options, sort_order | 编辑器控件和显示顺序 |
searchable, indexed, translatable | 搜索、查询和本地化行为 |
collection_id 引用 _emdash_collections.id,每个字段 slug 在其集合内唯一。
每集合内容表
每个集合获得自己的表,前缀为 ec_。具有 title 和 price 字段的 products 集合会生成如下形式的表:
CREATE TABLE ec_products (
-- 系统列,存在于每个内容表中
id TEXT PRIMARY KEY,
slug TEXT,
status TEXT DEFAULT 'draft',
author_id TEXT,
primary_byline_id TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
published_at TEXT,
scheduled_at TEXT,
deleted_at TEXT,
version INTEGER DEFAULT 1,
live_revision_id TEXT,
draft_revision_id TEXT,
locale TEXT NOT NULL DEFAULT 'en',
translation_group TEXT,
-- 内容列,从字段定义创建
title TEXT NOT NULL,
price REAL,
UNIQUE (slug, locale)
);
真实列为每个字段提供数据库类型,允许索引和外键,并让数据库工具在不解码内容 JSON blob 的情况下检查模式。唯一约束允许翻译共享 slug,同时保持每个 slug 在语言环境内唯一。同一条目的所有语言环境变体共享 translation_group 值,使 EmDash 能够找到彼此互为翻译的行。
主要数据关注点保持分离:
| 关注点 | 位置 | 表 |
|---|---|---|
| 模式 | 系统表 | _emdash_collections, _emdash_fields |
| 内容 | 每集合表 | ec_posts, ec_products, … |
| 媒体 | 独立表 + 存储 | media 表 + 已配置存储 |
| 设置 | 选项表 | 带 site: 前缀的 options |
运行时模式更改
通过管理 UI 添加字段会执行以下步骤:
- 将字段定义插入
_emdash_fields。 - 向集合的
ec_*表添加相应列,当字段配置为已索引时创建索引。 - 刷新生成的开发类型,使新字段出现在编辑器工具中。
内容验证读取当前字段定义,并在创建或更新内容时构建 Zod 模式。更改字段的底层 SQL 类型、required 或 unique 约束或本地化行为可能需要手动内容迁移;SchemaRegistry 拒绝不支持的原地更改,而不是隐式重建表。
运行时验证
EmDash 从集合的当前字段派生 Zod 模式。生成器将类型和约束细节委托给 generateFieldSchema():
export function generateZodSchema(
collection: CollectionWithFields,
): z.ZodObject<Record<string, ZodType>> {
const shape: Record<string, ZodType> = {};
for (const field of collection.fields) {
shape[field.slug] = generateFieldSchema(field);
}
return z.object(shape);
}
内容处理器还会拒绝未知字段、检查必填字符串值并验证对其他集合的引用。
数据层
EmDash 使用 Kysely 在 SQLite、libSQL、Cloudflare D1 和 PostgreSQL 之间提供类型化 SQL。站点配置选择数据库适配器;集成通过 virtual:emdash/dialect 公开其方言工厂。
Live Content Collections 加载器
内容通过 Astro 的 Live Content Collections 在运行时提供。emdashLoader() 实现 Astro 的 LiveLoader 接口,并注册为单个 _emdash 集合:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
单个 _emdash 集合包装每个 EmDash 集合。getEmDashCollection("posts") 提供 posts 类型过滤器,加载器将其映射到 ec_posts 表。
请求路径
来自 Astro 页面的内容请求遵循此路径:
- 页面调用
getEmDashCollection()或getEmDashEntry()。 - 查询包装器使用内部
_emdash集合和请求的 EmDash 集合类型调用 Astro 的getLiveCollection()或getLiveEntry()。 emdashLoader()通过 Kysely 查询相关的ec_*表,应用发布、语言环境、过滤、排序和分页规则。- 查询包装器将行映射到 Astro 条目并加载其署名和分类法术语。
- Astro 组件渲染返回的条目。
预览和编辑模式状态通过请求上下文传播,因此在中间件验证请求后,相同的查询函数可以返回草稿内容。
管理 API 请求遵循单独的路径:
- 中间件认证请求并将已解析的用户存储在
Astro.locals中。 - API 路由解析请求并检查该操作所需的权限。
- 路由将业务逻辑委托给处理器或仓库。
- 处理器在数据库操作周围运行插件生命周期钩子(当该操作公开钩子时)。
- 路由向管理应用程序返回标准 JSON 成功或错误响应。
管理面板内部结构
管理面板是一个 React 单页应用程序。Astro 提供其外壳,认证中间件保护管理路由。在应用程序内部,TanStack Router 处理导航,TanStack Query 加载服务器状态,TanStack Table 渲染数据网格,React Hook Form 和 Zod 管理表单,TipTap 编辑 Portable Text,Kumo 提供设计系统。
对于会话认证,中间件将未认证的浏览器请求重定向到登录页面,并为未认证的 API 请求返回 JSON 错误。加载活动用户后,将该用户放置在 Astro.locals 上供路由使用:
const sessionUser = await resolveSessionUser(session);
if (!sessionUser?.id) {
if (isApiRoute) {
return apiError("NOT_AUTHENTICATED", "Not authenticated", 401);
}
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
loginUrl.searchParams.set("redirect", url.pathname);
return context.redirect(loginUrl.toString());
}
在此分支之后,中间件加载用户,拒绝缺失或禁用的账户,将活动用户放置在 Astro.locals 上,并继续到路由。
清单驱动的 UI
管理面板不会硬编码集合模式或插件贡献。它获取 GET /_emdash/api/manifest,该端点描述当前集合、字段、插件、分类法、认证模式和其他已配置的功能。简化的清单如下所示:
{
"collections": {
"posts": {
"label": "Blog Posts",
"labelSingular": "Post",
"supports": ["drafts", "revisions", "preview"],
"fields": {
"title": { "kind": "string", "label": "Title", "required": true }
}
}
},
"plugins": {
"audit-log": { "version": "0.2.1", "enabled": true }
},
"taxonomies": [
{ "name": "category", "label": "Categories", "hierarchical": true }
],
"version": "0.37.0"
}
管理面板使用清单构建集合导航和字段编辑器。由于端点读取实时模式,集合和字段更改无需重建管理应用程序即可显示。
插件管理 UI
已配置的插件管理入口点收集到 virtual:emdash/admin-registry 中。生成的模块使用静态导入,以便打包器可以包含 React 组件:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
富文本转换
Portable Text 字段使用基于 ProseMirror 的 TipTap。EmDash 在编辑器加载时将 Portable Text 转换为 ProseMirror,并在条目保存时转换回 Portable Text。来自插件或导入的未知块被保留为只读占位符,而不是被丢弃。
签名上传
媒体上传在存储适配器支持时使用直接到存储的签名 URL,否则使用同源流式端点:
- 客户端从
POST /_emdash/api/media/upload-url请求上传目标。EmDash 创建一个待处理的媒体项。 - 客户端上传到返回的目标。S3 兼容适配器可以返回绕过应用程序正文大小限制的签名 URL;原生 R2 绑定和本地存储返回 EmDash 流式端点。
- 客户端通过
POST /_emdash/api/media/:id/confirm确认上传。 - EmDash 验证存储的文件并将媒体项标记为就绪。
扩展内容导入器
WordPress 导入器使用可插拔的 ImportSource 接口。源可以探测 URL、根据当前模式分析可用内容并流式传输标准化的内容项:
interface ImportSource {
id: string;
name: string;
description: string;
icon: "upload" | "globe" | "wordpress" | "plug";
requiresFile?: boolean;
canProbe?: boolean;
probe?(url: string): Promise<SourceProbeResult | null>;
analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis>;
fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem>;
fetchMedia?(url: string, input: SourceInput): Promise<Blob>;
}
WXR 源导入 WordPress 导出文件。连接器源从安装了 EmDash WordPress 插件的站点直接导入。单独的 REST 源检测公开 WordPress 站点,但由于直接 REST 导入未实现,会引导用户使用 WXR 导出。当导入器可以生成相同的标准化分析和内容项形状时,注册另一个源。