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 sandboxed (formato estándar). Los hooks funcionan de manera idéntica en plugins nativos; la única diferencia es que los plugins nativos también pueden registrar page:fragments, lo que los plugins sandboxed no pueden hacer.

Firma del Hook

Cada manejador de hook toma dos argumentos:

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

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

Full config

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 debe detener la canalización en caso de error.
exclusivebooleanfalseSolo un plugin puede ser el proveedor activo. Se usa para email:deliver y comment:moderate.
handlerfunctionLa función manejadora del hook. Requerido.

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

Event: {}Returns: 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");
},

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) {
		const result = await ctx.storage.items.query({ limit: 1000 });
		await ctx.storage.items.deleteMany(result.items.map((i) => i.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 que se guarde el contenido. Devuelva contenido modificado o void para dejarlo sin cambios. Lance 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;
},

Event: { content, collection, isNew }Returns: contenido modificado o void.

content:afterSave

Se ejecuta después de que el contenido se haya guardado con éxito. Úselo 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 }),
		});
	}
},

Event: { content, collection, isNew }Returns: Promise<void>

content:beforeDelete

Se ejecuta antes de que se elimine el contenido. Devuelva 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;
},

Event: { id, collection }Returns: boolean | void

content:afterDelete

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

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

Event: { id, collection }Returns: Promise<void>

content:afterPublish

Se ejecuta después de que el contenido se promueva de borrador a publicado. Requiere la capacidad content:read.

Event: { content, collection }Returns: Promise<void>

content:afterUnpublish

Se ejecuta después de que el contenido se revierta de publicado a borrador. Requiere la capacidad content:read.

Event: { content, collection }Returns: Promise<void>

Hooks de medios

media:beforeUpload

Se ejecuta antes de que se cargue un archivo. Devuelva metadatos de archivo modificados o lance 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}` };
},

Event: { file: { name, type, size } }Returns: archivo modificado o void

media:afterUpload

Se ejecuta después de que un archivo se haya cargado con éxito.

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 deben aceptar incluyendo los componentes <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> de emdash/ui.

page:metadata

Contribuye metadatos tipados a <head> — meta tags, propiedades OpenGraph, <link> rels permitidos y JSON-LD. Disponible tanto para plugins sandboxed como 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 };
	}
}

Returns: PageMetadataContribution | PageMetadataContribution[] | null

Tipos de contribución:

KindRenderizaClave 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. Link rel 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 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, use page:metadata. Vea Plugins nativos: fragmentos de página si necesita 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"],   // 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" — toda la canalización se detiene y la operación original puede fallar.
  • errorPolicy: "continue" — el error se registra y los hooks restantes aún se ejecutan.
"content:afterSave": {
	timeout: 5000,
	errorPolicy: "continue",
	handler: async (event, ctx) => {
		await ctx.http!.fetch("https://unreliable-api.com/notify");
	},
},

Timeouts

Los hooks tienen un valor predeterminado de 5000ms. Aumente el timeout para trabajos más lentos:

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

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 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 envío de emailMensaje modificado, false o voidNo
email:deliverEntregar email vía transportevoid
email:afterSendDespués de envío de 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 del comentariovoidNo
page:metadataRenderizado de páginaContribuciones o nullNo
page:fragmentsRenderizado de página (solo nativo)Contribuciones o nullNo

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