Los hooks permiten a los plugins ejecutar 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 definición del plugin — no hay registro dinámico en tiempo de ejecución.
Esta página cubre los plugins sandboxed. Los hooks funcionan de manera idéntica en plugins nativos; los plugins nativos pueden además registrar page:fragments.
Firma del hook
Cada handler de hook toma dos argumentos:
async (event, ctx) => ReturnType;
event— datos sobre lo que acaba de ocurrir (contenido siendo guardado, medios subidos, transición de ciclo de vida, etc.)ctx— elPluginContextcon storage, KV, logging y APIs controladas por capacidades
satisfies SandboxedPlugin en la exportación por defecto infiere event del nombre del hook (el tipo de evento canónico completo) y ctx como PluginContext, por lo que los handlers no necesitan anotaciones de parámetros. Para referenciar un tipo de evento por nombre en un helper, impórtalo de emdash/plugin.
Configuración del hook
Un hook puede declararse como un handler 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 | 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 detener 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 handler 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 añade 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: {} — Devuelve: Promise<void>
plugin:activate
Se ejecuta cuando el plugin se habilita (después de la instalación o cuando se re-habilita).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Evento: {} — Devuelve: 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: {} — Devuelve: 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 } — Devuelve: Promise<void>
Hooks de contenido
Se ejecutan durante operaciones de creación, actualización y eliminación de contenido del sitio.
content:beforeSave
Se ejecuta antes de guardar contenido. Devuelve contenido modificado, o void para dejarlo sin cambios. Lanza una excepción 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 } — Devuelve: contenido modificado o void.
content:afterSave
Se ejecuta después de que el contenido se guarda exitosamente. Úsalo 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 }),
});
}
},
Evento: { content, collection, isNew } — Devuelve: Promise<void>
content:beforeDelete
Se ejecuta antes de eliminar contenido. Devuelve 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 } — Devuelve: boolean | void
content:afterDelete
Se ejecuta después de que el contenido se elimina exitosamente.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Evento: { id, collection } — Devuelve: 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 } — Devuelve: 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 } — Devuelve: Promise<void>
content:afterRestore
Se ejecuta después de que el contenido eliminado se restaura. Requiere la capacidad content:read.
Evento: { content, collection } — Devuelve: Promise<void>
content:afterSchedule
Se ejecuta después de que el contenido se programa para publicación futura. Requiere la capacidad content:read.
Evento: { content, collection } — Devuelve: Promise<void>
content:afterUnschedule
Se ejecuta después de que el contenido programado se desprograma. Requiere la capacidad content:read.
Evento: { content, collection } — Devuelve: Promise<void>
Hooks de medios
media:beforeUpload
Se ejecuta antes de subir un archivo. Devuelve metadatos de archivo modificados o lanza una excepción 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 } } — Devuelve: archivo modificado o void
media:afterUpload
Se ejecuta después de que un archivo se sube exitosamente.
Evento: { media: { id, filename, mimeType, size, url, createdAt } } — Devuelve: Promise<void>
Hooks de página pública
Estos permiten a los plugins contribuir a páginas públicas renderizadas. Los templates optan por esto incluyendo los componentes <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribuye metadatos tipados a <head> — meta tags, propiedades OpenGraph, rels <link> permitidos y JSON-LD. Disponible tanto para plugins sandboxed como nativos. Core 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,
},
};
},
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 };
}
}
Devuelve: 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 cada clave de deduplicación. Link rel está restringido a una lista blanca de 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, usa page:metadata. Consulta Plugins nativos: page fragments 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 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"], // espera a A incluso si su prioridad normalmente sería posterior
handler: async () => {},
}
Manejo de errores
Cuando un hook lanza una excepción o expira:
errorPolicy: "abort"— todo el pipeline se detiene y la operación originadora puede fallar.errorPolicy: "continue"— el error se registra y los hooks restantes siguen 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 timeout por defecto de 5000ms. Aumenta el timeout para trabajos más lentos:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Operación de larga duración
},
},
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 | void | No |
content:afterUnpublish | Después de despublicar | void | No |
content:afterRestore | Después de restaurar | void | No |
content:afterSchedule | Después de programar | void | No |
content:afterUnschedule | Después de desprogramar | 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 | Tarea programada disparada | void | No |
email:beforeSend | Antes de enviar email | Mensaje modificado, false o void | No |
email:deliver | Entregar email vía transporte | void | Sí |
email:afterSend | Después de enviar 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 comentario | void | No |
page:metadata | Renderizado de página | Contribuciones o null | No |
page:fragments | Renderizado (solo nativo) | Contribuciones o null | No |
Consulta la Referencia de Hooks para tipos de eventos completos y firmas de handlers.