外掛 Hooks

本頁內容

Hooks 讓外掛在事件發生時執行程式碼。所有 hooks 都會收到事件物件與外掛上下文。Hooks 在外掛定義時宣告,而非在 runtime 動態註冊。

Hook 簽名

每個 hook 處理器接收兩個引數:

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — 與事件相關的資料(正在儲存的內容、已上傳的媒體等)
  • ctx — 包含 storage、KV、logging 與受 capability 約束的 API 的外掛上下文

Hook 設定

Hooks 可宣告為簡單 handler,或帶完整設定:

簡單

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
handlerfunctionHook 處理函式。必填。

生命週期 Hooks

生命週期 Hooks 在外掛安裝、啟用與停用時執行。

plugin:install

在外掛首次加入網站時執行一次。

"plugin:install": async (_event, ctx) => {
  ctx.log.info("Installing plugin...");

  // Seed default data
  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");
  // Release resources, pause background work
}

事件: {}
回傳值: Promise<void>

plugin:uninstall

在外掛從網站移除時執行。

"plugin:uninstall": async (event, ctx) => {
  ctx.log.info("Uninstalling plugin...");

  if (event.deleteData) {
    // User opted to delete plugin data
    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

內容 Hooks 在建立、更新與刪除操作時執行。

content:beforeSave

在內容儲存前執行。回傳修改後的內容,或 void 表示不變。拋出錯誤可取消儲存。

"content:beforeSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  // Validate
  if (collection === "posts" && !content.title) {
    throw new Error("Posts require a title");
  }

  // Transform
  if (content.slug) {
    content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
  }

  return content;
}

事件:

{
	content: Record<string, unknown>; // Content data being saved
	collection: string; // Collection name
	isNew: boolean; // True if creating, false if updating
}

回傳值: Promise<Record<string, unknown> | void>

content:afterSave

在內容成功儲存後執行。用於副作用,如通知、logging 或與外部系統同步。

"content:afterSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  ctx.log.info(`${isNew ? "Created" : "Updated"} ${collection}/${content.id}`);

  // Trigger external sync
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:save", id: content.id })
    });
  }
}

事件:

{
	content: Record<string, unknown>; // Saved content (includes id, timestamps)
	collection: string;
	isNew: boolean;
}

回傳值: Promise<void>

content:beforeDelete

在內容刪除前執行。回傳 false 取消刪除,回傳 truevoid 允許刪除。

"content:beforeDelete": async (event, ctx) => {
  const { id, collection } = event;

  // Prevent deletion of protected content
  if (collection === "pages" && id === "home") {
    ctx.log.warn("Cannot delete home page");
    return false;
  }

  return true;
}

事件:

{
	id: string; // Content ID being deleted
	collection: string;
}

回傳值: Promise<boolean | void>

content:afterDelete

在內容成功刪除後執行。

"content:afterDelete": async (event, ctx) => {
  const { id, collection } = event;

  ctx.log.info(`Deleted ${collection}/${id}`);

  // Clean up related plugin data
  await ctx.storage.cache!.delete(`${collection}:${id}`);
}

事件:

{
	id: string;
	collection: string;
}

回傳值: Promise<void>

content:afterPublish

在內容發布(從草稿提升為上線)後執行。用於快取失效、通知或與外部系統同步等副作用。 需要 read:content capability。

"content:afterPublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Published ${collection}/${content.id}`);

  // Notify external system
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:publish", id: content.id })
    });
  }
}

事件:

{
	content: Record<string, unknown>; // Published content (includes id, timestamps)
	collection: string;
}

回傳值: Promise<void>

content:afterUnpublish

在內容取消發布(從上線恢復為草稿)後執行。用於快取失效或通知外部系統等副作用。 需要 read:content capability。

"content:afterUnpublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Unpublished ${collection}/${content.id}`);
}

事件:

{
	content: Record<string, unknown>; // Unpublished content
	collection: string;
}

回傳值: Promise<void>

媒體 Hooks

媒體 Hooks 在檔案上傳時執行。

media:beforeUpload

在檔案上傳前執行。回傳修改後的檔案資訊,或 void 表示不變。拋出錯誤可取消上傳。

