Hooks

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, et ils sont déclarés au moment de la définition du plugin : il n’existe aucun enregistrement dynamique à l’exécution.

Cette page traite des plugins sandboxed. Les plugins natifs utilisent les mêmes noms de hooks et les mêmes types d’événements, mais ils passent par le pipeline de hooks en processus et peuvent en plus enregistrer page:fragments. Le rejet d’une sauvegarde depuis la sandbox et le comportement en cas d’échec des runners isolés sont décrits plus bas.

Signature du hook

Chaque gestionnaire de hook prend deux arguments :

async (event, ctx) => ReturnType;
  • event — des données sur ce qui vient de se produire (contenu en cours de sauvegarde, média téléversé, transition du cycle de vie, etc.)
  • ctx — le PluginContext avec le stockage, le KV, la journalisation et des API protégées par des capabilities

Si vous affectez la définition à une constante typée SandboxedPlugin, event est déduit du nom du hook (le type d’événement canonique complet) et ctx l’est comme PluginContext ; les gestionnaires n’ont donc besoin d’aucune annotation de paramètre. Exportez cette constante par défaut. Pour référencer un type d’événement par son nom dans une fonction utilitaire, importez-le depuis emdash/plugin.

Configuration du hook

Un hook peut être déclaré comme un simple gestionnaire ou enveloppé dans un objet de configuration. Préférez la forme simple, sauf si le plugin prend aussi en charge une exécution délibérée en processus et a besoin des métadonnées décrites ci-dessous.

Simple

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

Configuration complète

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

Options de configuration

OptionTypeDéfautDescription
prioritynumber100Ordre d’exécution. Les nombres les plus bas s’exécutent en premier.
timeoutnumber5000Durée d’exécution maximale en millisecondes.
exclusivebooleanfalseUn seul plugin peut être le fournisseur actif. Utilisé pour email:deliver et comment:moderate.
handlerfunction—La fonction gestionnaire du hook. Obligatoire.

Capabilities requises

Plusieurs hooks exposent des données protégées ou peuvent modifier une opération. EmDash ne les enregistre que si le manifeste déclare la capability correspondante :

HooksCapabilityRaison
content:beforeSavecontent:writeLe hook peut remplacer le contenu soumis.
content:beforePublish, content:beforeSchedule, content:beforeUnpublishhooks.content-policy:registerLes hooks peuvent rejeter les changements d’état de publication.
Autres hooks content:*content:readLeurs événements exposent du contenu ou identifient une entrée.
media:beforeUploadmedia:writeLe hook peut remplacer les métadonnées du téléversement ou l’interrompre.
media:afterUploadmedia:readSon événement expose l’élément média stocké.
email:beforeSend, email:afterSendhooks.email-events:registerLes hooks inspectent les événements du cycle de vie des e-mails.
email:deliverhooks.email-transport:registerLe hook devient un fournisseur de transport d’e-mails.
Tous les hooks comment:*users:readLes événements de commentaire peuvent contenir les coordonnées de l’auteur et des métadonnées de requête.
page:fragmentshooks.page-fragments:registerLe hook injecte du contenu de page first-party et est réservé au natif.

Les hooks de cycle de vie, cron et page:metadata n’ont pas de capability d’enregistrement. Déclarez la capability indiquée même lorsqu’un hook ne fait que lire son événement et n’appelle pas l’API ctx correspondante. La déclaration donne à l’opérateur une invite de consentement exacte, protège l’API ctx et est obligatoire lorsque le plugin s’exécute en processus. Capabilities et sécurité explique l’effet à l’exécution.

Hooks de cycle de vie

S’exécutent pendant l’installation, l’activation, la désactivation et la suppression du plugin.

plugin:install

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

Cet exemple suppose que le manifeste déclare une collection de stockage items :

"plugin:install": async (_event, ctx) => {
	ctx.log.info("Installing plugin...");
	await ctx.settings.set("enabled", true);
	await ctx.storage.items.put("default", { name: "Default Item" });
},

Événement : {} — Retourne : Promise<void>

plugin:activate

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

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

Événement : {} — Retourne : 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");
},

Événement : {} — Retourne : Promise<void>

plugin:uninstall

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

"plugin:uninstall": async (event, ctx) => {
	ctx.log.info("Uninstalling plugin...");
	if (event.deleteData) {
		while (true) {
			const result = await ctx.storage.items.query({ limit: 100 });
			if (result.items.length === 0) break;
			await ctx.storage.items.deleteMany(result.items.map((item) => item.id));
		}
	}
},

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

