Referencia de hooks

En esta página

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:

HookDisparadorPuede modificarExclusivo
content:beforeSaveAntes de guardar el contenidoDatos del contenidoNo
content:afterSaveDespués de guardar el contenidoNadaNo
content:beforeDeleteAntes de eliminar el contenidoPuede cancelarNo
content:afterDeleteDespués de eliminar el contenidoNadaNo
content:beforePublishAntes de publicar el contenidoPuede cancelarNo
content:beforeScheduleAntes de programar el contenidoPuede cancelarNo
content:beforeUnpublishAntes de despublicar el contenidoPuede cancelarNo
content:afterPublishDespués de publicar el contenidoNadaNo
content:afterUnpublishDespués de despublicar el contenidoNadaNo
content:afterRestoreDespués de restaurar el contenidoNadaNo
content:afterScheduleDespués de programar el contenidoNadaNo
content:afterUnscheduleDespués de anular la programaciónNadaNo
media:beforeUploadAntes de subir un archivoMetadatos del archivoNo
media:afterUploadDespués de subir un archivoNadaNo
cronSe ejecuta una tarea programadaNadaNo
email:beforeSendAntes de entregar el correoMensaje, puede cancelarNo
email:deliverEntregar correo mediante transporteNadaSí
email:afterSendDespués de una entrega de correo correctaNadaNo
comment:beforeCreateAntes de almacenar el comentarioComentario, puede cancelarNo
comment:moderateDecidir el estado de aprobación del comentarioEstadoSí
comment:afterCreateDespués de almacenar el comentarioNadaNo
comment:afterModerateDespués de que un admin cambie el estado del comentarioNadaNo
page:metadataRenderizado del head de la página públicaAportar etiquetasNo
page:fragmentsRenderizado del body de la página públicaInyectar scriptsNo
plugin:installCuando el plugin se instala por primera vezNadaNo
plugin:activateCuando el plugin se habilitaNadaNo
plugin:deactivateCuando el plugin se deshabilitaNadaNo
plugin:uninstallCuando se elimina el pluginNadaNo

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 void para 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 false para cancelar la eliminación
  • Devuelva true o void para 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 void para 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 false para 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 false para rechazar
  • Devuelva void para 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ónTipoPredeterminadoDescripción
prioritynumber100Orden de ejecución (menor = antes)
timeoutnumber5000Tiempo máximo de ejecución en milisegundos
dependenciesstring[][]IDs de plugins que deben ejecutarse primero
errorPolicystring"abort""continue" para ignorar errores
exclusivebooleanfalseSolo 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:

  1. Ordenados por priority (ascendente)
  2. Los plugins con dependencies se ejecutan después de sus dependencias
  3. 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: ... }