本頁面面向開發 EmDash 的人,而非使用它建置網站的人。它記錄了內部機制——表格佈局、Astro 整合、請求路徑、程式碼產生。這些內容對使用 EmDash 不是必需的。如果您正在建置網站,請改為閱讀 Architecture 和 Content Model。
Astro 整合
EmDash 作為 Astro 整合從 emdash 套件執行。在建置時它會:
-
使用 Astro 的
injectRouteAPI 注入管理 SPA 和 REST API 路由。不會向使用者專案複製任何內容。注入的路徑包括:路徑模式 用途 /_emdash/admin/[...path]管理面板 SPA /_emdash/api/manifest管理清單(集合、外掛程式) /_emdash/api/content/[collection]內容條目 CRUD /_emdash/api/media/*媒體庫操作 /_emdash/api/schema/*架構管理 /_emdash/api/settings網站設定 /_emdash/api/menus/*導航選單 /_emdash/api/taxonomies/*分類、標籤、自訂分類法 -
產生虛擬模組,使打包器能夠解析和搖樹最佳化設定和外掛程式程式碼:
模組 用途 virtual:emdash/config資料庫和儲存設定 virtual:emdash/dialect資料庫方言工廠 virtual:emdash/plugin-admins外掛程式管理 UI 的靜態匯入 -
提供 Live Collections 載入器,管理遷移,並開啟儲存連線。
資料庫優先架構
架構定義存在於資料庫中,而不是程式碼中。兩個系統表追蹤結構。
_emdash_collections 每個集合儲存一列:
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,
supports JSON, -- ["drafts", "revisions", "preview"]
source TEXT, -- how it was created
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT
);
source 欄記錄來源:manual(管理 UI)、template:<name>(種子檔案)、import:wordpress(匯入器)或 discovered(從現有表自動偵測)。
_emdash_fields 每個欄位儲存一列,連結到其集合:
CREATE TABLE _emdash_fields (
id TEXT PRIMARY KEY,
collection_id TEXT REFERENCES _emdash_collections(id),
slug TEXT NOT NULL, -- column name
label TEXT NOT NULL,
type TEXT NOT NULL, -- field type
column_type TEXT NOT NULL, -- TEXT, REAL, INTEGER, JSON
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_。帶有 title 和 price 欄位的 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
);
真實欄(而不是帶有 JSON blob 的單個表)提供適當的索引、可用的外鍵、資料庫工具可以檢查的架構,以及無需逐欄位 JSON 解析。
關注點保持分離:
| 關注點 | 位置 | 表 |
|---|---|---|
| 架構 | 系統表 | _emdash_collections、_emdash_fields |
| 內容 | 每個集合的表 | ec_posts、ec_products、… |
| 媒體 | 獨立表 + 儲存 | media 表 + R2/S3 |
| 設定 | 選項表 | 帶 site: 前綴的 options |
執行時架構變更
透過管理 UI 新增欄位執行三個步驟:
- 向
_emdash_fields插入一條記錄。 - 執行
ALTER TABLE ec_<collection> ADD COLUMN <name> <TYPE>。 - 重新產生用於驗證的 Zod 架構。
SQLite 支援在執行時新增、重新命名和刪除欄(刪除需要 SQLite 3.35+)。不支援就地變更欄類型,因此 EmDash 透明地重建表:建立新表,複製列,刪除舊表,重新命名新表。
執行時驗證
EmDash 在啟動時從欄位定義建置 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);
}
資料層
EmDash 使用 Kysely 在所有支援的資料庫(SQLite、libSQL、Cloudflare D1 和 PostgreSQL)中提供型別安全的 SQL。方言由 virtual:emdash/dialect 從網站傳遞給整合的設定中選擇。
Live Collections 載入器
內容透過 Astro 的 Live Collections 在執行時提供。emdashLoader() 實作了 Astro 的 LiveLoader 介面,並註冊為單個 _emdash 集合:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({ loader: emdashLoader() }),
};
單個 _emdash 集合包裝每種內容類型;當呼叫 getEmDashCollection("posts") 時,載入器按類型篩選。
請求路徑
來自頁面的內容請求:
- Astro 接收請求並執行頁面元件。
getEmDashCollection()呼叫 Astro 的getLiveCollection()。emdashLoader透過 Kysely 查詢相關的ec_*表。- 列對應到 Astro 的條目格式(
id、slug、data)。 - 元件渲染。
管理請求:
- 中介軟體驗證工作階段權杖。
- API 路由透過儲存庫執行 CRUD。
- 生命週期鉤子觸發(例如
content:beforeSave)。 - Kysely 執行 SQL。
- 路由向管理 SPA 傳回 JSON。
管理面板內部結構
管理面板是一個 React 孤島。Astro 提供外殼並在中介軟體中強制執行身份驗證;內部的所有內容都是用戶端,建置在 TanStack Router、TanStack Query、TanStack Table、React Hook Form + Zod、TipTap 和 Kumo(Cloudflare 的 Base UI + Tailwind 設計系統)之上。
外殼路由在中介軟體中控制存取:
export async function onRequest({ request, locals }, next) {
const session = await getSession(request);
if (request.url.includes("/_emdash/admin")) {
if (!session?.user) return redirect("/_emdash/admin/login");
locals.user = session.user;
}
return next();
}
清單驅動的 UI
管理面板不硬編碼關於集合或外掛程式的任何內容。它取得 GET /_emdash/api/manifest,傳回請求使用者可以存取的集合、外掛程式和分類法,按角色篩選:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [{ "slug": "title", "type": "string", "required": true }]
}
],
"plugins": [{ "id": "audit-log", "label": "Audit Log" }],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
導航、表單和欄位編輯器從此清單產生,因此架構和外掛程式變更無需重建管理面板即可顯示,Zod 架構保留在伺服器端。
外掛程式管理 UI
外掛程式管理進入點被收集到產生的靜態匯入虛擬模組中,以便打包器可以解析和搖樹最佳化它們:
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
export const pluginAdmins = { seo: pluginAdmin0 };
富文字轉換
Portable Text 欄位在 TipTap(ProseMirror)中編輯。內容在載入和儲存邊界透過 portableTextToProsemirror() 和 prosemirrorToPortableText() 轉換。來自外掛程式或匯入的未知區塊被保留為唯讀佔位符。
簽名上傳
媒體上傳透過直接到儲存的簽名 URL 繞過 Worker 主體大小限制:
- 用戶端請求上傳 URL(
POST /api/media/upload-url)。 - 用戶端直接上傳到簽名 URL(R2 或 S3)。
- 用戶端確認(
POST /api/media/:id/confirm)。 - 伺服器提取中繼資料(尺寸、MIME 類型)。
擴充內容匯入器
WordPress 匯入器基於可插拔的 ImportSource 介面建置。自訂來源實作 probe、analyze 和 fetch:
interface ImportSource {
probe(input: ImportInput): Promise<ProbeResult>;
analyze(input: ImportInput): Promise<AnalysisResult>;
fetchContent(input: ImportInput): AsyncIterable<NormalizedEntry>;
}
probe 驗證輸入並報告發現的內容,analyze 將來源文章類型對應到 EmDash 集合並標記架構差距,fetchContent 串流傳輸規範化條目,匯入管道透過管理面板使用的相同儲存庫寫入。內建來源涵蓋 WordPress WXR、WordPress.com 和 WordPress REST API;註冊自訂來源以從其他系統匯入。