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 會:
- 在
_emdash_collections和_emdash_fields中插入記錄 - 執行
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:beforeSave、content:afterSave、content:beforeDelete、content:afterDelete - 媒體鉤子 —
media:beforeUpload、media:afterUpload - 隔離儲存 — 每個外掛都有命名空間的 KV 存取
- 後台 UI 擴充 — 儀表板小工具、設定頁面、自訂欄位編輯器
外掛可以以兩種模式執行:
- 原生 — 完全存取主機環境(用於第一方外掛)
- 沙盒 — 在 V8 isolates 中執行,具有基於能力的權限(用於 Cloudflare 上的第三方外掛)
// astro.config.mjs
import { seoPlugin } from "@emdash-cms/plugin-seo";
emdash({
plugins: [seoPlugin({ maxTitleLength: 60 })],
});
請求流程
典型的內容請求遵循此路徑:
- Astro 接收請求 — 你的頁面元件執行
- 查詢內容 —
getEmDashCollection()呼叫 Astro 的getLiveCollection() - 載入器執行 —
emdashLoader透過 Kysely 查詢相應的ec_*表 - 傳回資料 — 條目對應到 Astro 的條目格式,包含
id、slug和data - 頁面渲染 — 你的元件接收內容並渲染 HTML
對於後台請求:
- 中介軟體認證 — 驗證工作階段權杖
- API 路由處理請求 — 透過儲存庫進行 CRUD 操作
- 觸發鉤子 —
beforeCreate、afterUpdate等 - 資料庫更新 — Kysely 執行 SQL
- 傳回回應 — 向後台 SPA 傳回 JSON 回應
虛擬模組
EmDash 在建置時產生虛擬模組以組態執行時:
| 模組 | 用途 |
|---|---|
virtual:emdash/config | 資料庫和儲存組態 |
virtual:emdash/dialect | 資料庫方言工廠 |
virtual:emdash/plugin-admins | 外掛後台 UI 的靜態匯入 |
這種方法確保打包工具可以正確解析和 tree-shake 外掛程式碼。