Hooks de contenu

S’exécutent pendant les opérations de création, de mise à jour et de suppression du contenu du site.

content:beforeSave

S’exécute avant la sauvegarde du contenu. Retournez le contenu modifié, un résultat d’erreur de hook sandbox, ou void pour le laisser inchangé.

Pour rejeter une sauvegarde depuis la sandbox, retournez un résultat de hook versionné contenant une erreur SAVE_REJECTED. Définissez reason sur du texte brut de 1 à 500 caractères. EmDash identifie le plugin et affiche la raison à l’éditeur. Les résultats d’erreur vides, trop longs, mal formés ou inconnus font échouer la sauvegarde avec une erreur de hook générique.

"content:beforeSave": async (event, ctx) => {
	const { content } = event;
	if (typeof content.title !== "string" || content.title.trim() === "") {
		return {
			__emdashSandboxHookResult: true,
			version: 1,
			error: {
				code: "SAVE_REJECTED",
				reason: "Add a title before saving.",
			},
		};
	}

	content.title = content.title.trim();

	return content;
},

Ne mettez pas de HTML dans reason. L’administration affiche la valeur comme du texte.

Depuis le processus hôte, lancez plutôt ContentSaveRejectedError (exportée depuis emdash). L’API retourne SAVE_REJECTED avec votre message. Toute autre exception, dans l’un ou l’autre mode d’exécution, fait échouer la sauvegarde avec une réponse générique CONTENT_HOOK_ERROR.

Événement : { content, collection, isNew, id, actor } — Retourne : contenu modifié, un résultat d’erreur de hook sandbox ou void. Lors d’une mise à jour, id est l’ID de l’élément existant et content ne contient que les valeurs de champs soumises ; chargez l’élément stocké avec ctx.content.get(event.collection, event.id). Le slug de l’entrée ne fait pas partie de content, et une clé slug retournée par le hook échoue à la validation comme champ inconnu. Les sauvegardes authentifiées REST, d’édition visuelle et MCP incluent actor.id et le actor.role numérique. Les écritures internes sans utilisateur authentifié omettent actor.

content:afterSave

S’exécute après la sauvegarde réussie du contenu. À utiliser pour des effets de bord comme les notifications, la journalisation ou les synchronisations externes.

