本頁面面向參與 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 匯出。當匯入器可以產生相同的標準化分析和內容項目形狀時,註冊另一個來源。