钩子让插件能够响应事件来运行代码。所有钩子接收一个事件对象和插件上下文,并在插件定义时声明——运行时没有动态注册。
本页涵盖沙箱插件。钩子在原生插件中的工作方式相同;原生插件还可以注册 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");
},
},
}, 配置选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
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 } = 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 以取消;true 或 void 允许操作。
"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="..."> | 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(如果存在) |
每个去重键的第一个贡献优先。Link rel 限制为安全锁定的允许列表(canonical、alternate、author、license、nlweb、site.standard.document);href 必须是 HTTP 或 HTTPS。
page:fragments
向页面插入点贡献原始 HTML、脚本或样式表。仅限原生插件。
沙箱插件不能使用此钩子,因为其输出在访问者的浏览器中作为第一方代码运行,位于任何沙箱边界之外。对于沙箱安全的页面贡献,请使用 page:metadata。如果需要此功能,请参阅原生插件:页面片段。
钩子执行顺序
钩子按以下顺序执行:
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) => {
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 | 邮件发送前 | 修改后的消息、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 | 否 |
有关完整的事件类型和处理器签名,请参阅钩子参考。