Hooks

En esta página

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 — el PluginContext con 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ónTipoPor defectoDescripción
prioritynumber100Orden de ejecución. Los números más bajos se ejecutan primero.
timeoutnumber5000Tiempo máximo de ejecución en milisegundos.
dependenciesstring[][]IDs de plugins que deben ejecutarse antes de este hook.
errorPolicy"abort" | "continue""abort"Si detener el pipeline en caso de error.
exclusivebooleanfalseSolo un plugin puede ser el proveedor activo. Usado para email:deliver y comment:moderate.
handlerfunctionLa 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:

TipoRenderizaClave 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:

  1. Los hooks con valores de priority más bajos se ejecutan primero.
  2. Para prioridades iguales, los hooks se ejecutan en el orden de registro del plugin.
  3. Los hooks con dependencies esperan 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

HookDisparadorRetornoExclusivo
plugin:installPrimera instalación del pluginvoidNo
plugin:activatePlugin habilitadovoidNo
plugin:deactivatePlugin deshabilitadovoidNo
plugin:uninstallPlugin eliminadovoidNo
content:beforeSaveAntes de guardar contenidoContenido modificado o voidNo
content:afterSaveDespués de guardar contenidovoidNo
content:beforeDeleteAntes de eliminar contenidofalse para cancelar, sino permitirNo
content:afterDeleteDespués de eliminar contenidovoidNo
content:afterPublishDespués de publicarvoidNo
content:afterUnpublishDespués de despublicarvoidNo
content:afterRestoreDespués de restaurarvoidNo
content:afterScheduleDespués de programarvoidNo
content:afterUnscheduleDespués de desprogramarvoidNo
media:beforeUploadAntes de subir archivoInfo de archivo modificada o voidNo
media:afterUploadDespués de subir archivovoidNo
cronTarea programada disparadavoidNo
email:beforeSendAntes de enviar emailMensaje modificado, false o voidNo
email:deliverEntregar email vía transportevoid
email:afterSendDespués de enviar emailvoidNo
comment:beforeCreateAntes de almacenar comentarioEvento modificado, false o voidNo
comment:moderateDecidir estado del comentario{ status, reason? }
comment:afterCreateDespués de almacenar comentariovoidNo
comment:afterModerateAdmin cambia estado comentariovoidNo
page:metadataRenderizado de páginaContribuciones o nullNo
page:fragmentsRenderizado (solo nativo)Contribuciones o nullNo

Consulta la Referencia de Hooks para tipos de eventos completos y firmas de handlers.