Hooks de plugin

Sur cette page

Les hooks permettent aux plugins d’exécuter du code en réponse à des événements. Tous les hooks reçoivent un objet d’événement et le contexte du plugin. Les hooks sont déclarés à la définition du plugin, pas enregistrés dynamiquement à l’exécution.

Signature du hook

Chaque gestionnaire de hook reçoit deux arguments :

async (event: EventType, ctx: PluginContext) => ReturnType;
  • event — Données sur l’événement (contenu en cours d’enregistrement, média téléversé, etc.)
  • ctx — Le contexte du plugin avec storage, KV, logging et APIs soumises aux capabilities

Configuration des hooks

Les hooks peuvent être déclarés comme un gestionnaire simple ou avec une configuration complète :

Simple

hooks: {
  "content:afterSave": async (event, ctx) => {
    ctx.log.info("Content saved");
  }
}

Configuration complète

hooks: {
  "content:afterSave": {
    priority: 100,
    timeout: 5000,
    dependencies: ["audit-log"],
    errorPolicy: "continue",
    handler: async (event, ctx) => {
      ctx.log.info("Content saved");
    }
  }
}

Options de configuration

OptionTypeDéfautDescription
prioritynumber100Ordre d’exécution. Les valeurs plus faibles passent en premier.
timeoutnumber5000Durée d’exécution maximale en millisecondes.
dependenciesstring[][]IDs de plugins qui doivent s’exécuter avant ce hook.
errorPolicy"abort" | "continue""abort"Arrêter ou non le pipeline en cas d’erreur.
exclusivebooleanfalseUn seul plugin peut être le fournisseur actif. Utilisé pour email:deliver et comment:moderate.
handlerfunctionLa fonction gestionnaire du hook. Obligatoire.

Hooks du cycle de vie

Les hooks du cycle de vie s’exécutent lors de l’installation, l’activation et la désactivation du plugin.

plugin:install

S’exécute une fois lorsque le plugin est ajouté pour la première fois à un site.

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

Événement : {}
Retour : Promise<void>

plugin:activate

S’exécute lorsque le plugin est activé (après installation ou réactivation).

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

Événement : {}
Retour : Promise<void>

plugin:deactivate

S’exécute lorsque le plugin est désactivé (mais pas supprimé).

"plugin:deactivate": async (_event, ctx) => {
  ctx.log.info("Plugin deactivated");
  // Release resources, pause background work
}

Événement : {}
Retour : Promise<void>

plugin:uninstall

S’exécute lorsque le plugin est retiré d’un site.

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

Événement : { deleteData: boolean }
Retour : Promise<void>

Hooks de contenu

Les hooks de contenu s’exécutent lors des opérations de création, mise à jour et suppression.

content:beforeSave

S’exécute avant l’enregistrement du contenu. Retournez le contenu modifié ou void pour le laisser inchangé. Levez une exception pour annuler l’enregistrement.

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

Événement :

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

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

content:afterSave

S’exécute après l’enregistrement réussi du contenu. À utiliser pour les effets de bord : notifications, journalisation, synchronisation avec des systèmes externes.

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

Événement :

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

Retour : Promise<void>

content:beforeDelete

S’exécute avant la suppression du contenu. Retournez false pour annuler la suppression, true ou void pour l’autoriser.

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

Événement :

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

Retour : Promise<boolean | void>

content:afterDelete

S’exécute après la suppression réussie du contenu.

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

Événement :

{
	id: string;
	collection: string;
}

Retour : Promise<void>

content:afterPublish

S’exécute après la publication du contenu (passage du brouillon à la version en ligne). À utiliser pour l’invalidation de cache, les notifications ou la synchronisation avec des systèmes externes. Nécessite 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 })
    });
  }
}

Événement :

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

Retour : Promise<void>

content:afterUnpublish

S’exécute après la dépublication du contenu (retour de la version en ligne au brouillon). À utiliser pour l’invalidation de cache ou la notification de systèmes externes. Nécessite la capability read:content.

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

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

Événement :

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

Retour : Promise<void>

Hooks médias

Les hooks médias s’exécutent lors du téléversement de fichiers.

media:beforeUpload

