钩子让插件在响应事件时运行代码。所有钩子都接收一个事件对象和插件上下文。钩子在插件定义时声明,而不是在运行时动态注册。
钩子签名
每个钩子处理器接收两个参数:
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");
}
}
} 配置选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
priority | number | 100 | 执行顺序。较小的数字先运行。 |
timeout | number | 5000 | 最大执行时间(毫秒)。 |
dependencies | string[] | [] | 必须在此钩子之前运行的插件 ID。 |
errorPolicy | "abort" | "continue" | "abort" | 错误时是否停止管道。 |
exclusive | boolean | false | 只有一个插件可以成为活动提供程序。用于 email:deliver 和 comment:moderate。 |
handler | function | — | 钩子处理器函数。必需。 |
生命周期钩子
生命周期钩子在插件安装、激活和停用期间运行。
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 以取消删除,返回 true 或 void 以允许。
"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>
钩子执行顺序
钩子按此顺序运行:
- 具有较低
priority值的钩子先运行 - 对于相等的优先级,钩子按插件注册顺序运行
- 具有
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="..."> | key 或 name |
property | <meta property="..." content="..."> | key 或 property |
link | <link rel="canonical|alternate" href="..."> | canonical: 单例; alternate: key 或 hreflang |
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 | 电子邮件传递之前 | 修改后的消息、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 | 否 |
有关完整的事件类型和处理器签名,请参阅钩子参考。