Hooks

En esta página

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 hooks funcionan de manera idéntica en plugins nativos; los plugins nativos pueden registrar adicionalmente page:fragments.

Firma del hook

Cada manejador de hook toma dos argumentos:

async (event, ctx) => ReturnType;
  • event — datos sobre lo que acaba de suceder (contenido guardado, medios subidos, transición de ciclo de vida, etc.)
  • ctx — el PluginContext con almacenamiento, KV, registro y APIs controladas por capacidades

satisfies SandboxedPlugin en la exportación predeterminada infiere event del nombre del hook (el tipo de evento canónico completo) y ctx como PluginContext, por lo que los manejadores no necesitan anotaciones de parámetros. Para referenciar un tipo de evento por nombre en un helper, impórtalo desde emdash/plugin.

Configuración de hooks

Un hook puede declararse como un manejador 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ónTipoPredeterminadoDescripció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 o no el pipeline en caso de error.
exclusivebooleanfalseSolo un plugin puede ser el proveedor activo. Usado para email:deliver y comment:moderate.
handlerfunctionLa función manejadora 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 agrega 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: {}Retorna: Promise<void>

plugin:activate

Se ejecuta cuando el plugin se habilita (después de la instalación o cuando se vuelve a habilitar).

"plugin:activate": async (_event, ctx) => {
	ctx.log.info("Plugin activated");
},

Evento: {}Retorna: 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: {}Retorna: 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 }Retorna: 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 que se guarde el contenido. Retorna contenido modificado o void para dejarlo sin cambios. Lanza un error 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 }Retorna: contenido modificado o void.

content:afterSave

Se ejecuta después de que el contenido se haya guardado exitosamente. Úsalo para efectos secundarios como notificaciones, registro 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 }Retorna: Promise<void>

content:beforeDelete

Se ejecuta antes de que se elimine el contenido. Retorna 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 }Retorna: boolean | void

content:afterDelete

Se ejecuta después de que el contenido se haya eliminado exitosamente.

"content:afterDelete": async (event, ctx) => {
	await ctx.storage.cache.delete(`${event.collection}:${event.id}`);
},

Evento: { id, collection }Retorna: 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 }Retorna: 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 }Retorna: Promise<void>

Hooks de medios

media:beforeUpload

Se ejecuta antes de que se suba un archivo. Retorna metadatos de archivo modificados o lanza un error 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 } }Retorna: archivo modificado o void

media:afterUpload

Se ejecuta después de que un archivo se haya subido exitosamente.

Evento: { media: { id, filename, mimeType, size, url, createdAt } }Retorna: 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

Contribuye metadatos tipados a <head> — etiquetas meta, propiedades OpenGraph, rels de <link> permitidos y JSON-LD. Disponible para plugins tanto en sandbox como nativos. Core valida, desduplicay renderiza las contribuciones; los plugins retornan datos estructurados, nunca HTML crudo.

"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 };
	}
}

Retorna: 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 cualquier clave de deduplicación. El rel de link está restringido a una lista de permitidos bloqueada por seguridad (canonical, alternate, author, license, nlweb, site.standard.document); href debe ser HTTP o HTTPS.

page:fragments

Contribuye HTML crudo, scripts o hojas de estilo a puntos de inserción de páginas. 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 para sandbox, usa page:metadata. Ver Plugins nativos: fragmentos de página 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 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"],   // waits for A even if its priority would normally be later
	handler: async () => {},
}

Manejo de errores

Cuando un hook lanza un error o se agota el tiempo:

  • errorPolicy: "abort" — todo el pipeline se detiene y la operación original puede fallar.
  • errorPolicy: "continue" — el error se registra y los hooks restantes continúan 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 tiempo predeterminado de 5000ms. Aumenta el timeout para trabajos más lentos:

"content:afterSave": {
	timeout: 30000,
	handler: async (event, ctx) => {
		// Long-running operation
	},
},

Referencia de hooks

HookDisparadorRetornaExclusivo
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, o permitirNo
content:afterDeleteDespués de eliminar contenidovoidNo
content:afterPublishDespués de publicar contenidovoidNo
content:afterUnpublishDespués de despublicar contenidovoidNo
media:beforeUploadAntes de subir archivoInfo de archivo modificada o voidNo
media:afterUploadDespués de subir archivovoidNo
cronSe dispara tarea programadavoidNo
email:beforeSendAntes de enviar emailMensaje modificado, false o voidNo
email:deliverEnviar email vía transportevoid
email:afterSendDespués de enviar emailvoidNo
comment:beforeCreateAntes de guardar comentarioEvento modificado, false o voidNo
comment:moderateDecidir estado del comentario{ status, reason? }
comment:afterCreateDespués de guardar comentariovoidNo
comment:afterModerateAdmin cambia estado del comentariovoidNo
page:metadataRenderizado de páginaContribuciones o nullNo
page:fragmentsRenderizado de página (solo nativo)Contribuciones o nullNo

Ver la Referencia de Hooks para tipos de eventos completos y firmas de manejadores.