Los hooks permiten que los plugins ejecuten código en respuesta a eventos. Todos los hooks reciben un objeto de evento y el contexto del plugin, y se declaran en el momento de la definición del plugin — no hay registro dinámico en tiempo de ejecución.
Esta página cubre plugins sandboxed (formato estándar). Los hooks funcionan de manera idéntica en plugins nativos; la única diferencia es que los plugins nativos también pueden registrar page:fragments, lo que los plugins sandboxed no pueden hacer.
Firma del Hook
Cada manejador de hook toma dos argumentos:
async (event: EventType, ctx: PluginContext) => ReturnType;
event— datos sobre lo que acaba de suceder (contenido que se guarda, medios cargados, transición de ciclo de vida, etc.)ctx— elPluginContextcon almacenamiento, KV, logging y APIs controladas por capacidades
Configuración de Hooks
Un hook puede declararse como un manejador simple o envuelto en un objeto de configuración:
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
dependencies: ["audit-log"],
errorPolicy: "continue",
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Opciones de configuración
| Opción | Tipo | Por defecto | Descripción |
|---|---|---|---|
priority | number | 100 | Orden de ejecución. Los números más bajos se ejecutan primero. |
timeout | number | 5000 | Tiempo máximo de ejecución en milisegundos. |
dependencies | string[] | [] | IDs de plugins que deben ejecutarse antes de este hook. |
errorPolicy | "abort" | "continue" | "abort" | Si debe detener la canalización en caso de error. |
exclusive | boolean | false | Solo un plugin puede ser el proveedor activo. Se usa para email:deliver y comment:moderate. |
handler | function | — | La función manejadora del hook. Requerido. |
Hooks de ciclo de vida
Se ejecutan durante la instalación, activación, desactivación y eliminación del plugin.
plugin:install
Se ejecuta una vez cuando el plugin se agrega por primera vez a un sitio.
"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" });
},
Event: {} — Returns: Promise<void>
plugin:activate
Se ejecuta cuando el plugin se habilita (después de la instalación o cuando se vuelve a habilitar).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Event: {} — Returns: Promise<void>
plugin:deactivate
Se ejecuta cuando el plugin se deshabilita (pero no se elimina).
"plugin:deactivate": async (_event, ctx) => {
ctx.log.info("Plugin deactivated");
},
Event: {} — Returns: Promise<void>
plugin:uninstall
Se ejecuta cuando el plugin se elimina de un sitio.
"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));
}
},
Event: { deleteData: boolean } — Returns: Promise<void>
Hooks de contenido
Se ejecutan durante las operaciones de creación, actualización y eliminación del contenido del sitio.
content:beforeSave
Se ejecuta antes de que se guarde el contenido. Devuelva contenido modificado o void para dejarlo sin cambios. Lance un error para cancelar.
"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;
},
Event: { content, collection, isNew } — Returns: contenido modificado o void.
content:afterSave
Se ejecuta después de que el contenido se haya guardado con éxito. Úselo para efectos secundarios como notificaciones, logging o sincronizaciones externas.
"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 }),
});
}
},
Event: { content, collection, isNew } — Returns: Promise<void>
content:beforeDelete
Se ejecuta antes de que se elimine el contenido. Devuelva false para cancelar; true o void lo permite.
"content:beforeDelete": async (event, ctx) => {
if (event.collection === "pages" && event.id === "home") {
ctx.log.warn("Cannot delete home page");
return false;
}
return true;
},
Event: { id, collection } — Returns: boolean | void
content:afterDelete
Se ejecuta después de que el contenido se haya eliminado con éxito.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Event: { id, collection } — Returns: Promise<void>
content:afterPublish
Se ejecuta después de que el contenido se promueva de borrador a publicado. Requiere la capacidad content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
Se ejecuta después de que el contenido se revierta de publicado a borrador. Requiere la capacidad content:read.
Event: { content, collection } — Returns: Promise<void>
Hooks de medios
media:beforeUpload
Se ejecuta antes de que se cargue un archivo. Devuelva metadatos de archivo modificados o lance un error para cancelar.
"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}` };
},
Event: { file: { name, type, size } } — Returns: archivo modificado o void
media:afterUpload
Se ejecuta después de que un archivo se haya cargado con éxito.
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Returns: Promise<void>
Hooks de páginas públicas
Estos permiten que los plugins contribuyan a las páginas públicas renderizadas. Las plantillas deben aceptar incluyendo los componentes <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribuye metadatos tipados a <head> — meta tags, propiedades OpenGraph, <link> rels permitidos y JSON-LD. Disponible tanto para plugins sandboxed como nativos. El núcleo valida, deduplica y renderiza las contribuciones; los plugins devuelven datos estructurados, nunca HTML sin procesar.
"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,
},
};
},
Event:
{
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 };
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
Tipos de contribución:
| Kind | Renderiza | Clave de deduplicación |
|---|---|---|
meta | <meta name="..." content="..."> | key o name |
property | <meta property="..." content="..."> | key o property |
link | <link rel="canonical|alternate" href="..."> | canonical: singleton; alternate: key o hreflang |
jsonld | <script type="application/ld+json"> | id (si está presente) |
La primera contribución gana para cualquier clave de deduplicación. Link rel está restringido a una lista de permitidos bloqueada por seguridad (canonical, alternate, author, license, nlweb, site.standard.document); href debe ser HTTP o HTTPS.
page:fragments
Contribuye HTML sin procesar, scripts o hojas de estilo a puntos de inserción de página. Solo plugins nativos.
Los plugins sandboxed no pueden usar este hook porque su salida se ejecuta como código de primera parte en el navegador del visitante, fuera de cualquier límite de sandbox. Para contribuciones de página seguras para sandbox, use page:metadata. Vea Plugins nativos: fragmentos de página si necesita esta superficie.
Orden de ejecución de hooks
Los hooks se ejecutan en este orden:
- Los hooks con valores de
prioritymás bajos se ejecutan primero. - Para prioridades iguales, los hooks se ejecutan en el orden de registro del plugin.
- Los hooks con
dependenciesesperan a que esos plugins se completen.
// Plugin A
"content:afterSave": { priority: 50, handler: async () => {} }
// Plugin B
"content:afterSave": { priority: 100, handler: async () => {} }
// Plugin C
"content:afterSave": {
priority: 200,
dependencies: ["plugin-a"], // waits for A even if its priority would normally be later
handler: async () => {},
}
Manejo de errores
Cuando un hook lanza un error o se agota el tiempo:
errorPolicy: "abort"— toda la canalización se detiene y la operación original puede fallar.errorPolicy: "continue"— el error se registra y los hooks restantes aún se ejecutan.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
Timeouts
Los hooks tienen un valor predeterminado de 5000ms. Aumente el timeout para trabajos más lentos:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Referencia de hooks
| Hook | Disparador | Retorno | Exclusivo |
|---|---|---|---|
plugin:install | Primera instalación del plugin | void | No |
plugin:activate | Plugin habilitado | void | No |
plugin:deactivate | Plugin deshabilitado | void | No |
plugin:uninstall | Plugin eliminado | void | No |
content:beforeSave | Antes de guardar contenido | Contenido modificado o void | No |
content:afterSave | Después de guardar contenido | void | No |
content:beforeDelete | Antes de eliminar contenido | false para cancelar, sino permitir | No |
content:afterDelete | Después de eliminar contenido | void | No |
content:afterPublish | Después de publicar contenido | void | No |
content:afterUnpublish | Después de despublicar contenido | void | No |
media:beforeUpload | Antes de subir archivo | Info de archivo modificada o void | No |
media:afterUpload | Después de subir archivo | void | No |
cron | Se dispara tarea programada | void | No |
email:beforeSend | Antes de envío de email | Mensaje modificado, false o void | No |
email:deliver | Entregar email vía transporte | void | Sí |
email:afterSend | Después de envío de email | void | No |
comment:beforeCreate | Antes de almacenar comentario | Evento modificado, false o void | No |
comment:moderate | Decidir estado del comentario | { status, reason? } | Sí |
comment:afterCreate | Después de almacenar comentario | void | No |
comment:afterModerate | Admin cambia estado del comentario | void | No |
page:metadata | Renderizado de página | Contribuciones o null | No |
page:fragments | Renderizado de página (solo nativo) | Contribuciones o null | No |
Vea la Referencia de Hooks para tipos de eventos completos y firmas de manejadores.