Hooks de plugins

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. Los hooks se declaran al definir el plugin, no se registran dinámicamente en runtime.

Firma del hook

Cada manejador de hook recibe dos argumentos:

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — Datos del evento (contenido que se guarda, medios subidos, etc.)
  • ctx — El contexto del plugin con storage, KV, logging y APIs condicionadas por capabilities

Configuración de hooks

Los hooks pueden declararse como un manejador simple o con configuración completa:

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 menores corren primero.
timeoutnumber5000Tiempo máximo de ejecución en milisegundos.
dependenciesstring[][]IDs de plugin que deben ejecutarse antes que este hook.
errorPolicy"abort" | "continue""abort"Si se detiene la canalización ante un error.
exclusivebooleanfalseSolo un plugin puede ser el proveedor activo. Se usa en email:deliver y comment:moderate.
handlerfunctionLa función manejadora del hook. Obligatoria.

Hooks del ciclo de vida

Los hooks del ciclo de vida se ejecutan durante la instalación, activación y desactivació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...");

  // Seed default data
  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 está habilitado (tras instalarlo o al rehabilitarlo).

"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");
  // Release resources, pause background work
}

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) {
    // User opted to delete plugin data
    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

Los hooks de contenido se ejecutan durante operaciones de crear, actualizar y eliminar.

content:beforeSave

Se ejecuta antes de guardar el contenido. Devuelve contenido modificado o void para dejarlo igual. Lanza un error para cancelar el guardado.

"content:beforeSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  // Validate
  if (collection === "posts" && !content.title) {
    throw new Error("Posts require a title");
  }

  // Transform
  if (content.slug) {
    content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
  }

  return content;
}

Evento:

{
	content: Record<string, unknown>; // Content data being saved
	collection: string; // Collection name
	isNew: boolean; // True if creating, false if updating
}

Devuelve: Promise<Record<string, unknown> | void>

content:afterSave

Se ejecuta tras guardar el contenido correctamente. Úsalo para efectos secundarios como notificaciones, logging o sincronización con sistemas externos.

"content:afterSave": async (event, ctx) => {
  const { content, collection, isNew } = event;

  ctx.log.info(`${isNew ? "Created" : "Updated"} ${collection}/${content.id}`);

  // Trigger external sync
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:save", id: content.id })
    });
  }
}

Evento:

{
	content: Record<string, unknown>; // Saved content (includes id, timestamps)
	collection: string;
	isNew: boolean;
}

Devuelve: Promise<void>

content:beforeDelete

Se ejecuta antes de eliminar contenido. Devuelve false para cancelar la eliminación, true o void para permitirla.

"content:beforeDelete": async (event, ctx) => {
  const { id, collection } = event;

  // Prevent deletion of protected content
  if (collection === "pages" && id === "home") {
    ctx.log.warn("Cannot delete home page");
    return false;
  }

  return true;
}

Evento:

{
	id: string; // Content ID being deleted
	collection: string;
}

Devuelve: Promise<boolean | void>

content:afterDelete

Se ejecuta tras eliminar el contenido correctamente.

"content:afterDelete": async (event, ctx) => {
  const { id, collection } = event;

  ctx.log.info(`Deleted ${collection}/${id}`);

  // Clean up related plugin data
  await ctx.storage.cache!.delete(`${collection}:${id}`);
}

Evento:

{
	id: string;
	collection: string;
}

Devuelve: Promise<void>

content:afterPublish

Se ejecuta tras publicar el contenido (pasar de borrador a en vivo). Úsalo para efectos secundarios como invalidar caché, notificaciones o sincronización con sistemas externos. Requiere la capability read:content.

"content:afterPublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Published ${collection}/${content.id}`);

  // Notify external system
  if (ctx.http) {
    await ctx.http.fetch("https://api.example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ event: "content:publish", id: content.id })
    });
  }
}

Evento:

{
	content: Record<string, unknown>; // Published content (includes id, timestamps)
	collection: string;
}

Devuelve: Promise<void>

content:afterUnpublish

Se ejecuta tras despublicar el contenido (volver de en vivo a borrador). Úsalo para efectos secundarios como invalidar caché o notificar sistemas externos. Requiere la capability read:content.

"content:afterUnpublish": async (event, ctx) => {
  const { content, collection } = event;

  ctx.log.info(`Unpublished ${collection}/${content.id}`);
}

Evento:

{
	content: Record<string, unknown>; // Unpublished content
	collection: string;
}

Devuelve: Promise<void>

Hooks de medios

Los hooks de medios se ejecutan durante la subida de archivos.

media:beforeUpload

Se ejecuta antes de subir un archivo. Devuelve información de archivo modificada o void. Lanza un error para cancelar la subida.