"content:afterSave": async (event, ctx) => {
	const contentId = String(event.content.id);
	ctx.log.info(`${event.isNew ? "Created" : "Updated"} ${event.collection}/${contentId}`, {
		actorId: event.actor?.id,
	});

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

Événement : { content, collection, isNew, actor } — Retourne : Promise<void>. Les sauvegardes authentifiées incluent le même instantané optionnel actor que content:beforeSave.

content:beforeDelete

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

"content:beforeDelete": async (event, ctx) => {
	if (event.collection === "pages" && event.id === "home") {
		ctx.log.warn("Cannot delete home page");
		return false;
	}
	return true;
},

Événement : { id, collection, permanent: false } — Retourne : boolean | void

Ce hook s’exécute avant qu’une entrée soit déplacée vers la corbeille. La suppression définitive d’une entrée depuis la corbeille n’exécute pas de nouveau content:beforeDelete.

content:afterDelete

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

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

Événement : { id, collection, permanent } — Retourne : Promise<void>. permanent vaut false lorsque l’entrée a été déplacée vers la corbeille et true lorsqu’elle a été définitivement supprimée.

Déclarez hooks.content-policy:register pour inspecter et rejeter la publication, la planification ou la dépublication sans recevoir d’accès en lecture, en écriture ni aux actions de publication sur le contenu.

Retournez void pour autoriser l’action ou { cancel: true, reason } pour la rejeter. La raison doit contenir de 1 à 500 caractères de texte brut. Les décisions invalides et les erreurs inattendues interrompent l’opération par défaut, sans exposer l’exception. Les rejets explicites retournent PUBLISH_REJECTED, SCHEDULE_REJECTED ou UNPUBLISH_REJECTED.

Les trois événements contiennent { content, collection, origin, actor? }. origin.source vaut api, mcp, visual-editor, plugin, scheduler ou system ; les origines de plugin contiennent aussi pluginId. Les actions humaines authentifiées incluent actor.id, le actor.role numérique et le actor.source correspondant. EmDash n’accepte l’origine visual-editor qu’à partir du jeton d’action signé et de courte durée intégré dans un rendu authentifié de la barre d’outils ; les requêtes API ordinaires ne peuvent pas choisir leur origine.

Les événements de publication et de planification exposent le brouillon effectif dans content.data et le slug préparé dans content.slug. Les événements de dépublication exposent le contenu actuellement en ligne que l’action retirerait.

content:beforePublish

Le hook suivant exige un marqueur d’approbation avant que le contenu puisse être mis en ligne :

"content:beforePublish": async (event) => {
	const data = event.content.data;
	const approvalStatus =
		typeof data === "object" && data !== null && "approval_status" in data
			? data.approval_status
			: undefined;
	if (approvalStatus !== "approved") {
		return { cancel: true, reason: "Approve this entry before publishing." };
	}
},

Ce hook s’exécute avant la publication manuelle, MCP, par plugin, système et planifiée. Le contenu planifié est vérifié de nouveau lorsque son heure de publication arrive. Le rejet par le planificateur annule la planification de l’entrée, enregistre la raison pouvant être affichée publiquement et liste l’entrée concernée dans le tableau de bord, au lieu de retenter le même rejet permanent à chaque tick du planificateur. Une planification, une publication ou une suppression réussie efface l’enregistrement. Un administrateur peut écarter un enregistrement périmé lorsque l’entrée ou le plugin de politique n’est plus disponible.

content:beforeSchedule

S’exécute avant qu’une entrée reçoive une heure de publication. L’événement contient aussi scheduledAt.

Il n’existe pas de hook content:beforeUnschedule. Un administrateur peut toujours annuler une publication future.

content:beforeUnpublish

S’exécute avant le retrait du contenu en ligne.

content:afterPublish

S’exécute après le passage du contenu de brouillon à en ligne. Nécessite la capability content:read.

Événement : { content, collection } — Retourne : Promise<void>

content:afterUnpublish

S’exécute après le retour du contenu de en ligne à brouillon. Nécessite la capability content:read.

Événement : { content, collection } — Retourne : Promise<void>

content:afterRestore

S’exécute après la restauration d’un contenu placé dans la corbeille. Nécessite la capability content:read.

Événement : { content, collection } — Retourne : Promise<void>

content:afterSchedule

S’exécute après la planification d’un contenu pour une publication future. Nécessite la capability content:read.

Événement : { content, collection } — Retourne : Promise<void>

content:afterUnschedule

S’exécute après l’annulation de la planification d’un contenu planifié. Nécessite la capability content:read.

Événement : { content, collection } — Retourne : Promise<void>

Hooks de médias

media:beforeUpload

S’exécute avant le téléversement d’un fichier. Retournez les métadonnées de fichier modifiées ou lancez une exception pour annuler.

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

Événement : { file: { name, type, size } } — Retourne : fichier modifié ou void

media:afterUpload

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

Événement : { media: { id, filename, mimeType, size, url, createdAt } } — Retourne : Promise<void>

Hooks de pages publiques

Ils permettent aux plugins de contribuer aux pages publiques rendues. Les templates les activent en incluant les composants <EmDashHead>, <EmDashBodyStart> et <EmDashBodyEnd> de emdash/ui.

page:metadata

Apporte des métadonnées typées à <head> : balises meta, propriétés OpenGraph, <link> rels figurant sur une liste d’autorisation et JSON-LD. Disponible pour les plugins sandboxed comme natifs. Le cœur valide, dédoublonne et rend les contributions ; les plugins retournent 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 };
		seo?: {
			ogTitle?: string | null;
			ogDescription?: string | null;
			ogImage?: string | null;
			robots?: string | null;
		};
		articleMeta?: {
			publishedTime?: string | null;
			modifiedTime?: string | null;
			author?: string | null;
		};
		siteName?: string;
		breadcrumbs?: Array<{ name: string; url: string }>;
		siteUrl?: string;
	}
}

Retourne : PageMetadataContribution | PageMetadataContribution[] | null

Types de contribution :

TypeRendClé de dédoublonnage
meta<meta name="..." content="...">key ou name
property<meta property="..." content="...">key ou property
link<link rel="<allowed value>" href="...">canonical : singleton ; alternate : key ou hreflang
jsonld<script type="application/ld+json">id (s’il est présent)

Pour toute clé de dédoublonnage, la première contribution l’emporte. <EmDashHead> compose les contributions dans l’ordre plugins → paramètres du site → métadonnées de base fournies par le template, de sorte que les contributions des plugins l’emportent sur tout ce qui se trouve en dessous. Sur les pages de contenu, les valeurs du panneau SEO de l’entrée sont intégrées au contexte de page avant la génération des métadonnées de base : elles remplacent les champs fournis par le template (et c’est ce que votre hook voit dans le contexte de page), tandis que les contributions des plugins l’emportent toujours grâce au dédoublonnage « le premier gagne ». Le rel d’un lien est limité à une liste d’autorisation verrouillée pour la sécurité (canonical, alternate, author, license, nlweb, site.standard.document) ; href doit être en HTTP ou HTTPS.

