鉤子

本頁內容

鉤子讓外掛能夠回應事件來執行程式碼。所有鉤子接收一個事件物件和外掛上下文,並在外掛定義時宣告——執行階段沒有動態註冊。

本頁涵蓋沙箱外掛。鉤子在原生外掛中的運作方式相同;原生外掛還可以註冊 page:fragments

鉤子簽名

每個鉤子處理器接受兩個參數:

async (event, ctx) => ReturnType;
  • event — 關於剛發生事情的資料(正在儲存的內容、上傳的媒體、生命週期轉換等)
  • ctx — 具有儲存、KV、日誌和能力控制 API 的 PluginContext

預設匯出上的 satisfies SandboxedPlugin 從鉤子名稱(完整的規範事件類型)推斷 event,並將 ctx 推斷為 PluginContext,因此處理器不需要參數註解。要在輔助函數中按名稱參考事件類型,請從 emdash/plugin 匯入。

鉤子設定

鉤子可以宣告為簡單處理器或包裝在設定物件中:

簡單

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");
		},
	},
},

設定選項

選項型別預設值說明
prioritynumber100執行順序。數值越小越先執行。
timeoutnumber5000最大執行時間(毫秒)。
dependenciesstring[][]必須在此鉤子之前執行的外掛 ID。
errorPolicy"abort" | "continue""abort"是否在錯誤時停止管線。
exclusivebooleanfalse只能有一個外掛作為活動提供者。用於 email:delivercomment:moderate
handlerfunction鉤子處理器函數。必需。

生命週期鉤子

在外掛安裝、啟用、停用和移除期間執行。

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>

內容鉤子

在站點內容的建立、更新和刪除操作期間執行。

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 以取消;truevoid 允許操作。

"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>

content:afterRestore

在已刪除的內容恢復後執行。需要 content:read 能力。

事件: { content, collection }回傳: Promise<void>

content:afterSchedule

在內容被排程為將來發佈後執行。需要 content:read 能力。

事件: { content, collection }回傳: Promise<void>

content:afterUnschedule

在已排程的內容被取消排程後執行。需要 content:read 能力。

事件: { content, collection }回傳: Promise<void>

媒體鉤子

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>

公開頁面鉤子

這些鉤子讓外掛能夠為渲染的公開頁面做貢獻。範本透過包含來自 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="...">keyname
property<meta property="..." content="...">keyproperty
link<link rel="canonical|alternate" href="...">canonical: 單例; alternate: keyhreflang
jsonld<script type="application/ld+json">id(如果存在)

每個去重鍵的第一個貢獻優先。Link rel 限制為安全鎖定的允許清單(canonicalalternateauthorlicensenlwebsite.standard.document);href 必須是 HTTP 或 HTTPS。

page:fragments

向頁面插入點貢獻原始 HTML、指令碼或樣式表。僅限原生外掛。

沙箱外掛不能使用此鉤子,因為其輸出在訪客的瀏覽器中作為第一方程式碼執行,位於任何沙箱邊界之外。對於沙箱安全的頁面貢獻,請使用 page:metadata。如果需要此功能,請參閱原生外掛:頁面片段

鉤子執行順序

鉤子按以下順序執行:

  1. priority 值較低的鉤子先執行。
  2. 優先順序相同時,按外掛註冊順序執行。
  3. 具有 dependencies 的鉤子等待這些外掛完成。
// 外掛 A
"content:afterSave": { priority: 50, handler: async () => {} }

// 外掛 B
"content:afterSave": { priority: 100, handler: async () => {} }

// 外掛 C
"content:afterSave": {
	priority: 200,
	dependencies: ["plugin-a"],   // 即使優先順序通常會更晚,也會等待 A
	handler: async () => {},
}

錯誤處理

當鉤子拋出例外或逾時時:

  • errorPolicy: "abort" — 整個管線停止,原始操作可能失敗。
  • errorPolicy: "continue" — 錯誤被記錄,其餘鉤子繼續執行。
"content:afterSave": {
	timeout: 5000,
	errorPolicy: "continue",
	handler: async (event, ctx) => {
		await ctx.http!.fetch("https://unreliable-api.com/notify");
	},
},

逾時

鉤子預設逾時為 5000ms。對於較慢的工作增加逾時:

"content:afterSave": {
	timeout: 30000,
	handler: async (event, ctx) => {
		// 長時間執行的操作
	},
},

鉤子參考

鉤子觸發器回傳值排他
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
content:afterRestore內容恢復後void
content:afterSchedule內容排程後void
content:afterUnschedule取消排程後void
media:beforeUpload檔案上傳前修改後的檔案資訊或 void
media:afterUpload檔案上傳後void
cron排程任務觸發void
email:beforeSend郵件傳送前修改後的訊息、falsevoid
email:deliver透過傳輸傳送郵件void
email:afterSend郵件傳送後void
comment:beforeCreate留言儲存前修改後的事件、falsevoid
comment:moderate決定留言狀態{ status, reason? }
comment:afterCreate留言儲存後void
comment:afterModerate管理員更改留言狀態void
page:metadata頁面渲染貢獻或 null
page:fragments頁面渲染(僅原生)貢獻或 null

有關完整的事件類型和處理器簽名,請參閱鉤子參考