"media:beforeUpload": async (event, ctx) => {
  const { file } = event;

  // Validate file type
  if (!file.type.startsWith("image/")) {
    throw new Error("Only images are allowed");
  }

  // Validate file size (10MB max)
  if (file.size > 10 * 1024 * 1024) {
    throw new Error("File too large");
  }

  // Rename file
  return {
    ...file,
    name: `${Date.now()}-${file.name}`
  };
}

Evento:

{
	file: {
		name: string; // Original filename
		type: string; // MIME type
		size: number; // Size in bytes
	}
}

Devuelve: Promise<{ name: string; type: string; size: number } | void>

media:afterUpload

Se ejecuta tras subir el archivo correctamente.

"media:afterUpload": async (event, ctx) => {
  const { media } = event;

  ctx.log.info(`Uploaded ${media.filename}`, {
    id: media.id,
    size: media.size,
    mimeType: media.mimeType
  });
}

Evento:

{
	media: {
		id: string;
		filename: string;
		mimeType: string;
		size: number | null;
		url: string;
		createdAt: string;
	}
}

Devuelve: Promise<void>

Orden de ejecución de los hooks

Los hooks se ejecutan en este orden:

  1. Los hooks con valores priority más bajos se ejecutan primero
  2. Con la misma prioridad, el orden es el de registro de plugins
  3. Los hooks con dependencies esperan a que esos plugins terminen
// Plugin A
"content:afterSave": {
  priority: 50,  // Runs first
  handler: async () => {}
}

// Plugin B
"content:afterSave": {
  priority: 100,  // Runs second (default priority)
  handler: async () => {}
}

// Plugin C
"content:afterSave": {
  priority: 200,
  dependencies: ["plugin-a"],  // Runs after A, even if priority was lower
  handler: async () => {}
}

Manejo de errores

Cuando un hook lanza error o agota el tiempo:

  • errorPolicy: "abort" — Se detiene toda la canalización. La operación original puede fallar.
  • errorPolicy: "continue" — Se registra el error y los hooks restantes siguen ejecutándose.
"content:afterSave": {
  timeout: 5000,
  errorPolicy: "continue",  // Don't fail the save if this hook fails
  handler: async (event, ctx) => {
    // External API call that might fail
    await ctx.http!.fetch("https://unreliable-api.com/notify");
  }
}

Timeouts

Los hooks tienen un timeout predeterminado de 5000 ms (5 segundos). Auméntalo para operaciones que puedan tardar más:

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

Hooks de página pública

Los hooks de página pública permiten que los plugins contribuyan al <head> y <body> de las páginas renderizadas. Las plantillas se apuntan con <EmDashHead>, <EmDashBodyStart> y <EmDashBodyEnd> desde emdash/ui.

page:metadata

Aporta metadatos tipados al <head>: meta tags, propiedades OpenGraph, enlaces canonical/alternate y datos estructurados JSON-LD. Funciona en modo de confianza y en sandbox.

El núcleo valida, deduplica y renderiza las contribuciones. Los plugins devuelven 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 };
  }
}

Devuelve: PageMetadataContribution | PageMetadataContribution[] | null

Tipos de contribución:

TipoSe renderiza comoClave 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 existe)

La primera contribución gana para cada clave de deduplicación. Los href de enlaces deben ser HTTP o HTTPS.

page:fragments

Aporta HTML crudo, scripts o marcado en puntos de inserción de la página. Solo plugins de confianza — los plugins en sandbox no pueden usar este hook.

"page:fragments": async (event, ctx) => {
  return {
    kind: "external-script",
    placement: "head",
    src: "https://www.googletagmanager.com/gtm.js?id=GTM-XXXXX",
    async: true,
  };
}

Devuelve: PageFragmentContribution | PageFragmentContribution[] | null

Ubicaciones: "head", "body:start", "body:end". Las plantillas que omiten un componente para una ubicación ignoran en silencio las contribuciones dirigidas a ella.

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:afterSaveTras guardar contenidovoidNo
content:beforeDeleteAntes de eliminar contenidofalse para cancelar, si no permitirNo
content:afterDeleteTras eliminar contenidovoidNo
content:afterPublishTras publicar contenidovoidNo
content:afterUnpublishTras despublicar contenidovoidNo
media:beforeUploadAntes de subir archivoInfo de archivo modificada o voidNo
media:afterUploadTras subir archivovoidNo
cronSe dispara la tarea programadavoidNo
email:beforeSendAntes del envío del correoMensaje modificado, false o voidNo
email:deliverEntregar correo vía transportevoid
email:afterSendTras el envío del correovoidNo
comment:beforeCreateAntes de almacenar el comentarioEvento modificado, false o voidNo
comment:moderateDecidir estado del comentario{ status, reason? }
comment:afterCreateTras almacenar el comentariovoidNo
comment:afterModerateEl admin cambia el estado del comentariovoidNo
page:metadataRender de páginaContribuciones o nullNo
page:fragmentsRender de página (de confianza)Contribuciones o nullNo

Consulta la referencia de hooks para tipos de evento completos y firmas de manejadores.