S’exécute avant le téléversement d’un fichier. Retournez des informations de fichier modifiées ou void. Levez une exception pour annuler le téléversement.

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

Événement :

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

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

media:afterUpload

S’exécute après un téléversement réussi.

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

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

Événement :

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

Retour : Promise<void>

Ordre d’exécution des hooks

Les hooks s’exécutent dans cet ordre :

  1. Les hooks avec des valeurs priority plus basses passent en premier
  2. À priorité égale, l’ordre est celui de l’enregistrement des plugins
  3. Les hooks avec dependencies attendent la fin de ces plugins
// 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 () => {}
}

Gestion des erreurs

Lorsqu’un hook lève une exception ou dépasse le délai :

  • errorPolicy: "abort" — Tout le pipeline s’arrête. L’opération d’origine peut échouer.
  • errorPolicy: "continue" — L’erreur est journalisée et les hooks restants s’exécutent encore.
"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

Les hooks ont un délai par défaut de 5000 ms (5 secondes). Augmentez-le pour les opérations plus longues :

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

Hooks de page publique

Les hooks de page publique permettent aux plugins de contribuer au <head> et au <body> des pages rendues. Les templates s’activent avec <EmDashHead>, <EmDashBodyStart> et <EmDashBodyEnd> depuis emdash/ui.

page:metadata

Contribue des métadonnées typées au <head> — balises meta, propriétés OpenGraph, liens canonical/alternate et données structurées JSON-LD. Fonctionne en mode de confiance et en sandbox.

Le cœur valide, déduplique et rend les contributions. Les plugins renvoient des données structurées, jamais du HTML brut.

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

Événement :

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

Retour : PageMetadataContribution | PageMetadataContribution[] | null

Types de contribution :

TypeRendu sous forme deClé de déduplication
meta<meta name="..." content="...">key ou name
property<meta property="..." content="...">key ou property
link<link rel="canonical|alternate" href="...">canonical : singleton ; alternate : key ou hreflang
jsonld<script type="application/ld+json">id (si présent)

La première contribution l’emporte pour chaque clé de déduplication. Les href des liens doivent être HTTP ou HTTPS.

page:fragments

Contribue du HTML brut, des scripts ou du balisage aux points d’insertion de la page. Plugins de confiance uniquement — les plugins sandboxés ne peuvent pas utiliser ce hook.

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

Retour : PageFragmentContribution | PageFragmentContribution[] | null

Emplacements : "head", "body:start", "body:end". Les templates qui omettent un composant pour un emplacement ignorent silencieusement les contributions ciblées.

Référence des hooks

HookDéclencheurRetourExclusif
plugin:installPremière installation du pluginvoidNon
plugin:activatePlugin activévoidNon
plugin:deactivatePlugin désactivévoidNon
plugin:uninstallPlugin retirévoidNon
content:beforeSaveAvant enregistrement du contenuContenu modifié ou voidNon
content:afterSaveAprès enregistrement du contenuvoidNon
content:beforeDeleteAvant suppression du contenufalse pour annuler, sinon autoriserNon
content:afterDeleteAprès suppression du contenuvoidNon
content:afterPublishAprès publication du contenuvoidNon
content:afterUnpublishAprès dépublication du contenuvoidNon
media:beforeUploadAvant téléversement de fichierInfos fichier modifiées ou voidNon
media:afterUploadAprès téléversement de fichiervoidNon
cronLa tâche planifiée se déclenchevoidNon
email:beforeSendAvant envoi d’e-mailMessage modifié, false ou voidNon
email:deliverDélivrer l’e-mail via le transportvoidOui
email:afterSendAprès envoi d’e-mailvoidNon
comment:beforeCreateAvant stockage du commentaireÉvénement modifié, false ou voidNon
comment:moderateDécider du statut du commentaire{ status, reason? }Oui
comment:afterCreateAprès stockage du commentairevoidNon
comment:afterModerateL’admin change le statut du commentairevoidNon
page:metadataRendu de pageContributions ou nullNon
page:fragmentsRendu de page (confiance)Contributions ou nullNon

Voir la référence des hooks pour les types d’événements complets et les signatures des gestionnaires.