跳转到内容

插件钩子

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

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

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

钩子可以声明为简单的处理程序,也可以使用完整配置:

hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
}
}
选项类型默认值描述
prioritynumber100执行顺序。数值越小越先运行。
timeoutnumber5000最大执行时间(毫秒)。
dependenciesstring[][]必须在此钩子之前运行的插件 ID。
errorPolicy"abort" | "continue""abort"出错时是否停止整个流水线。
exclusivebooleanfalse只有一个插件可以作为活动提供者。用于 email:deliver 和 comment:moderate。
handlerfunction—钩子处理函数。必需。

生命周期钩子在插件安装、激活和停用时运行。

在插件首次添加到站点时运行一次。

"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": async (_event, ctx) => {
ctx.log.info("Plugin activated");
}

事件: {}
返回: Promise<void>

在插件禁用时运行(但未移除)。

"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
// Release resources, pause background work
}

事件: {}
返回: Promise<void>

在插件从站点移除时运行。

"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>

内容钩子在创建、更新和删除操作期间运行。

在内容保存之前运行。返回修改后的内容或 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": 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>

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

"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": 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>

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

在文件上传之前运行。返回修改后的文件信息或 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": 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 的钩子会等待这些插件完成
// 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 () => {}
}

当钩子抛出错误或超时时:

  • errorPolicy: "abort" — 整个流水线停止。原始操作可能失败。
  • errorPolicy: "continue" — 错误被记录,剩余的钩子仍会运行。
"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");
}
}

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

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

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

向 <head> 贡献类型化的元数据——meta 标签、OpenGraph 属性、规范/备用链接以及 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.title,
description: event.page.description,
},
};
}

事件:

{
page: {
url: string;
path: string;
locale: string | null;
kind: "content" | "custom";
pageType: string;
title: 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。

向页面插入点贡献原始 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否
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否

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