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 en sandbox. Los hooks funcionan de manera idéntica en plugins nativos; los plugins nativos pueden registrar adicionalmente page:fragments.
Firma del hook
Cada manejador de hook toma dos argumentos:
async (event, ctx) => ReturnType;
event— datos sobre lo que acaba de suceder (contenido guardado, medios subidos, transición de ciclo de vida, etc.)ctx— elPluginContextcon almacenamiento, KV, registro y APIs controladas por capacidades
satisfies SandboxedPlugin en la exportación predeterminada infiere event del nombre del hook (el tipo de evento canónico completo) y ctx como PluginContext, por lo que los manejadores no necesitan anotaciones de parámetros. Para referenciar un tipo de evento por nombre en un helper, impórtalo desde emdash/plugin.
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");
},
}, Configuración completa
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 | Predeterminado | 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 detener o no el pipeline en caso de error. |
exclusive | boolean | false | Solo un plugin puede ser el proveedor activo. Usado para email:deliver y comment:moderate. |
handler | function | — | La función manejadora del hook. Requerida. |
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" });
},
Evento: {} — Retorna: 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");
},
Evento: {} — Retorna: 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");
},
Evento: {} — Retorna: 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));
}
},
Evento: { deleteData: boolean } — Retorna: 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. Retorna contenido modificado o void para dejarlo sin cambios. Lanza 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;
},
Evento: { content, collection, isNew } — Retorna: contenido modificado o void.
content:afterSave
Se ejecuta después de que el contenido se haya guardado exitosamente. Úsalo para efectos secundarios como notificaciones, registro 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 }),
});
}
},
Evento: { content, collection, isNew } — Retorna: Promise<void>
content:beforeDelete
Se ejecuta antes de que se elimine el contenido. Retorna 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;
},
Evento: { id, collection } — Retorna: boolean | void
content:afterDelete
Se ejecuta después de que el contenido se haya eliminado exitosamente.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Evento: { id, collection } — Retorna: Promise<void>
content:afterPublish
Se ejecuta después de que el contenido se promueve de borrador a publicado. Requiere la capacidad content:read.
Evento: { content, collection } — Retorna: Promise<void>
content:afterUnpublish
Se ejecuta después de que el contenido se revierte de publicado a borrador. Requiere la capacidad content:read.
Evento: { content, collection } — Retorna: Promise<void>
Hooks de medios
media:beforeUpload
Se ejecuta antes de que se suba un archivo. Retorna metadatos de archivo modificados o lanza 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}` };
},
Evento: { file: { name, type, size } } — Retorna: archivo modificado o void
media:afterUpload
Se ejecuta después de que un archivo se haya subido exitosamente.
Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Retorna: Promise<void>
Hooks de páginas públicas
Estos permiten que los plugins contribuyan a las páginas públicas renderizadas. Las plantillas optan por incluir los componentes <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribuye metadatos tipados a <head> — etiquetas meta, propiedades OpenGraph, rels de <link> permitidos y JSON-LD. Disponible para plugins tanto en sandbox como nativos. Core valida, desduplicay renderiza las contribuciones; los plugins retornan datos estructurados, nunca HTML crudo.
"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,
},
};
},
Evento:
{
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 };
}
}
Retorna: PageMetadataContribution | PageMetadataContribution[] | null
Tipos de contribución:
| Tipo | 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. El rel de link 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 crudo, scripts o hojas de estilo a puntos de inserción de páginas. Solo plugins nativos.
Los plugins en sandbox 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, usa page:metadata. Ver Plugins nativos: fragmentos de página si necesitas 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 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"— todo el pipeline se detiene y la operación original puede fallar.errorPolicy: "continue"— el error se registra y los hooks restantes continúan ejecutándose.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
Timeouts
Los hooks tienen un tiempo predeterminado de 5000ms. Aumenta el timeout para trabajos más lentos:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Referencia de hooks
| Hook | Disparador | Retorna | 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, o 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 enviar email | Mensaje modificado, false o void | No |
email:deliver | Enviar email vía transporte | void | Sí |
email:afterSend | Después de enviar email | void | No |
comment:beforeCreate | Antes de guardar comentario | Evento modificado, false o void | No |
comment:moderate | Decidir estado del comentario | { status, reason? } | Sí |
comment:afterCreate | Después de guardar 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 |
Ver la Referencia de Hooks para tipos de eventos completos y firmas de manejadores.