Hooks 允許外掛程式回應事件執行程式碼。所有 hooks 都接收一個事件物件和外掛程式上下文,並在外掛程式定義時宣告 — 執行時不存在動態註冊。
本頁面介紹沙箱(標準格式)外掛程式。Hooks 在原生外掛程式中的運作方式完全相同;唯一的區別是原生外掛程式還可以註冊 page:fragments,而沙箱外掛程式不能。
Hook 簽章
每個 hook 處理器接收兩個參數:
async (event: EventType, ctx: PluginContext) => ReturnType;
event— 關於剛剛發生的事情的資料(正在儲存的內容、上傳的媒體、生命週期轉換等)ctx— 帶有儲存、KV、日誌和能力控制的 API 的PluginContext
Hook 設定
Hook 可以宣告為簡單的處理器或包裝在設定物件中:
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
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" });
},
Event: {} — Returns: Promise<void>
plugin:activate
當外掛程式被啟用時執行(安裝後或重新啟用時)。
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Event: {} — Returns: Promise<void>
plugin:deactivate
當外掛程式被停用時執行(但未刪除)。
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Event: {} — Returns: 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));
}
},
Event: { deleteData: boolean } — Returns: 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;
},
Event: { content, collection, isNew } — Returns: 修改後的內容或 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 }),
});
}
},
Event: { content, collection, isNew } — Returns: 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;
},
Event: { id, collection } — Returns: boolean | void
content:afterDelete
在成功刪除內容後執行。
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Event: { id, collection } — Returns: Promise<void>
content:afterPublish
在內容從草稿升級到已發布後執行。需要 content:read 能力。
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
在內容從已發布恢復到草稿後執行。需要 content:read 能力。
Event: { content, collection } — Returns: 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}` };
},
Event: { file: { name, type, size } } — Returns: 修改後的檔案或 void
media:afterUpload
在成功上傳檔案後執行。
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Returns: Promise<void>
公開頁面 hooks
這些允許外掛程式為渲染的公開頁面做出貢獻。範本透過包含來自 emdash/ui 的 <EmDashHead>、<EmDashBodyStart> 和 <EmDashBodyEnd> 元件來選擇加入。
page:metadata
為 <head> 貢獻類型化的中繼資料 — 中繼標籤、OpenGraph 屬性、允許清單的 <link> rels 和 JSON-LD。可用於沙箱外掛程式和原生外掛程式。 核心驗證、去重和渲染貢獻;外掛程式傳回結構化資料,永遠不傳回原始 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,
},
};
},
Event:
{
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 };
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
貢獻類型:
| Kind | 渲染 | 去重鍵 |
|---|---|---|
meta | <meta name="..." content="..."> | key 或 name |
property | <meta property="..." content="..."> | key 或 property |
link | <link rel="canonical|alternate" href="..."> | canonical: singleton; alternate: key 或 hreflang |
jsonld | <script type="application/ld+json"> | id(如果存在) |
對於任何去重鍵,第一個貢獻獲勝。Link 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 參考。