插件钩子

本页内容

钩子让插件在响应事件时运行代码。所有钩子都接收一个事件对象和插件上下文。钩子在插件定义时声明,而不是在运行时动态注册。

钩子签名

每个钩子处理器接收两个参数:

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — 关于事件的数据(正在保存的内容、上传的媒体等)
  • ctx — 带有存储、KV、日志记录和能力限制 API 的插件上下文

钩子配置

钩子可以声明为简单处理器或完整配置:

简单

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, isNew } = event;

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

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

  return content;
}

事件:

{
	content: Record<string, unknown>; // 正在保存的内容数据
	collection: string; // 集合名称
	isNew: boolean; // 如果创建则为 true,更新则为 false
}

返回: Promise<Record<string, unknown> | void>

content:afterSave

在内容成功保存后运行。用于副作用,如通知、日志记录或同步到外部系统。

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

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

  // 触发外部同步
  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>; // 已保存的内容(包括 id、时间戳)
	collection: string;
	isNew: boolean;
}

返回: Promise<void>

content:beforeDelete

在内容删除之前运行。返回 false 以取消删除,返回 truevoid 以允许。

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

  // 防止删除受保护的内容
  if (collection === "pages" && id === "home") {
    ctx.log.warn("Cannot delete home page");
    return false;
  }

  return true;
}

事件:

{
	id: string; // 正在删除的内容 ID
	collection: string;
}

返回: Promise<boolean | void>

content:afterDelete

在内容成功删除后运行。

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

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

  // 清理相关的插件数据
  await ctx.storage.cache!.delete(`${collection}:${id}`);
}

事件:

{
	id: string;
	collection: string;
}

返回: Promise<void>

content:afterPublish

在内容发布后运行(从草稿提升到实时)。用于副作用,如缓存失效、通知或同步到外部系统。 需要 read:content 能力。

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

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

  // 通知外部系统
  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>; // 已发布的内容(包括 id、时间戳)
	collection: string;
}

返回: Promise<void>

content:afterUnpublish

在内容取消发布后运行(从实时恢复到草稿)。用于副作用,如缓存失效或通知外部系统。 需要 read:content 能力。

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

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

事件:

{
	content: Record<string, unknown>; // 未发布的内容
	collection: string;
}

返回: Promise<void>

媒体钩子

媒体钩子在文件上传期间运行。

media:beforeUpload

在文件上传之前运行。返回修改后的文件信息或 void 以保持不变。抛出异常以取消上传。

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

  // 验证文件类型
  if (!file.type.startsWith("image/")) {
    throw new Error("Only images are allowed");
  }

  // 验证文件大小(最大 10MB)
  if (file.size > 10 * 1024 * 1024) {
    throw new Error("File too large");
  }

  // 重命名文件
  return {
    ...file,
    name: `${Date.now()}-${file.name}`
  };
}

事件:

{
	file: {
		name: string; // 原始文件名
		type: string; // MIME 类型
		size: number; // 大小(字节)
	}
}

返回: 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>

钩子执行顺序

钩子按此顺序运行:

  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) => {
    // 可能失败的外部 API 调用
    await ctx.http!.fetch("https://unreliable-api.com/notify");
  }
}

超时

钩子的默认超时为 5000ms(5 秒)。对于可能需要更长时间的操作,增加它:

"content:afterSave": {
  timeout: 30000,  // 30 秒
  handler: async (event, ctx) => {
    // 长时间运行的操作
  }
}

公共页面钩子

公共页面钩子让插件为渲染页面的 <head><body> 做出贡献。模板使用 emdash/ui 中的 <EmDashHead><EmDashBodyStart><EmDashBodyEnd> 组件选择加入。

page:metadata

<head> 贡献类型化元数据 — meta 标签、OpenGraph 属性、canonical/alternate 链接和 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,
    },
  };
}

事件:

{
  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(如果存在)

任何去重键的第一个贡献获胜。链接 href 必须是 HTTP 或 HTTPS。

page:fragments

向页面插入点贡献原始 HTML、脚本或标记。仅原生插件 — 沙盒插件无法使用此钩子。

"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

放置位置:"head""body:start""body:end"。省略放置位置组件的模板会静默忽略针对它的贡献。

钩子参考

钩子触发器返回独占
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

有关完整的事件类型和处理器签名,请参阅钩子参考