EmDash 管理後台是嵌入在 Astro 網站中的 React 單頁應用,為編輯與管理員提供完整的內容管理介面。
架構概覽
┌────────────────────────────────────────────────────────────────┐
│ Astro 外殼 │
│ /_emdash/admin/[...path].astro │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ React SPA │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │ │
│ │ │ TanStack │ │ TanStack │ │ Kumo │ │ │
│ │ │ Router │ │ Query │ │ 元件 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ REST API 客戶端 │ │ │
│ │ │ /_emdash/api/* │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────┘
管理後台是 React「大型島嶼」。Astro 負責外殼與驗證;管理後台內的導航與渲染全部在客戶端完成。
技術棧
| 層級 | 技術 | 作用 |
|---|---|---|
| 路由 | TanStack Router | 類型安全的客戶端路由 |
| 資料 | TanStack Query | 服務端狀態、快取、變更 |
| UI | Kumo | 可存取元件(Base UI + Tailwind) |
| 表格 | TanStack Table | 排序、篩選、分頁 |
| 表單 | React Hook Form + Zod | 與伺服器 schema 一致的驗證 |
| 圖標 | Phosphor | 統一圖標 |
| 編輯器 | TipTap | 富文字編輯(Portable Text) |
路由結構
管理後台掛載在 /_emdash/admin/,使用客戶端路由。
| 路徑 | 介面 |
|---|---|
/ | 儀表板 |
/content/:collection | 內容列表 |
/content/:collection/:id | 內容編輯器 |
/content/:collection/new | 新建條目 |
/media | 媒體庫 |
/content-types | Schema 構建器(僅管理員) |
/menus | 導航選單 |
/widgets | 小工具區域 |
/taxonomies | 分類 / 標籤管理 |
/settings | 網站設定 |
/plugins/:pluginId/* | 外掛頁面 |
清單驅動的 UI
管理後台不把集合或外掛寫死在程式碼裡,而是從伺服器拉取清單(manifest)。
GET /_emdash/api/manifest
回應:
{
"collections": [
{
"slug": "posts",
"label": "Blog Posts",
"labelSingular": "Post",
"icon": "file-text",
"supports": ["drafts", "revisions", "preview"],
"fields": [
{ "slug": "title", "type": "string", "required": true },
{ "slug": "content", "type": "portableText" }
]
}
],
"plugins": [
{
"id": "audit-log",
"label": "Audit Log",
"adminPages": [{ "path": "history", "label": "Audit History" }],
"widgets": [{ "id": "recent-activity", "title": "Recent Activity" }]
}
],
"taxonomies": [{ "name": "category", "label": "Categories", "hierarchical": true }],
"version": "abc123"
}
管理後台僅憑此清單構建導航、表單與編輯器。優點包括:
- Schema 變更立即生效 — 無需重建管理後台
- 外掛 UI 自動接入 — 頁面與小元件來自清單
- 邊界類型安全 — Zod schema 保留在伺服器端
資料流
- 加載管理後台 SPA — 初始化 TanStack Router
- 取得清單 — TanStack Query 快取集合/外掛元資料
- 構建導航 — 側欄由清單生成
- 用戶導航 — 客戶端路由,無整頁刷新
- 拉取資料 — TanStack Query 請求 REST API
- 渲染表單 — 由清單中的欄位描述生成欄位編輯器
- 提交變更 — 通過 TanStack Query 變更與樂觀更新
- 服務端驗證 — 服務端 Zod,錯誤以 JSON 返回
REST API 端點
管理後台僅通過 REST API 通信。
內容 API
| 方法 | 端點 | 用途 |
|---|---|---|
GET | /api/content/:collection | 列出條目 |
POST | /api/content/:collection | 建立條目 |
GET | /api/content/:collection/:id | 取得條目 |
PUT | /api/content/:collection/:id | 更新條目 |
DELETE | /api/content/:collection/:id | 軟刪除 |
GET | /api/content/:collection/:id/revisions | 修訂列表 |
POST | /api/content/:collection/:id/preview-url | 生成預覽 URL |
Schema API
| 方法 | 端點 | 用途 |
|---|---|---|
GET | /api/schema | 導出完整 schema |
GET | /api/schema/collections | 列出集合 |
POST | /api/schema/collections | 建立集合 |
PUT | /api/schema/collections/:slug | 更新集合 |
DELETE | /api/schema/collections/:slug | 刪除集合 |
POST | /api/schema/collections/:slug/fields | 添加欄位 |
PUT | /api/schema/collections/:slug/fields/:field | 更新欄位 |
DELETE | /api/schema/collections/:slug/fields/:field | 刪除欄位 |
媒體 API
| 方法 | 端點 | 用途 |
|---|---|---|
GET | /api/media | 列出媒體 |
POST | /api/media/upload-url | 取得簽署上傳 URL |
POST | /api/media/:id/confirm | 確認上傳完成 |
DELETE | /api/media/:id | 刪除媒體 |
GET | /api/media/file/:key | 提供媒體文件 |
其他 API
| 端點 | 用途 |
|---|---|
/api/settings | 網站設定(GET/POST) |
/api/menus/* | 導航選單 |
/api/widget-areas/* | 小工具管理 |
/api/taxonomies/* | 分類術語 |
/api/admin/plugins/* | 外掛狀態 |
分頁
所有列表端點均使用基於遊標的分頁。
{
"items": [...],
"nextCursor": "eyJpZCI6IjAxSjEyMzQ1NiJ9"
}
取得下一頁:
GET /api/content/posts?cursor=eyJpZCI6IjAxSjEyMzQ1NiJ9
外掛管理後台 UI
外掛可通過頁面與儀表板小工具擴展管理後台。整合會生成帶靜態 import 的虛擬模塊。
// virtual:emdash/plugin-admins(生成)
import * as pluginAdmin0 from "@emdash-cms/plugin-seo/admin";
import * as pluginAdmin1 from "@emdash-cms/plugin-analytics/admin";
export const pluginAdmins = {
seo: pluginAdmin0,
analytics: pluginAdmin1,
};
外掛頁面
外掛頁面掛載在 /_emdash/admin/plugins/:pluginId/* 下。
// @emdash-cms/plugin-seo/src/admin.tsx
export const pages = [
{
path: "settings",
component: SEOSettingsPage,
label: "SEO Settings",
},
];
渲染 URL:/_emdash/admin/plugins/seo/settings
儀表板小工具
外掛可向儀表板添加小工具。
export const widgets = [
{
id: "seo-overview",
component: SEOWidget,
title: "SEO Overview",
size: "half", // "full" | "half" | "third"
},
];
驗證
管理後台外殼路由由 Astro 中間件強制驗證。
// 簡化的中間件示例
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();
}
管理後台 SPA 本身不處理登錄;設定會話 Cookie 的是 Astro 頁面。
基於角色的存取
不同角色可見的管理後台範圍不同。
| 角色 | 可見區塊 |
|---|---|
| Editor | 儀表板、分配的集合、媒體 |
| Admin | 另含內容類型、全部集合、設定 |
| Developer | 另含 CLI 存取、生成的類型 |
清單端點會按請求用戶角色過濾集合與功能。
內容編輯器
內容編輯器根據欄位定義動態生成表單。
// 簡化的編輯器渲染示例
function ContentEditor({ collection, fields }) {
return (
<form>
{fields.map((field) => (
<FieldWidget
key={field.slug}
type={field.type}
label={field.label}
required={field.required}
options={field.options}
/>
))}
</form>
);
}
每種欄位類型有對應的小元件。
| 欄位類型 | 小元件 |
|---|---|
string | 文字輸入 |
text | 文字域 |
number | 數字輸入 |
boolean | 開關 |
datetime | 日期時間選擇器 |
select | 下拉框 |
multiSelect | 多選 |
portableText | TipTap 編輯器 |
image | 媒體選擇器 |
reference | 條目選擇器 |
富文字編輯器
Portable Text 欄位使用 TipTap(ProseMirror)編輯。
用戶輸入 → TipTap(ProseMirror JSON)→ 保存 → Portable Text(DB)
加載 → Portable Text(DB)→ TipTap(ProseMirror JSON)→ 展示
在加載/保存邊界通過 portableTextToProsemirror() 與 prosemirrorToPortableText() 轉換。
支持的塊:
- 段落、標題(H1–H6)
- 項目符號與編號列表
- 引用、程式碼塊
- 圖片(來自媒體庫)
- 連結
來自外掛或導入的未知塊以只讀佔位保留。
媒體庫
媒體庫提供:
- 網格與列表視圖
- 按類型、日期搜尋與篩選
- 拖放上傳
- 帶元資料的圖片預覽
- 批量選擇與刪除
上傳使用簽署 URL,由客戶端直傳存儲。
- 請求上傳 URL —
POST /api/media/upload-url - 直傳 — 客戶端向簽署 URL(R2/S3)PUT 文件
- 確認上傳 —
POST /api/media/:id/confirm - 服務端提取元資料 — 尺寸、MIME 類型等
該方式可繞過 Workers 請求體大小限制,並獲得真實上傳進度。