架構

本頁內容

EmDash 與 Astro 深度整合,提供完整的 CMS 體驗。本文說明關鍵架構決策以及各模組如何協作。

高階概覽

┌──────────────────────────────────────────────────────────────────┐
│                         你的 Astro 網站                           │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                   EmDash 整合                              │  │
│  │                                                            │  │
│  │  ┌──────────────┐   ┌──────────────┐   ┌───────────────┐   │  │
│  │  │   內容       │   │    後台      │   │    外掛       │   │  │
│  │  │   API        │   │    面板      │   │               │   │  │
│  │  └──────────────┘   └──────────────┘   └───────────────┘   │  │
│  │                                                            │  │
│  │  ┌──────────────────────────────────────────────────────┐  │  │
│  │  │                    資料層                            │  │  │
│  │  │      資料庫 (D1/SQLite)  +  儲存 (R2/S3)              │  │  │
│  │  └──────────────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │                    Astro 框架                              │  │
│  │         Live Collections · 中介軟體 · 工作階段             │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

EmDash 以 Astro 整合的形式執行。它為後台面板和 REST API 注入路由,為 Live Collections 提供內容載入器,並管理資料庫遷移和儲存連線。

資料庫優先的 Schema

與傳統 CMS 在程式碼中定義 schema 不同,EmDash 將 schema 定義儲存在資料庫本身中。兩個系統表追蹤你的內容結構:

  • _emdash_collections — 集合中繼資料(slug、標籤、功能)
  • _emdash_fields — 每個集合的欄位定義

當你透過後台 UI 建立一個包含 title 和 price 欄位的 “products” 集合時,EmDash 會:

  1. _emdash_collections_emdash_fields 中插入記錄
  2. 執行 ALTER TABLE 建立帶有相應欄的 ec_products

這種設計實現了:

  • 執行時 schema 修改 — 無需程式碼變更或重新建置即可建立和編輯內容類型
  • 非開發人員友善的設定 — 內容編輯者可以透過 UI 設計他們的資料模型
  • 真實的 SQL 欄 — 適當的索引、外鍵和查詢最佳化

每個集合一張表

每個集合都有自己的 SQLite 表,帶有 ec_ 前綴:

-- 新增 "posts" 集合時建立
CREATE TABLE ec_posts (
  -- 系統欄(始終存在)
  id TEXT PRIMARY KEY,
  slug TEXT UNIQUE,
  status TEXT DEFAULT 'draft',  -- draft, published, scheduled
  author_id TEXT,
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now')),
  published_at TEXT,
  deleted_at TEXT,              -- 軟刪除
  version INTEGER DEFAULT 1,    -- 樂觀鎖

  -- 內容欄(來自你的欄位定義)
  title TEXT NOT NULL,
  content JSON,                 -- Portable Text
  excerpt TEXT
);

為什麼使用每個集合一張表而不是單個包含 JSON 的內容表?

  • 真實的 SQL 欄支援適當的索引和查詢
  • 外鍵正常運作
  • Schema 在資料庫中自我記錄
  • 欄位存取無 JSON 解析開銷
  • 資料庫工具可以直接檢查 schema

Live Collections 整合

EmDash 使用 Astro 6 的 Live Collections 在執行時提供內容。內容變更立即可用,無需靜態重建。

emdashLoader() 實作了 Astro 的 LiveLoader 介面:

// src/live.config.ts
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";

export const collections = {
	_emdash: defineLiveCollection({ loader: emdashLoader() }),
};

使用提供的包裝函式查詢內容:

import { getEmDashCollection, getEmDashEntry } from "emdash";

// 取得所有已發布的文章
const { entries: posts } = await getEmDashCollection("posts");

// 取得草稿
const { entries: drafts } = await getEmDashCollection("posts", {
	status: "draft",
});

// 透過 slug 取得單個條目
const { entry: post } = await getEmDashEntry("posts", "my-post-slug");

路由注入

EmDash 整合使用 Astro 的 injectRoute API 新增後台和 API 路由:

路徑模式用途
/_emdash/admin/[...path]後台面板 SPA
/_emdash/api/manifest後台清單(集合、外掛)
/_emdash/api/content/[collection]內容條目的 CRUD
/_emdash/api/media/*媒體庫操作
/_emdash/api/schema/*Schema 管理
/_emdash/api/settings網站設定
/_emdash/api/menus/*導覽選單
/_emdash/api/taxonomies/*分類、標籤、自訂分類法

路由從 emdash 套件注入 — 不會複製到你的專案中。

資料層

EmDash 使用 Kysely 跨所有支援的資料庫進行型別安全的 SQL 查詢:

SQLite

使用 sqlite({ url: "file:./data.db" }) 進行本地開發

D1

Cloudflare 的無伺服器 SQL,使用 d1({ binding: "DB" })

libSQL

遠端 SQLite,使用 libsql({ url: "...", authToken: "..." })

資料庫組態在 astro.config.mjs 中傳遞給整合:

import { defineConfig } from "astro/config";
import emdash from "emdash/astro";
import { sqlite } from "emdash/db";
import { local } from "emdash/storage";

export default defineConfig({
	integrations: [
		emdash({
			database: sqlite({ url: "file:./data.db" }),
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

儲存抽象

媒體檔案與資料庫分開儲存。EmDash 支援:

  • 本地檔案系統 — 開發和簡單部署
  • Cloudflare R2 — 邊緣的 S3 相容物件儲存
  • S3 相容 — 任何 S3 相容的物件儲存

上傳使用簽名 URL 進行直接用戶端到儲存的上傳,繞過 Workers 的請求主體大小限制。

外掛架構

外掛透過類似 WordPress 的鉤子系統擴充 EmDash:

  • 內容鉤子content:beforeSavecontent:afterSavecontent:beforeDeletecontent:afterDelete
  • 媒體鉤子media:beforeUploadmedia:afterUpload
  • 隔離儲存 — 每個外掛都有命名空間的 KV 存取
  • 後台 UI 擴充 — 儀表板小工具、設定頁面、自訂欄位編輯器

外掛可以以兩種模式執行:

  1. 原生 — 完全存取主機環境(用於第一方外掛)
  2. 沙盒 — 在 V8 isolates 中執行,具有基於能力的權限(用於 Cloudflare 上的第三方外掛)
// astro.config.mjs
import { seoPlugin } from "@emdash-cms/plugin-seo";

emdash({
	plugins: [seoPlugin({ maxTitleLength: 60 })],
});

請求流程

典型的內容請求遵循此路徑:

  1. Astro 接收請求 — 你的頁面元件執行
  2. 查詢內容getEmDashCollection() 呼叫 Astro 的 getLiveCollection()
  3. 載入器執行emdashLoader 透過 Kysely 查詢相應的 ec_*
  4. 傳回資料 — 條目對應到 Astro 的條目格式,包含 idslugdata
  5. 頁面渲染 — 你的元件接收內容並渲染 HTML

對於後台請求:

  1. 中介軟體認證 — 驗證工作階段權杖
  2. API 路由處理請求 — 透過儲存庫進行 CRUD 操作
  3. 觸發鉤子beforeCreateafterUpdate
  4. 資料庫更新 — Kysely 執行 SQL
  5. 傳回回應 — 向後台 SPA 傳回 JSON 回應

虛擬模組

EmDash 在建置時產生虛擬模組以組態執行時:

模組用途
virtual:emdash/config資料庫和儲存組態
virtual:emdash/dialect資料庫方言工廠
virtual:emdash/plugin-admins外掛後台 UI 的靜態匯入

這種方法確保打包工具可以正確解析和 tree-shake 外掛程式碼。

下一步