Los hooks permiten a los plugins interceptar y modificar el comportamiento de EmDash en puntos concretos del ciclo de vida del contenido, los medios, el correo, los comentarios y las páginas.
Resumen de hooks
La siguiente tabla enumera cada hook, qué lo dispara, qué puede modificar y si es exclusivo:
| Hook | Disparador | Puede modificar | Exclusivo |
|---|---|---|---|
content:beforeSave | Antes de guardar el contenido | Datos del contenido | No |
content:afterSave | Después de guardar el contenido | Nada | No |
content:beforeDelete | Antes de eliminar el contenido | Puede cancelar | No |
content:afterDelete | Después de eliminar el contenido | Nada | No |
content:beforePublish | Antes de publicar el contenido | Puede cancelar | No |
content:beforeSchedule | Antes de programar el contenido | Puede cancelar | No |
content:beforeUnpublish | Antes de despublicar el contenido | Puede cancelar | No |
content:afterPublish | Después de publicar el contenido | Nada | No |
content:afterUnpublish | Después de despublicar el contenido | Nada | No |
content:afterRestore | Después de restaurar el contenido | Nada | No |
content:afterSchedule | Después de programar el contenido | Nada | No |
content:afterUnschedule | Después de anular la programación | Nada | No |
media:beforeUpload | Antes de subir un archivo | Metadatos del archivo | No |
media:afterUpload | Después de subir un archivo | Nada | No |
cron | Se ejecuta una tarea programada | Nada | No |
email:beforeSend | Antes de entregar el correo | Mensaje, puede cancelar | No |
email:deliver | Entregar correo mediante transporte | Nada | Sí |
email:afterSend | Después de una entrega de correo correcta | Nada | No |
comment:beforeCreate | Antes de almacenar el comentario | Comentario, puede cancelar | No |
comment:moderate | Decidir el estado de aprobación del comentario | Estado | Sí |
comment:afterCreate | Después de almacenar el comentario | Nada | No |
comment:afterModerate | Después de que un admin cambie el estado del comentario | Nada | No |
page:metadata | Renderizado del head de la página pública | Aportar etiquetas | No |
page:fragments | Renderizado del body de la página pública | Inyectar scripts | No |
plugin:install | Cuando el plugin se instala por primera vez | Nada | No |
plugin:activate | Cuando el plugin se habilita | Nada | No |
plugin:deactivate | Cuando el plugin se deshabilita | Nada | No |
plugin:uninstall | Cuando se elimina el plugin | Nada | No |
Hooks de contenido
content:beforeSave
Capability: content:write
Se ejecuta antes de guardar el contenido en la base de datos. Úselo para validar, transformar o enriquecer el contenido. Un hook en sandbox rechaza el guardado devolviendo un resultado de hook versión 1 con error SAVE_REJECTED y un reason en texto plano de 1–500 caracteres. La API responde con SAVE_REJECTED y el panel de administración identifica el plugin y muestra el motivo como texto. Resultados de error vacíos, demasiado largos, mal formados o desconocidos fallan el guardado con una respuesta genérica CONTENT_HOOK_ERROR.
Desde el proceso host, lance ContentSaveRejectedError (exportado desde emdash) para rechazar un guardado. Cualquier otra excepción en cualquiera de los modos de ejecución falla el guardado con una respuesta genérica que no expone el mensaje de la excepción.
import { definePlugin } from "emdash";
export default definePlugin({
id: "my-plugin",
version: "1.0.0",
hooks: {
"content:beforeSave": async (event, ctx) => {
const { content, collection, isNew } = event;
// Add timestamps
if (isNew) {
content.createdBy = "system";
}
content.modifiedAt = new Date().toISOString();
// Return modified content
return content;
},
},
});
Evento
interface ActorInfo {
readonly id: string;
readonly role: number;
}
interface ContentHookEvent {
content: Record<string, unknown>; // Content data
collection: string; // Collection slug
isNew: boolean; // True for creates, false for updates
id?: string; // ID of the existing item on updates; absent on creates
actor?: ActorInfo; // Authenticated user that initiated the save
}
En una actualización, content contiene solo los valores de campo enviados. Cargue el elemento almacenado con ctx.content.get(event.collection, event.id) cuando el hook necesite comparar con él. Los guardados REST autenticados, de edición visual y MCP incluyen actor. Las escrituras internas sin usuario autenticado lo omiten.
Valor de retorno
- Devuelva el objeto de contenido modificado para aplicar cambios
- Devuelva un sobre de error de hook en sandbox para rechazar el guardado con un motivo en texto plano acotado
- Devuelva
voidpara dejar pasar sin cambios
Un hook en sandbox devuelve este sobre completo para rechazar un guardado:
return {
__emdashSandboxHookResult: true,
version: 1,
error: {
code: "SAVE_REJECTED",
reason: "Add a summary before saving.",
},
};
El host recorta reason y acepta entre 1 y 500 caracteres.
content:afterSave
Capability: content:read
Se ejecuta después de guardar el contenido. Úselo para efectos secundarios como notificaciones, invalidación de caché o sincronización externa.
hooks: {
"content:afterSave": async (event, ctx) => {
const { content, collection, isNew } = event;
if (collection === "posts" && content.status === "published") {
// Notify external service
await ctx.http?.fetch("https://api.example.com/notify", {
method: "POST",
body: JSON.stringify({ postId: content.id }),
});
}
},
}
Evento
content:afterSave recibe content, collection, isNew y el actor autenticado opcional. content es la entrada guardada completa, con su ID de base de datos en content.id y los campos de la collection bajo content.data. La id opcional separada usada en actualizaciones de content:beforeSave no está presente después del guardado.
Valor de retorno
No se espera un valor de retorno.
content:beforeDelete
Capability: content:read
Se ejecuta antes de eliminar el contenido. Úselo para validar la eliminación o impedirla.
hooks: {
"content:beforeDelete": async (event, ctx) => {
const { id, collection } = event;
// Prevent deletion of protected content
const item = await ctx.content?.get(collection, id);
if (item?.data.protected) {
return false; // Cancel deletion
}
// Allow deletion
return true;
},
}
Evento
interface ContentDeleteEvent {
id: string; // Entry ID
collection: string; // Collection slug
permanent?: false; // Present for native plugins; omitted in the sandbox runtime
}
content:beforeDelete solo se ejecuta cuando una entrada se mueve a la papelera. Los plugins nativos reciben permanent: false; el runtime en sandbox envía solo id y collection. No ramifique en un hook en sandbox según este campo. La eliminación permanente omite content:beforeDelete, por lo que el hook no puede impedir que un administrador elimine definitivamente una entrada que ya está en la papelera.
Valor de retorno
- Devuelva
falsepara cancelar la eliminación - Devuelva
trueovoidpara permitirla
content:afterDelete
Capability: content:read
Se ejecuta después de eliminar el contenido. Úselo para tareas de limpieza.
hooks: {
"content:afterDelete": async (event, ctx) => {
const { id, collection, permanent } = event;
if (permanent) {
await ctx.storage.relatedItems.delete(`${collection}:${id}`);
}
},
}
El evento contiene id, collection y permanent. permanent es false cuando la entrada va a la papelera y true cuando se elimina de forma permanente. Compruébelo antes de quitar datos que una entrada restaurada de la papelera aún necesitaría. Este hook no tiene valor de retorno.
content:beforePublish
Capability: hooks.content-policy:register
Se ejecuta antes de publicar el contenido. Devuelva void para permitir la publicación o { cancel: true, reason } para rechazarla. EmDash acepta 1–500 caracteres en texto plano en reason y devuelve PUBLISH_REJECTED ante una cancelación explícita.
El evento contiene el content, collection y origin actuales. Las acciones autenticadas de API, MCP y editor visual también incluyen actor con su source correspondiente. El origen visual-editor requiere el token de acción firmado y de corta duración de un render de barra de herramientas autenticado. Las entradas programadas vuelven a ejecutar este hook cuando llega su fecha. Un rechazo del programador anula la programación y lista la entrada afectada y el motivo en el panel. Una programación, publicación o eliminación correcta borra el registro; un administrador puede descartar un registro obsoleto.
content:beforeSchedule
Capability: hooks.content-policy:register
Se ejecuta antes de que una entrada reciba una hora de publicación. Usa el mismo formato de decisión y campos de origen que content:beforePublish, añade scheduledAt al evento y devuelve SCHEDULE_REJECTED ante una cancelación explícita.
No existe el hook content:beforeUnschedule. Un administrador siempre puede cancelar una publicación futura.
content:beforeUnpublish
Capability: hooks.content-policy:register
Se ejecuta antes de quitar contenido publicado en vivo. Usa el mismo formato de decisión y campos de origen que content:beforePublish y devuelve UNPUBLISH_REJECTED ante una cancelación explícita.
hooks.content-policy:register no implica content:read, content:write ni acciones de publicación. Decisiones no válidas y errores inesperados de política de abort fallan de forma cerrada sin exponer mensajes de excepción.
content:afterPublish
Se ejecuta después de que una entrada se publica correctamente, incluida una que EmDash publica automáticamente a la hora programada. Úselo para trabajo que depende de la entrada publicada, como notificar a otro servicio o actualizar un índice de búsqueda externo.
hooks: {
"content:afterPublish": async (event, ctx) => {
ctx.log.info(`Published ${event.collection}/${event.content.id}`);
},
}
El hook requiere la capability content:read. EmDash lo ejecuta después de la respuesta de publicación, por lo que su valor de retorno no puede cambiar la entrada ni deshacer la publicación. Se registra un error; con errorPolicy: "abort", los hooks de publicación posteriores no se ejecutan.
content:afterUnpublish
Se ejecuta después de despublicar correctamente una entrada. Úselo para quitar o actualizar copias de contenido en sistemas externos. Despublicar también cancela programaciones pendientes sin ejecutar content:afterUnschedule.
hooks: {
"content:afterUnpublish": async (event, ctx) => {
ctx.log.info(`Unpublished ${event.collection}/${event.content.id}`);
},
}
Este hook tiene el mismo requisito de capability content:read, ejecución diferida, forma de evento y contrato sin valor de retorno que content:afterPublish.
content:afterRestore
Se ejecuta después de restaurar contenido de la papelera. Requiere la capability content:read. La entrada restaurada es un borrador sin programación, cualquiera que fuera su estado al moverla a la papelera. Quitar la programación no ejecuta además content:afterUnschedule.
hooks: {
"content:afterRestore": async (event, ctx) => {
ctx.log.info(`Restored ${event.collection}/${event.content.id}`);
},
}
content:afterSchedule
Se ejecuta después de programar contenido para publicación futura. Requiere la capability content:read.
hooks: {
"content:afterSchedule": async (event, ctx) => {
ctx.log.info(`Scheduled ${event.collection}/${event.content.id}`);
},
}
content:afterUnschedule
Se ejecuta después de anular la programación del contenido. Requiere la capability content:read.
hooks: {
"content:afterUnschedule": async (event, ctx) => {
ctx.log.info(`Unscheduled ${event.collection}/${event.content.id}`);
},
}
Evento
interface ContentStateChangeEvent {
content: Record<string, unknown>;
collection: string;
}
Esta forma de evento la comparten content:afterPublish, content:afterUnpublish, content:afterRestore, content:afterSchedule y content:afterUnschedule. content es la entrada completa tras el cambio de estado, incluidos id, slug y estado; los campos de la collection están en content.data.
Valor de retorno
No se espera un valor de retorno.
Hooks de medios
media:beforeUpload
Capability: media:write
Se ejecuta antes de subir un archivo. Úselo para validar, renombrar o rechazar archivos.
hooks: {
"media:beforeUpload": async (event, ctx) => {
const { file } = event;
// Reject files over 10MB
if (file.size > 10 * 1024 * 1024) {
throw new Error("File too large");
}
// Rename file
return {
name: `${Date.now()}-${file.name}`,
type: file.type,
size: file.size,
};
},
}
Evento
interface MediaUploadEvent {
file: {
name: string; // Original filename
type: string; // MIME type
size: number; // Size in bytes
};
}
Valor de retorno
- Devuelva metadatos de archivo modificados para aplicar cambios
- Devuelva
voidpara dejar pasar sin cambios - Lance una excepción para rechazar la subida
media:afterUpload
Capability: media:read
Se ejecuta después de subir un archivo. Úselo para procesamiento, miniaturas o extracción de metadatos.
hooks: {
"media:afterUpload": async (event, ctx) => {
const { media } = event;
if (media.mimeType.startsWith("image/")) {
// Store image metadata
await ctx.kv.set(`media:${media.id}:analyzed`, {
processedAt: new Date().toISOString(),
});
}
},
}
Evento
interface MediaAfterUploadEvent {
media: {
id: string;
filename: string;
mimeType: string;
size: number | null;
url: string;
createdAt: string;
};
}
Valor de retorno
No se espera un valor de retorno.
Hooks del ciclo de vida
Los hooks del ciclo de vida no requieren capability de registro.
plugin:install
Se ejecuta cuando un plugin se instala por primera vez. Úselo para configuración inicial, crear collections de almacenamiento o datos semilla.
hooks: {
"plugin:install": async (event, ctx) => {
// Initialize default settings
await ctx.settings.set("enabled", true);
await ctx.settings.set("threshold", 100);
ctx.log.info("Plugin installed successfully");
},
}
plugin:activate
Se ejecuta cuando un plugin se habilita (tras la instalación o una reactivación).
hooks: {
"plugin:activate": async (event, ctx) => {
ctx.log.info("Plugin activated");
},
}
plugin:deactivate
Se ejecuta cuando un plugin se deshabilita.
hooks: {
"plugin:deactivate": async (event, ctx) => {
ctx.log.info("Plugin deactivated");
},
}
plugin:install, plugin:activate y plugin:deactivate reciben un objeto de evento vacío. No tienen valor de retorno.
plugin:uninstall
Se ejecuta cuando se elimina un plugin. Úselo para limpieza.
hooks: {
"plugin:uninstall": async (event, ctx) => {
const { deleteData } = event;
if (deleteData) {
// Clean up all plugin data
const items = await ctx.settings.list();
for (const { key } of items) {
await ctx.kv.delete(key);
}
}
ctx.log.info("Plugin uninstalled");
},
}
Evento
interface UninstallEvent {
deleteData: boolean; // User chose to delete data
}
El hook de desinstalación no tiene valor de retorno.
Hook cron
cron
Capability: Ninguna requerida
Se dispara cuando se ejecuta una tarea programada. Programe tareas con ctx.cron.schedule(). Las expresiones cron se evalúan en UTC en cada host.
hooks: {
"cron": async (event, ctx) => {
if (event.name === "daily-sync") {
const data = await ctx.http?.fetch("https://api.example.com/data");
ctx.log.info("Sync complete");
}
},
}
Evento
interface CronEvent {
name: string;
data?: Record<string, unknown>;
scheduledAt: string;
}
El hook cron no tiene valor de retorno.
Hooks de correo
En mensajes enviados por plugins, los hooks de correo se ejecutan en este orden: email:beforeSend, luego email:deliver, luego email:afterSend. Los mensajes de autenticación del sistema van directamente a email:deliver; no pasan por email:beforeSend ni email:afterSend.
email:beforeSend
Capability: hooks.email-events:register
Hook middleware antes de la entrega. Transforme mensajes o cancele la entrega.
hooks: {
"email:beforeSend": async (event, ctx) => {
// Add footer to all emails
return {
...event.message,
text: event.message.text + "\n\n—Sent from My Site",
};
// Or return false to cancel delivery
},
}
Evento
interface EmailBeforeSendEvent {
message: {
to: string;
cc?: string[];
replyTo?: string;
subject: string;
text: string;
html?: string;
};
source: string;
}
Valor de retorno
- Devuelva el mensaje modificado para transformarlo
- Devuelva
falsepara cancelar la entrega
email:deliver
Capability: hooks.email-transport:register | Exclusivo: Sí
El proveedor de transporte. Solo un plugin puede entregar correos. Responsable de enviar el mensaje mediante un servicio de correo.
hooks: {
"email:deliver": {
exclusive: true,
handler: async (event, ctx) => {
await sendViaSES(event.message);
},
},
}
Evento y valor de retorno
interface EmailDeliverEvent {
message: {
to: string;
cc?: string[];
replyTo?: string;
subject: string;
text: string;
html?: string;
};
source: string;
}
El hook no tiene valor de retorno. source es "system" para mensajes de autenticación de EmDash y el ID del plugin para mensajes enviados por un plugin.
Los plugins pueden establecer cc (destinatarios adicionales) y replyTo en un mensaje. Los proveedores deben entregar ambos cuando estén presentes; un proveedor que omita cc en silencio deja a esos destinatarios sin el mensaje.
email:afterSend
Capability: hooks.email-events:register
Hook fire-and-forget tras una entrega correcta. Los errores se registran pero no se propagan.
hooks: {
"email:afterSend": async (event, ctx) => {
await ctx.kv.set(`email:log:${Date.now()}`, {
to: event.message.to,
subject: event.message.subject,
});
},
}
Evento y valor de retorno
email:afterSend recibe los mismos campos message y source que email:deliver. No tiene valor de retorno.
Hooks de comentarios
Los hooks de comentarios se ejecutan en este orden: comment:beforeCreate, luego comment:moderate, luego comment:afterCreate. El hook comment:afterModerate se ejecuta por separado cuando un administrador o un plugin autorizado cambia el estado de un comentario.
Tras almacenar un comentario, los hooks reciben esta forma de registro:
interface StoredComment {
id: string;
collection: string;
contentId: string;
parentId: string | null;
authorName: string;
authorEmail: string;
authorUserId: string | null;
body: string;
status: string;
moderationMetadata: Record<string, unknown> | null;
createdAt: string;
updatedAt: string;
}
comment:beforeCreate
Capability: users:read
Hook middleware antes de almacenar un comentario. Enriquezca, valide o rechace comentarios.
hooks: {
"comment:beforeCreate": async (event, ctx) => {
// Reject comments with links
if (event.comment.body.includes("http")) {
return false;
}
},
}
Evento
interface CommentBeforeCreateEvent {
comment: {
collection: string;
contentId: string;
parentId: string | null;
authorName: string;
authorEmail: string;
authorUserId: string | null;
body: string;
ipHash: string | null;
userAgent: string | null;
};
metadata: Record<string, unknown>;
}
Valor de retorno
- Devuelva el evento modificado para transformarlo
- Devuelva
falsepara rechazar - Devuelva
voidpara dejar pasar
comment:moderate
Capability: users:read | Exclusivo: Sí
Decide si un comentario está aprobado, pendiente o es spam. Solo un proveedor de moderación está activo.
hooks: {
"comment:moderate": {
exclusive: true,
handler: async (event, ctx) => {
const score = await checkSpam(event.comment);
return {
status: score > 0.8 ? "spam" : score > 0.5 ? "pending" : "approved",
reason: `Spam score: ${score}`,
};
},
},
}
Evento
interface CommentModerateEvent {
comment: { /* same as beforeCreate */ };
metadata: Record<string, unknown>;
collectionSettings: {
commentsEnabled: boolean;
commentsModeration: "all" | "first_time" | "none";
commentsClosedAfterDays: number;
commentsAutoApproveUsers: boolean;
};
priorApprovedCount: number;
}
Valor de retorno
{ status: "approved" | "pending" | "spam"; reason?: string }
Moderador activo
EmDash incluye un moderador integrado que aplica la configuración de comentarios de la collection. Cuando exactamente un plugin proporciona comment:moderate, EmDash selecciona ese plugin en lugar del moderador integrado y guarda la elección. Cuando varios plugins lo proporcionan y no hay elección guardada, EmDash no selecciona ninguno y los comentarios nuevos esperan revisión. Una elección guardada, incluida la del moderador integrado, permanece mientras el moderador seleccionado esté habilitado.
comment:afterCreate
Capability: users:read
Hook fire-and-forget tras almacenar un comentario. Úselo para notificaciones. Enviar correo también requiere la capability email:send y un proveedor email:deliver configurado; sin ambos, ctx.email es undefined.
hooks: {
"comment:afterCreate": async (event, ctx) => {
const recipient = event.contentAuthor?.email;
if (event.comment.status === "approved" && recipient && ctx.email) {
await ctx.email.send({
to: recipient,
subject: `New comment on "${event.content.title}"`,
text: `${event.comment.authorName} commented: ${event.comment.body}`,
});
}
},
}
Evento y valor de retorno
interface CommentAfterCreateEvent {
comment: StoredComment;
metadata: Record<string, unknown>;
content: { id: string; collection: string; slug: string; title?: string };
contentAuthor?: { id: string; name: string | null; email: string };
}
El hook no tiene valor de retorno.
comment:afterModerate
Capability: users:read
Se ejecuta después de que un administrador o un plugin cambie el estado de un comentario. Una transición correcta ejecuta el hook una vez. Los errores del hook se registran y no deshacen el cambio de estado.
Evento
interface CommentAfterModerateEvent {
comment: StoredComment;
previousStatus: string;
newStatus: string;
moderator: { id: string; name: string | null };
origin?:
| { source: "admin"; userId: string }
| { source: "plugin"; pluginId: string };
}
El hook no tiene valor de retorno.
Hooks de página
Los hooks de página se ejecutan al renderizar páginas públicas. Permiten a los plugins inyectar metadatos y scripts.
Ambos hooks de página reciben el contexto actual de la página pública:
interface PublicPageContext {
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;
}
interface PageMetadataEvent { page: PublicPageContext }
interface PageFragmentEvent { page: PublicPageContext }
page:metadata
Capability: Ninguna requerida
Aporte meta tags, propiedades Open Graph, datos estructurados JSON-LD o link tags al head de la página.
hooks: {
"page:metadata": async (event, ctx) => {
return [
{ kind: "meta", name: "generator", content: "EmDash" },
{ kind: "property", property: "og:site_name", content: event.page.siteName ?? "My Site" },
{ kind: "jsonld", graph: { "@type": "WebSite", name: event.page.siteName } },
];
},
}
Tipos de contribución
type PageMetadataContribution =
| { kind: "meta"; name: string; content: string; key?: string }
| { kind: "property"; property: string; content: string; key?: string }
| {
kind: "link";
rel: "canonical" | "alternate" | "author" | "license" | "nlweb" | "site.standard.document";
href: string;
hreflang?: string;
key?: string;
}
| {
kind: "jsonld";
id?: string;
graph: Record<string, unknown> | Array<Record<string, unknown>>;
};
El campo key deduplica contribuciones: solo se usa la última contribución con una clave dada.
Devuelva una contribución, un array de contribuciones o null cuando el plugin no deba añadir nada.
page:fragments
Capability: hooks.page-fragments:register
Inserte scripts o HTML en páginas. Solo disponible para plugins nativos.
hooks: {
"page:fragments": async (event, ctx) => {
return [
{
kind: "external-script",
placement: "body:end",
src: "https://analytics.example.com/script.js",
async: true,
},
{
kind: "inline-script",
placement: "head",
code: `window.siteId = "abc123";`,
},
];
},
}
Tipos de contribución
type PageFragmentContribution =
| {
kind: "external-script";
placement: "head" | "body:start" | "body:end";
src: string;
async?: boolean;
defer?: boolean;
attributes?: Record<string, string>;
key?: string;
}
| {
kind: "inline-script";
placement: "head" | "body:start" | "body:end";
code: string;
attributes?: Record<string, string>;
key?: string;
}
| {
kind: "html";
placement: "head" | "body:start" | "body:end";
html: string;
key?: string;
};
Devuelva una contribución de fragmento, un array de contribuciones o null cuando el plugin no deba añadir nada.
Configuración de hooks
Los hooks aceptan una función handler o un objeto de configuración:
hooks: {
// Simple handler
"content:afterSave": async (event, ctx) => { ... },
// With configuration
"content:beforeSave": {
priority: 50, // Lower runs first (default: 100)
timeout: 10000, // Max execution time in ms (default: 5000)
dependencies: [], // Run after these plugins
errorPolicy: "abort", // "continue" or "abort" (default)
handler: async (event, ctx) => { ... },
},
}
Opciones de configuración
| Opción | Tipo | Predeterminado | Descripción |
|---|---|---|---|
priority | number | 100 | Orden de ejecución (menor = antes) |
timeout | number | 5000 | Tiempo máximo de ejecución en milisegundos |
dependencies | string[] | [] | IDs de plugins que deben ejecutarse primero |
errorPolicy | string | "abort" | "continue" para ignorar errores |
exclusive | boolean | false | Solo un plugin puede ser el proveedor activo (hooks con patrón provider como email:deliver, comment:moderate) |
Contexto del plugin
Todos los hooks reciben un objeto de contexto con acceso a las API del plugin:
interface PluginContext {
plugin: { id: string; version: string };
storage: PluginStorage;
kv: KVAccess;
content?: ContentAccess;
media?: MediaAccess;
http?: HttpAccess;
log: LogAccess;
site: { name: string; url: string; locale: string };
url(path: string): string;
users?: UserAccess;
cron?: CronAccess;
email?: EmailAccess;
}
Consulte Capacidades de plugins para la capability requerida por cada API de contexto.
Manejo de errores
Los errores en hooks se registran y se tratan según errorPolicy:
"abort"(predeterminado): detener la ejecución y revertir la transacción si aplica"continue": registrar el error y continuar con el siguiente hook
hooks: {
"content:beforeSave": {
errorPolicy: "continue", // Don't block save if this fails
handler: async (event, ctx) => {
try {
await ctx.http?.fetch("https://api.example.com/validate");
} catch (error) {
ctx.log.warn("Validation service unavailable", error);
}
},
},
}
Orden de ejecución
Los hooks se ejecutan en este orden:
- Ordenados por
priority(ascendente) - Los plugins con
dependenciesse ejecutan después de sus dependencias - Con la misma prioridad, el orden es determinista pero no especificado
// This runs first (priority 10)
{ priority: 10, handler: ... }
// This runs second (priority 50)
{ priority: 50, handler: ... }
// This runs last (default priority 100)
{ handler: ... }