"media:beforeUpload": async (event, ctx) => {
  const { file } = event;

  // Validate file type
  if (!file.type.startsWith("image/")) {
    throw new Error("Only images are allowed");
  }

  // Validate file size (10MB max)
  if (file.size > 10 * 1024 * 1024) {
    throw new Error("File too large");
  }

  // Rename file
  return {
    ...file,
    name: `${Date.now()}-${file.name}`
  };
}

事件:

{
	file: {
		name: string; // Original filename
		type: string; // MIME type
		size: number; // Size in bytes
	}
}

回傳值: Promise<{ name: string; type: string; size: number } | void>

media:afterUpload

在檔案成功上傳後執行。

"media:afterUpload": async (event, ctx) => {
  const { media } = event;

  ctx.log.info(`Uploaded ${media.filename}`, {
    id: media.id,
    size: media.size,
    mimeType: media.mimeType
  });
}

事件:

{
	media: {
		id: string;
		filename: string;
		mimeType: string;
		size: number | null;
		url: string;
		createdAt: string;
	}
}

回傳值: Promise<void>

Hook 執行順序

Hooks 依下列順序執行:

  1. priority 數值較小的先執行
  2. 優先順序相同時,依外掛註冊順序執行
  3. 帶有 dependencies 的 hooks 會等待所列外掛先完成
// Plugin A
"content:afterSave": {
  priority: 50,  // Runs first
  handler: async () => {}
}

// Plugin B
"content:afterSave": {
  priority: 100,  // Runs second (default priority)
  handler: async () => {}
}

// Plugin C
"content:afterSave": {
  priority: 200,
  dependencies: ["plugin-a"],  // Runs after A, even if priority was lower
  handler: async () => {}
}

錯誤處理

當 hook 拋出錯誤或逾時時:

  • errorPolicy: "abort" — 整條管線停止。原始操作可能失敗。
  • errorPolicy: "continue" — 記錄錯誤,其餘 hooks 仍會繼續執行。
"content:afterSave": {
  timeout: 5000,
  errorPolicy: "continue",  // Don't fail the save if this hook fails
  handler: async (event, ctx) => {
    // External API call that might fail
    await ctx.http!.fetch("https://unreliable-api.com/notify");
  }
}

逾時

Hooks 預設逾時為 5000ms(5 秒)。對可能更耗時的操作可提高該值:

"content:afterSave": {
  timeout: 30000,  // 30 seconds
  handler: async (event, ctx) => {
    // Long-running operation
  }
}

公開頁面 Hooks

公開頁面 Hooks 讓外掛向渲染頁面的 <head><body> 貢獻內容。範本透過 emdash/ui<EmDashHead><EmDashBodyStart><EmDashBodyEnd> 元件選擇接入。

page:metadata

<head> 貢獻型別化 metadata — meta 標籤、OpenGraph 屬性、canonical/alternate 連結與 JSON-LD 結構化資料。在可信模式與 sandbox 模式下皆可用。

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

貢獻類型:

Kind渲染為去重鍵
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 的 href 須為 HTTP 或 HTTPS。

page:fragments

向頁面插入點貢獻原始 HTML、指令碼或 markup。僅限可信外掛 — sandbox 外掛不能使用此 hook。

"page:fragments": async (event, ctx) => {
  return {
    kind: "external-script",
    placement: "head",
    src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
    async: true,
  };
}

回傳值: PageFragmentContribution | PageFragmentContribution[] | null

Placements:"head""body:start""body:end"。若範本未包含某 placement 的元件,則針對該 placement 的貢獻會被靜默忽略。

Hooks 速查

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透過 transport 遞送郵件void
email:afterSend寄送郵件後void
comment:beforeCreate儲存留言前修改後的事件、falsevoid
comment:moderate決定留言狀態{ status, reason? }
comment:afterCreate儲存留言後void
comment:afterModerate管理員變更留言狀態void
page:metadata頁面渲染貢獻或 null
page:fragments頁面渲染(可信)貢獻或 null

完整事件類型與 handler 簽名見 Hook 參考