page:fragments

Apporte du HTML brut, des scripts ou des feuilles de style aux points d’insertion de la page. Plugins natifs uniquement.

Les plugins sandboxed ne peuvent pas utiliser ce hook, car sa sortie s’exécute comme du code first-party dans le navigateur du visiteur, en dehors de toute frontière de sandbox. Pour des contributions de page sûres en sandbox, utilisez page:metadata. Consultez Plugins natifs : fragments de page si vous avez besoin de cette surface.

Ordre d’exécution des hooks

Lorsqu’un plugin au format sandboxed s’exécute en processus, les hooks utilisent le pipeline de hooks partagé :

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

Un runner de sandbox isolé invoque les plugins sandboxed actifs dans l’ordre de chargement. Gardez les hooks indépendants : n’exigez pas qu’un plugin sandboxed s’exécute avant un autre.

Gestion des erreurs

Les échecs des hooks sandboxed dépendent du moment où le hook s’exécute :

  • Une erreur lancée dans content:beforeSave fait échouer la sauvegarde avec CONTENT_HOOK_ERROR. Retournez l’enveloppe documentée SAVE_REJECTED lorsque l’éditeur doit voir une raison de validation précise.
  • Retourner false depuis content:beforeDelete interrompt le déplacement vers la corbeille. Si ce hook lance une exception, EmDash journalise l’erreur et poursuit la suppression.
  • Les hooks « after » de contenu s’exécutent une fois l’opération réussie. Leurs erreurs sont journalisées et ne peuvent pas annuler l’opération.
  • Les hooks de cycle de vie, de médias, d’e-mail et de commentaire suivent le contrat de l’opération dont ils proviennent. Utilisez la Référence des hooks pour vérifier une valeur de retour précise avant de vous appuyer sur le comportement en cas d’échec.

Un plugin en processus peut utiliser errorPolicy: "abort" ou "continue" dans la forme de configuration complète. Ce paramètre n’est pas un contrôle de reprise portable pour un plugin sandboxed isolé.

Timeouts

Le pipeline de hooks en processus est réglé par défaut sur 5 000 ms et accepte un timeout plus long dans la forme de configuration complète :

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

Référence des hooks

HookDéclencheurRetourExclusif
plugin:installPremière installation du pluginvoidNon
plugin:activatePlugin activévoidNon
plugin:deactivatePlugin désactivévoidNon
plugin:uninstallPlugin supprimévoidNon
content:beforeSaveAvant la sauvegarde du contenuContenu modifié, enveloppe de rejet ou voidNon
content:afterSaveAprès la sauvegarde du contenuvoidNon
content:beforeDeleteAvant le déplacement du contenu vers la corbeillefalse pour annuler, sinon autoriserNon
content:afterDeleteAprès le déplacement vers la corbeille ou la suppression définitivevoidNon
content:afterPublishAprès la publication du contenuvoidNon
content:afterUnpublishAprès la dépublication du contenuvoidNon
content:afterRestoreAprès la restauration du contenuvoidNon
content:afterScheduleAprès la planification du contenuvoidNon
content:afterUnscheduleAprès l’annulation de la planification du contenuvoidNon
media:beforeUploadAvant le téléversement d’un fichierInformations de fichier modifiées ou voidNon
media:afterUploadAprès le téléversement d’un fichiervoidNon
cronUne tâche planifiée se déclenchevoidNon
email:beforeSendAvant l’envoi de l’e-mailMessage modifié, false ou voidNon
email:deliverEnvoi de l’e-mail via un transportvoidOui
email:afterSendAprès l’envoi de l’e-mailvoidNon
comment:beforeCreateAvant l’enregistrement du commentaireÉvénement modifié, false ou voidNon
comment:moderateDécider du statut du commentaire{ status, reason? }Oui
comment:afterCreateAprès l’enregistrement du commentairevoidNon
comment:afterModerateL’administrateur modifie le statut du commentairevoidNon
byline:afterSaveAprès l’enregistrement de la bylinevoidNon
byline:afterDeleteAprès la suppression de la bylinevoidNon
page:metadataRendu de la pageContributions ou nullNon
page:fragmentsRendu de la page (natif uniquement)Contributions ou nullNon

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