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 plugins nativos usan los mismos nombres de hook y tipos de evento, pero usan la canalización de hooks en proceso y además pueden registrar page:fragments. El rechazo de guardado en sandbox y el comportamiento de fallo del runner aislado se describen a continuación.
Firma del hook
Cada manejador de hook toma dos argumentos:
async (event, ctx) => ReturnType;
event— datos sobre lo que acaba de ocurrir (contenido que se guarda, medios subidos, transición de ciclo de vida, etc.)ctx— elPluginContextcon almacenamiento, KV, registro y APIs protegidas por capabilities
Asignar la definición a una constante tipada como SandboxedPlugin infiere event del nombre del hook (el tipo de evento canónico completo) y ctx como PluginContext, de modo que los manejadores no necesitan anotaciones de parámetros. Exporte esa constante como default. Para referenciar un tipo de evento por nombre en un helper, impórtelo desde emdash/plugin.
Configuración del hook
Un hook puede declararse como un manejador simple o envuelto en un objeto de configuración. Prefiera la forma simple a menos que el plugin también admita ejecución deliberada en proceso y necesite los metadatos descritos abajo.
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Opciones de configuración
| Option | Type | Default | Description |
|---|---|---|---|
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. |
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. Obligatoria. |
Capabilities requeridas
Varios hooks exponen datos protegidos o pueden cambiar una operación. EmDash solo los registra cuando el manifiesto declara la capability correspondiente:
| Hooks | Capability | Reason |
|---|---|---|
content:beforeSave | content:write | El hook puede reemplazar el contenido enviado. |
content:beforePublish, content:beforeSchedule, content:beforeUnpublish | hooks.content-policy:register | Los hooks pueden rechazar cambios de estado de publicación. |
Otros hooks content:* | content:read | Sus eventos exponen contenido o identifican una entrada. |
media:beforeUpload | media:write | El hook puede reemplazar metadatos de carga o detener la carga. |
media:afterUpload | media:read | Su evento expone el elemento de medios almacenado. |
email:beforeSend, email:afterSend | hooks.email-events:register | Los hooks inspeccionan eventos del ciclo de vida del correo. |
email:deliver | hooks.email-transport:register | El hook se convierte en un proveedor de transporte de correo. |
Todos los hooks comment:* | users:read | Los eventos de comentario pueden contener información de contacto del autor y metadatos de solicitud. |
page:fragments | hooks.page-fragments:register | El hook inyecta contenido de página de primera parte y es solo nativo. |
Los hooks de ciclo de vida, cron y page:metadata no tienen capability de registro. Declare la capability listada incluso cuando un hook solo lee su evento y no llama a la API ctx correspondiente. La declaración da al operador un aviso de consentimiento preciso, controla la API ctx y es necesaria cuando el plugin se ejecuta en proceso. Capabilities y seguridad explica el efecto en tiempo de ejecución.
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.
Este ejemplo asume que el manifiesto declara una colección de almacenamiento items:
"plugin:install": async (_event, ctx) => {
ctx.log.info("Installing plugin...");
await ctx.settings.set("enabled", true);
await ctx.storage.items.put("default", { name: "Default Item" });
},
Event: {} — Returns: Promise<void>
plugin:activate
Se ejecuta cuando el plugin se habilita (tras la instalación o al volver a habilitarlo).
"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) {
while (true) {
const result = await ctx.storage.items.query({ limit: 100 });
if (result.items.length === 0) break;
await ctx.storage.items.deleteMany(result.items.map((item) => item.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 guardar el contenido. Devuelva contenido modificado, un resultado de error de hook de sandbox o void para dejarlo sin cambios.
Para rechazar un guardado desde el sandbox, devuelva un resultado de hook versionado con un error SAVE_REJECTED. Establezca reason en texto plano de entre 1 y 500 caracteres. EmDash identifica el plugin y muestra el motivo al editor. Los resultados de error vacíos, demasiado largos, mal formados y desconocidos fallan el guardado con un error de hook genérico.
"content:beforeSave": async (event, ctx) => {
const { content } = event;
if (typeof content.title !== "string" || content.title.trim() === "") {
return {
__emdashSandboxHookResult: true,
version: 1,
error: {
code: "SAVE_REJECTED",
reason: "Add a title before saving.",
},
};
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
No ponga HTML en reason. El admin renderiza el valor como texto.
Desde el proceso host, lance ContentSaveRejectedError (exportado desde emdash) en su lugar. La API devuelve SAVE_REJECTED con su mensaje. Cualquier otra excepción de cualquiera de los modos de ejecución falla el guardado con una respuesta genérica CONTENT_HOOK_ERROR.
Event: { content, collection, isNew, id, actor } — Returns: contenido modificado, un resultado de error de hook de sandbox o void. En una actualización, id es el ID del elemento existente y content contiene solo los valores de campo enviados; cargue el elemento almacenado con ctx.content.get(event.collection, event.id). Los guardados autenticados de REST, edición visual y MCP incluyen actor.id y el actor.role numérico. Las escrituras internas sin usuario autenticado omiten actor.
content:afterSave
Se ejecuta después de que el contenido se guarda correctamente. Úselo para efectos secundarios como notificaciones, registro o sincronizaciones externas.
"content:afterSave": async (event, ctx) => {
const contentId = String(event.content.id);
ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${contentId}`, {
actorId: event.actor?.id,
});
if (ctx.http) {
await ctx.http.fetch("https://api.example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "content:save", id: contentId }),
});
}
},
Event: { content, collection, isNew, actor } — Returns: Promise<void>. Los guardados autenticados incluyen la misma instantánea opcional de actor que content:beforeSave.
content:beforeDelete
Se ejecuta antes de eliminar el contenido. Devuelva false para cancelar; true o void lo permiten.
"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, permanent: false } — Returns: boolean | void
Este hook se ejecuta antes de que una entrada se mueva a la papelera. Eliminar permanentemente una entrada de la papelera no vuelve a ejecutar content:beforeDelete.
content:afterDelete
Se ejecuta después de que el contenido se elimina correctamente.
"content:afterDelete": async (event, ctx) => {
await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},
Event: { id, collection, permanent } — Returns: Promise<void>. permanent es false cuando la entrada se movió a la papelera y true cuando se eliminó permanentemente.
Declare hooks.content-policy:register para inspeccionar y rechazar la publicación, la programación o la despublicación sin recibir acceso de lectura, escritura o acciones de publicación de contenido.
Devuelva void para permitir la acción o { cancel: true, reason } para rechazarla. El motivo debe contener de 1 a 500 caracteres de texto plano. Las decisiones inválidas y los errores inesperados abortan por defecto sin exponer la excepción. Los rechazos explícitos devuelven PUBLISH_REJECTED, SCHEDULE_REJECTED o UNPUBLISH_REJECTED.
Los tres eventos contienen { content, collection, origin, actor? }. origin.source es api, mcp, visual-editor, plugin, scheduler o system; los orígenes de plugin también contienen pluginId. Las acciones humanas autenticadas incluyen actor.id, actor.role numérico y el actor.source correspondiente. EmDash acepta el origen visual-editor solo desde el token de acción firmado y de corta duración incrustado en un render de barra de herramientas autenticado; las solicitudes API ordinarias no pueden seleccionar su origen.
Los eventos de publicación y programación exponen el borrador efectivo en content.data y el slug preparado en content.slug. Los eventos de despublicación exponen el contenido actualmente en vivo que la acción eliminaría.
content:beforePublish
El siguiente hook requiere un marcador de aprobación antes de que el contenido pueda pasar a vivo:
"content:beforePublish": async (event) => {
const data = event.content.data;
const approvalStatus =
typeof data === "object" && data !== null && "approval_status" in data
? data.approval_status
: undefined;
if (approvalStatus !== "approved") {
return { cancel: true, reason: "Approve this entry before publishing." };
}
},
Este hook se ejecuta antes de la publicación manual, MCP, de plugin, de sistema y programada. El contenido programado se comprueba de nuevo cuando llega su hora de publicación. Un rechazo del programador desprograma la entrada, almacena el motivo seguro para el público y lista la entrada afectada en el panel en lugar de reintentar el mismo rechazo permanente en cada tick del programador. Una programación, publicación o eliminación exitosa borra el registro. Un administrador puede descartar un registro obsoleto cuando la entrada o el plugin de política ya no están disponibles.
content:beforeSchedule
Se ejecuta antes de que una entrada reciba una hora de publicación. El evento también contiene scheduledAt.
No hay un hook content:beforeUnschedule. Un administrador siempre puede cancelar una publicación futura.
content:beforeUnpublish
Se ejecuta antes de eliminar el contenido en vivo.
content:afterPublish
Se ejecuta después de que el contenido se promociona de borrador a vivo. Requiere la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
Se ejecuta después de que el contenido se revierte de vivo a borrador. Requiere la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterRestore
Se ejecuta después de restaurar contenido de la papelera. Requiere la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterSchedule
Se ejecuta después de programar contenido para publicación futura. Requiere la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnschedule
Se ejecuta después de desprogramar contenido programado. Requiere la capability content:read.
Event: { content, collection } — Returns: Promise<void>
Hooks de medios
media:beforeUpload
Se ejecuta antes de subir un archivo. Devuelva metadatos de archivo modificados o lance 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 sube correctamente.
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 optan por incluir los componentes <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> de emdash/ui.
page:metadata
Aporta metadatos tipados a <head>: meta tags, propiedades OpenGraph, rel de <link> en la lista permitida y JSON-LD. Disponible para plugins en sandbox y 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 };
seo?: {
ogTitle?: string | null;
ogDescription?: string | null;
ogImage?: string | null;
robots?: string | null;
};
articleMeta?: {
publishedTime?: string | null;
modifiedTime?: string | null;
author?: string | null;
};
siteName?: string;
breadcrumbs?: Array<{ name: string; url: string }>;
siteUrl?: string;
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
Contribution kinds:
| Kind | Renders | Dedupe key |
|---|---|---|
meta | <meta name="..." content="..."> | key o name |
property | <meta property="..." content="..."> | key o property |
link | <link rel="<allowed value>" 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. <EmDashHead> compone las contribuciones en el orden plugins → configuración del sitio → metadatos base de la plantilla, de modo que las contribuciones del plugin anulan todo lo que hay debajo. En páginas de contenido, los valores del panel SEO de la entrada se pliegan en el contexto de la página antes de generar los metadatos base: reemplazan los campos de la plantilla (y son lo que su hook ve en el contexto de la página), mientras que las contribuciones del plugin siguen ganando mediante deduplicación first-wins. El rel de enlace está restringido a una lista permitida bloqueada por seguridad (canonical, alternate, author, license, nlweb, site.standard.document); href debe ser HTTP o HTTPS.
page:fragments
Aporta HTML sin procesar, scripts o hojas de estilo a puntos de inserción de la página. 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 en sandbox, use page:metadata. Consulte Plugins nativos: fragmentos de página si necesita esta superficie.
Orden de ejecución de hooks
Cuando un plugin en formato sandbox se ejecuta en proceso, los hooks usan la canalización de hooks compartida:
- Los hooks con valores
prioritymás bajos se ejecutan primero. - Con prioridades iguales, los hooks se ejecutan en el orden de registro del plugin.
- Los hooks con
dependenciesesperan a que esos plugins terminen.
// 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 () => {},
}
Un runner de sandbox aislado invoca los plugins en sandbox activos en orden de carga. Mantenga los hooks independientes: no exija que un plugin en sandbox se ejecute antes que otro.
Manejo de errores
Los fallos de hooks en sandbox dependen de cuándo se ejecuta el hook:
- Un error lanzado en
content:beforeSavefalla el guardado conCONTENT_HOOK_ERROR. Devuelva el sobreSAVE_REJECTEDdocumentado cuando el editor deba ver un motivo de validación específico. - Devolver
falsedesdecontent:beforeDeletedetiene el traslado a la papelera. Si ese hook lanza, EmDash registra el error y continúa la eliminación. - Los after-hooks de contenido se ejecutan después de que la operación tiene éxito. Sus errores se registran y no pueden revertir la operación.
- Los hooks de ciclo de vida, medios, correo y comentarios siguen el contrato de su operación de origen. Use la referencia de hooks para comprobar un valor de retorno específico antes de confiar en el comportamiento de fallo.
Un plugin en proceso puede usar errorPolicy: "abort" o "continue" en la forma de configuración completa. Esa configuración no es un control de recuperación portable para un plugin en sandbox aislado.
Timeouts
La canalización de hooks en proceso tiene un valor predeterminado de 5.000 ms y acepta un timeout más largo en la forma de configuración completa:
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Long-running operation
},
},
Referencia de hooks
| Hook | Trigger | Return | Exclusive |
|---|---|---|---|
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, sobre de rechazo o void | No |
content:afterSave | Después de guardar contenido | void | No |
content:beforeDelete | Antes de mover contenido a la papelera | false para cancelar, si no permitir | No |
content:afterDelete | Después de papelera o eliminación permanente | void | No |
content:afterPublish | Después de publicar contenido | void | No |
content:afterUnpublish | Después de despublicar contenido | void | No |
content:afterRestore | Después de restaurar contenido | void | No |
content:afterSchedule | Después de programar contenido | void | No |
content:afterUnschedule | Después de desprogramar 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 una tarea programada | void | No |
email:beforeSend | Antes de la entrega de correo | Mensaje modificado, false o void | No |
email:deliver | Entregar correo vía transporte | void | Yes |
email:afterSend | Después de la entrega de correo | void | No |
comment:beforeCreate | Antes de almacenar el comentario | Evento modificado, false o void | No |
comment:moderate | Decidir el estado del comentario | { status, reason? } | Yes |
comment:afterCreate | Después de almacenar el comentario | void | No |
comment:afterModerate | El admin cambia el estado del comentario | void | No |
page:metadata | Render de página | Contribuciones o null | No |
page:fragments | Render de página (solo nativo) | Contribuciones o null | No |
Consulte la referencia de hooks para tipos de evento completos y firmas de manejadores.