Hooks 允許外掛程式回應事件執行程式碼。所有 hooks 都會接收一個事件物件和外掛程式上下文,並在外掛程式定義時宣告 — 執行時沒有動態註冊。
本頁面介紹沙箱外掛程式。Hooks 在原生外掛程式中的運作方式完全相同;原生外掛程式還可以額外註冊 page:fragments。
Hook 簽章
每個 hook 處理器接收兩個參數:
async (event, ctx) => ReturnType;
event— 關於剛剛發生事件的資料(儲存內容、上傳媒體、生命週期轉換等)ctx—PluginContext,包含儲存、KV、日誌和功能控制的 API
預設匯出上的 satisfies SandboxedPlugin 會從 hook 名稱推斷 event(完整的規範事件類型)並將 ctx 推斷為 PluginContext,因此處理器不需要參數註釋。要在輔助函式中按名稱引用事件類型,請從 emdash/plugin 匯入。
Hook 設定
Hook 可以宣告為簡單的處理器或包裝在設定物件中:
簡單
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, 完整設定
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
dependencies: ["audit-log"],
errorPolicy: "continue",
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, 設定選項
| 選項 | 類型 | 預設值 | 描述 |
|---|---|---|---|
priority | number | 100 | 執行順序。較小的數字先執行。 |
timeout | number | 5000 | 最大執行時間(毫秒)。 |
dependencies | string[] | [] | 必須在此 hook 之前執行的外掛程式 ID。 |
errorPolicy | "abort" | "continue" | "abort" | 出錯時是否停止管道。 |
exclusive | boolean | false | 只有一個外掛程式可以成為活動提供者。用於 email:deliver 和 comment:moderate。 |
handler | function | — | Hook 處理器函式。必需。 |
生命週期 hooks
在外掛程式安裝、啟用、停用和移除期間執行。
plugin:install
在外掛程式首次新增到網站時執行一次。
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.kv.set("settings:enabled", true);
await ctx.storage.items.put("default", { name: "Default Item" });
},
事件: {} — 傳回: Promise<void>
plugin:activate
在外掛程式啟用時執行(安裝後或重新啟用時)。
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
事件: {} — 傳回: Promise<void>
plugin:deactivate
在外掛程式停用時執行(但未移除)。
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
事件: {} — 傳回: Promise<void>
plugin:uninstall
在外掛程式從網站移除時執行。
"plugin:uninstall": async (event, ctx) => {
ctx.log.info("Uninstalling plugin...");
if (event.deleteData) {
const result = await ctx.storage.items.query({ limit: 1000 });
await ctx.storage.items.deleteMany(result.items.map((i) => i.id));
}
},
事件: { deleteData: boolean } — 傳回: Promise<void>
內容 hooks
在網站內容的建立、更新和刪除操作期間執行。
content:beforeSave
在內容儲存之前執行。傳回修改後的內容,或傳回 void 保持不變。拋出錯誤以取消。
"content:beforeSave": async (event, ctx) => {
const { content, collection } = event;
if (collection === "posts" && !content.title) {
throw new Error("Posts require a title");
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
事件: { content, collection, isNew } — 傳回: 修改後的內容或 void。
content:afterSave
在內容成功儲存後執行。用於通知、日誌記錄或外部同步等副作用。
"content:afterSave": async (event, ctx) => {
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${event.content.id}`);
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: event.content.id }),
});
}
},
事件: { content, collection, isNew } — 傳回: Promise<void>
content:beforeDelete
在內容刪除之前執行。傳回 false 以取消;true 或 void 允許。
"content:beforeDelete": async (event, ctx) => {
if (event.collection === "pages" && event.id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
},
事件: { id, collection } — 傳回: boolean | void
content:afterDelete
在內容成功刪除後執行。
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
事件: { id, collection } — 傳回: Promise<void>
content:afterPublish
在內容從草稿提升為發布後執行。需要 content:read 能力。
事件: { content, collection } — 傳回: Promise<void>
content:afterUnpublish
在內容從發布恢復為草稿後執行。需要 content:read 能力。
事件: { content, collection } — 傳回: Promise<void>
媒體 hooks
media:beforeUpload
在檔案上傳之前執行。傳回修改後的檔案中繼資料或拋出錯誤以取消。
"media:beforeUpload": async (event, ctx) => {
if (!event.file.type.startsWith("image/")) {
throw new Error("Only images are allowed");
}
if (event.file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
return { ...event.file, name: `${Date.now()}-${event.file.name}` };
},
事件: { file: { name, type, size } } — 傳回: 修改後的檔案或 void
media:afterUpload
在檔案成功上傳後執行。
事件: { media: { id, filename, mimeType, size, url, createdAt } } — 傳回: Promise<void>
公共頁面 hooks
這些允許外掛程式為渲染的公共頁面做貢獻。模板透過包含來自 emdash/ui 的 <EmDashHead>、<EmDashBodyStart> 和 <EmDashBodyEnd> 元件來選擇加入。
page:metadata
向 <head> 貢獻類型化中繼資料 — meta 標籤、OpenGraph 屬性、白名單 <link> rel 和 JSON-LD。沙箱和原生外掛程式均可使用。 Core 驗證、去重並渲染貢獻;外掛程式傳回結構化資料,從不傳回原始 HTML。
"page:metadata": async (event, ctx) => {
if (event.page.kind !== "content") return null;
return {
kind: "jsonld",
id: `schema:${event.page.content?.collection}:${event.page.content?.id}`,
graph: {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: event.page.pageTitle ?? event.page.title,
description: event.page.description,
},
};
},
事件:
{
page: {
url: string;
path: string;
locale: string | null;
kind: "content" | "custom";
pageType: string;
title: string | null;
pageTitle?: string | null;
description: string | null;
canonical: string | null;
image: string | null;
content?: { collection: string; id: string; slug: string | null };
}
}
傳回: PageMetadataContribution | PageMetadataContribution[] | null
貢獻類型:
| 類型 | 渲染 | 去重鍵 |
|---|---|---|
meta | <meta name="..." content="..."> | key 或 name |
property | <meta property="..." content="..."> | key 或 property |
link | <link rel="canonical|alternate" href="..."> | canonical:單例;alternate:key 或 hreflang |
jsonld | <script type="application/ld+json"> | id(如果存在) |
對於任何去重鍵,第一個貢獻獲勝。連結 rel 被限制為安全鎖定的白名單(canonical、alternate、author、license、nlweb、site.standard.document);href 必須是 HTTP 或 HTTPS。
page:fragments
向頁面插入點貢獻原始 HTML、指令碼或樣式表。僅限原生外掛程式。
沙箱外掛程式無法使用此 hook,因為其輸出在訪問者的瀏覽器中作為第一方程式碼執行,在任何沙箱邊界之外。對於沙箱安全的頁面貢獻,請使用 page:metadata。如果需要此功能,請參閱原生外掛程式:頁面片段。
Hook 執行順序
Hooks 按以下順序執行:
priority值較小的 hooks 先執行。- 對於相同的優先順序,hooks 按外掛程式註冊順序執行。
- 帶有
dependencies的 hooks 等待那些外掛程式完成。
// Plugin A
"content:afterSave": { priority: 50, handler: async () => {} }
// Plugin B
"content:afterSave": { priority: 100, handler: async () => {} }
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // waits for A even if its priority would normally be later
handler: async () => {},
}
錯誤處理
當 hook 拋出錯誤或逾時時:
errorPolicy: "abort"— 整個管道停止,原始操作可能失敗。errorPolicy: "continue"— 錯誤被記錄,剩餘的 hooks 繼續執行。
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
逾時
Hooks 預設為 5000ms。為較慢的工作增加逾時:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Hook 參考
| Hook | 觸發器 | 傳回 | 獨占 |
|---|---|---|---|
plugin:install | 首次外掛程式安裝 | void | 否 |
plugin:activate | 外掛程式啟用 | void | 否 |
plugin:deactivate | 外掛程式停用 | void | 否 |
plugin:uninstall | 外掛程式移除 | void | 否 |
content:beforeSave | 內容儲存前 | 修改後的內容或 void | 否 |
content:afterSave | 內容儲存後 | void | 否 |
content:beforeDelete | 內容刪除前 | false 取消,否則允許 | 否 |
content:afterDelete | 內容刪除後 | void | 否 |
content:afterPublish | 內容發布後 | void | 否 |
content:afterUnpublish | 內容取消發布後 | void | 否 |
media:beforeUpload | 檔案上傳前 | 修改後的檔案資訊或 void | 否 |
media:afterUpload | 檔案上傳後 | void | 否 |
cron | 計畫任務觸發 | void | 否 |
email:beforeSend | 郵件傳送前 | 修改後的訊息、false 或 void | 否 |
email:deliver | 透過傳輸傳送郵件 | void | 是 |
email:afterSend | 郵件傳送後 | void | 否 |
comment:beforeCreate | 評論儲存前 | 修改後的事件、false 或 void | 否 |
comment:moderate | 決定評論狀態 | { status, reason? } | 是 |
comment:afterCreate | 評論儲存後 | void | 否 |
comment:afterModerate | 管理員更改評論狀態 | void | 否 |
page:metadata | 頁面渲染 | 貢獻或 null | 否 |
page:fragments | 頁面渲染(僅限原生) | 貢獻或 null | 否 |
有關完整的事件類型和處理器簽章,請參閱 Hook 參考。