EmDash 的外掛系統讓你在不修改核心程式碼的情況下擴充 CMS。外掛可以鉤入內容生命週期事件、儲存自己的資料、向管理員暴露設定,以及向後台面板新增自訂 UI。
設計理念
EmDash 外掛有兩種類型:沙盒化和原生。沙盒化外掛在隔離的 V8 worker 中執行,可以從市場一鍵安裝。原生外掛在處理程序中執行,並在程式碼中設定。
優先使用沙盒化外掛。 它們可以從後台 UI 安裝、更新和刪除,而無需觸及程式碼或重新部署。僅在需要需要建置時整合的功能(React 後台頁面、Portable Text 渲染元件或頁面片段注入)時使用原生外掛。
關鍵原則:
- 沙盒優先 — 為沙盒設計;僅在需要時使用原生模式
- 宣告式 — 鉤子、儲存和路由在定義時宣告,而不是動態註冊
- 型別安全 — 完全支援 TypeScript,具有型別化的上下文物件
- 基於能力 — 外掛宣告它們需要什麼;沙盒強制執行它
外掛可以做什麼
鉤入事件
在內容儲存、媒體上傳和外掛生命週期事件之前或之後執行程式碼。
儲存資料
在索引集合中持久化外掛特定的資料,無需編寫資料庫遷移。
暴露設定
宣告設定 schema 並獲得自動產生的後台 UI 以進行組態。
新增後台頁面
使用 React 元件建立自訂後台頁面和儀表板小工具。
建立 API 路由
為你的外掛的後台 UI 或外部整合暴露端點。
發出 HTTP 請求
使用宣告的主機限制呼叫外部 API 以確保安全。
外掛架構
每個外掛都使用 definePlugin() 建立:
import { definePlugin } from "emdash";
export default definePlugin({
id: "my-plugin",
version: "1.0.0",
// 外掛需要存取的 API
capabilities: ["read:content", "network:fetch"],
// 外掛可以向其發出 HTTP 請求的主機
allowedHosts: ["api.example.com"],
// 持久儲存集合
storage: {
entries: {
indexes: ["userId", "createdAt"],
},
},
// 事件處理器
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved", { id: event.content.id });
},
},
// REST API 端點
routes: {
status: {
handler: async (ctx) => ({ ok: true }),
},
},
// 後台 UI 組態
admin: {
settingsSchema: {
apiKey: { type: "secret", label: "API Key" },
},
pages: [{ path: "/dashboard", label: "Dashboard" }],
widgets: [{ id: "status", size: "half" }],
},
});
外掛上下文
每個鉤子和路由處理器都接收一個 PluginContext 物件,可以存取:
| 屬性 | 描述 | 可用性 |
|---|---|---|
ctx.storage | 外掛的文件集合 | 始終(如果宣告) |
ctx.kv | 用於設定和狀態的鍵值儲存 | 始終 |
ctx.content | 讀/寫網站內容 | 使用 read:content 或 write:content |
ctx.media | 讀/寫媒體檔案 | 使用 read:media 或 write:media |
ctx.http | 用於外部請求的 HTTP 用戶端 | 使用 network:fetch |
ctx.log | 結構化日誌記錄器(debug、info、warn、error) | 始終 |
ctx.plugin | 外掛中繼資料(id、version) | 始終 |
ctx.site | 網站資訊:name、url、locale | 始終 |
ctx.url() | 從路徑產生絕對 URL | 始終 |
ctx.users | 讀取使用者資訊:get()、getByEmail()、list() | 使用 read:users |
ctx.cron | 排程任務:schedule()、cancel()、list() | 始終 |
ctx.email | 傳送電子郵件:send() | 使用 email:send + 組態提供程式 |
上下文形狀在所有鉤子和路由中都是相同的。受能力限制的屬性僅在外掛宣告所需能力時才存在。
能力
能力決定了外掛上下文中可用的 API:
| 能力 | 授予存取權限 |
|---|---|
read:content | ctx.content.get()、ctx.content.list() |
write:content | ctx.content.create()、ctx.content.update()、ctx.content.delete() |
read:media | ctx.media.get()、ctx.media.list() |
write:media | ctx.media.getUploadUrl()、ctx.media.upload()、ctx.media.delete() |
network:fetch | ctx.http.fetch()(限制為 allowedHosts) |
network:fetch:any | ctx.http.fetch()(無限制 — 用於使用者設定的 URL) |
read:users | ctx.users.get()、ctx.users.getByEmail()、ctx.users.list() |
email:send | ctx.email.send()(需要提供程式外掛) |
email:provide | 註冊 email:deliver 獨佔鉤子(傳輸提供程式) |
email:intercept | 註冊 email:beforeSend / email:afterSend 鉤子 |
page:inject | 註冊 page:metadata / page:fragments 鉤子 |
註冊
在你的 Astro 組態中註冊外掛:
import { defineConfig } from "astro/config";
import { emdash } from "emdash/astro";
import seoPlugin from "@emdash-cms/plugin-seo";
import auditLogPlugin from "@emdash-cms/plugin-audit-log";
export default defineConfig({
integrations: [
emdash({
plugins: [seoPlugin({ generateSitemap: true }), auditLogPlugin({ retentionDays: 90 })],
}),
],
});
外掛在建置時解析。對於具有相同優先順序的鉤子,順序很重要 — 陣列中較早的外掛先執行。
執行模式
EmDash 支援兩種外掛執行模式:
| 模式 | 描述 | 平台 |
|---|---|---|
| 沙盒化 | 具有強制限制的隔離 V8 worker | 僅 Cloudflare |
| 原生 | 處理程序中,具有完全存取權限 | 任何 |
在沙盒模式下,能力在執行時層級強制執行 — 外掛只能存取它們宣告的內容。在原生模式下,能力是建議性的,外掛具有完全的處理程序存取權限。