Hooks

本頁內容

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

設定選項

選項類型預設值描述
prioritynumber100執行順序。較小的數字先執行。
timeoutnumber5000最大執行時間(毫秒)。
dependenciesstring[][]必須在此 hook 之前執行的外掛程式 ID。
errorPolicy"abort" | "continue""abort"是否在錯誤時停止管線。
exclusivebooleanfalse只有一個外掛程式可以是活躍提供者。用於 email:delivercomment:moderate
handlerfunctionhook 處理器函式。必需。

生命週期 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 以取消;truevoid 允許。

"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="...">keyname
property<meta property="..." content="...">keyproperty
link<link rel="canonical|alternate" href="...">canonical: singleton; alternate: keyhreflang
jsonld<script type="application/ld+json">id(如果存在)

對於任何去重鍵,第一個貢獻獲勝。Link rel 限制為安全鎖定的允許清單(canonicalalternateauthorlicensenlwebsite.standard.document);href 必須是 HTTP 或 HTTPS。

page:fragments

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

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

Hook 執行順序

Hooks 按以下順序執行:

  1. 具有較低 priority 值的 hooks 先執行。
  2. 對於相同的優先順序,hooks 按外掛程式註冊順序執行。
  3. 具有 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電子郵件傳送前修改後的訊息、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

有關完整的事件類型和處理器簽章,請參閱 Hook 參考