Les hooks permettent aux plugins d’exécuter du code en réponse à des événements. Tous les hooks reçoivent un objet événement et le contexte du plugin, et ils sont déclarés au moment de la définition du plugin — il n’y a pas d’enregistrement dynamique au runtime.
Cette page couvre les plugins sandboxed. Les hooks fonctionnent de manière identique dans les plugins natifs ; les plugins natifs peuvent en plus enregistrer page:fragments.
Signature du hook
Chaque handler de hook prend deux arguments :
async (event, ctx) => ReturnType;
event— données sur ce qui vient de se passer (contenu en cours de sauvegarde, médias uploadés, transition de cycle de vie, etc.)ctx— lePluginContextavec storage, KV, logging et APIs contrôlées par capacités
satisfies SandboxedPlugin sur l’export par défaut infère event à partir du nom du hook (le type d’événement canonique complet) et ctx comme PluginContext, donc les handlers n’ont pas besoin d’annotations de paramètres. Pour référencer un type d’événement par nom dans un helper, importez-le depuis emdash/plugin.
Configuration du hook
Un hook peut être déclaré comme un handler simple ou enveloppé dans un objet de configuration :
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
| Option | Type | Par défaut | Description |
|---|---|---|---|
priority | number | 100 | Ordre d’exécution. Les nombres plus bas s’exécutent en premier. |
timeout | number | 5000 | Temps d’exécution maximum en millisecondes. |
dependencies | string[] | [] | IDs de plugins qui doivent s’exécuter avant ce hook. |
errorPolicy | "abort" | "continue" | "abort" | Si le pipeline doit s’arrêter en cas d’erreur. |
exclusive | boolean | false | Un seul plugin peut être le fournisseur actif. Utilisé pour email:deliver et comment:moderate. |
handler | function | — | La fonction handler du hook. Requise. |
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 fois quand le plugin est ajouté à un site pour la première fois.
"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" });
},
Événement : {} — Retourne : Promise<void>
plugin:activate
S’exécute quand le plugin est activé (après l’installation ou lors de la réactivation).
"plugin:activate": async (_event, ctx) => {
ctx.log.info("Plugin activated");
},
Événement : {} — Retourne : Promise<void>
plugin:deactivate
S’exécute quand 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 quand le plugin est supprimé d’un site.
"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));
}
},
Événement : { deleteData: boolean } — Retourne : Promise<void>
Hooks de contenu
S’exécutent lors des opérations de création, mise à jour et suppression sur le contenu du site.
content:beforeSave
S’exécute avant la sauvegarde du contenu. Retourne le contenu modifié, ou void pour le laisser inchangé. Lancez une exception pour annuler.
"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;
},
Événement : { content, collection, isNew } — Retourne : contenu modifié ou void.
content:afterSave
S’exécute après la sauvegarde réussie du contenu. Utilisez-le pour les effets de bord comme les notifications, le logging ou les synchronisations externes.
"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 }),
});
}
},
Événement : { content, collection, isNew } — Retourne : Promise<void>
content:beforeDelete
S’exécute avant la suppression du contenu. Retourne 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 } — Retourne : boolean | void
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 } — Retourne : Promise<void>
content:afterPublish
S’exécute après la promotion du contenu de brouillon à publié. Nécessite la capacité content:read.
Événement : { content, collection } — Retourne : Promise<void>
content:afterUnpublish
S’exécute après le retour du contenu de publié à brouillon. Nécessite la capacité content:read.
Événement : { content, collection } — Retourne : Promise<void>
content:afterRestore
S’exécute après la restauration du contenu supprimé. Nécessite la capacité content:read.
Événement : { content, collection } — Retourne : Promise<void>
content:afterSchedule
S’exécute après la planification du contenu pour publication future. Nécessite la capacité content:read.
Événement : { content, collection } — Retourne : Promise<void>
content:afterUnschedule
S’exécute après la déprogrammation du contenu planifié. Nécessite la capacité content:read.
Événement : { content, collection } — Retourne : Promise<void>
Hooks de médias
media:beforeUpload
S’exécute avant l’upload d’un fichier. Retourne des 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 l’upload réussi d’un fichier.
Événement : { media: { id, filename, mimeType, size, url, createdAt } } — Retourne : Promise<void>
Hooks de page publique
Ceux-ci permettent aux plugins de contribuer aux pages publiques rendues. Les templates optent pour cela en incluant les composants <EmDashHead>, <EmDashBodyStart> et <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribue des métadonnées typées à <head> — balises meta, propriétés OpenGraph, rels <link> autorisés et JSON-LD. Disponible pour les plugins sandboxed et natifs. Core valide, déduplique 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 };
}
}
Retourne : PageMetadataContribution | PageMetadataContribution[] | null
Types de contribution :
| Type | Rend | Clé 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 gagne pour chaque clé de déduplication. Le rel du lien est restreint à une liste blanche de sécurité (canonical, alternate, author, license, nlweb, site.standard.document) ; href doit être HTTP ou HTTPS.
page:fragments
Contribue du HTML brut, des scripts ou des feuilles de style aux points d’insertion de 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 pour le sandbox, utilisez page:metadata. Voir Plugins natifs : page fragments si vous avez besoin de cette surface.
Ordre d’exécution des hooks
Les hooks s’exécutent dans cet ordre :
- Les hooks avec des valeurs de
priorityplus basses s’exécutent en premier. - Pour des priorités égales, les hooks s’exécutent dans l’ordre d’enregistrement du plugin.
- Les hooks avec
dependenciesattendent que ces plugins se terminent.
// 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"], // attend A même si sa priorité serait normalement plus tard
handler: async () => {},
}
Gestion des erreurs
Quand un hook lance une exception ou expire :
errorPolicy: "abort"— tout le pipeline s’arrête et l’opération d’origine peut échouer.errorPolicy: "continue"— l’erreur est journalisée et les hooks restants continuent de s’exécuter.
"content:afterSave": {
timeout: 5000,
errorPolicy: "continue",
handler: async (event, ctx) => {
await ctx.http!.fetch("https://unreliable-api.com/notify");
},
},
Timeouts
Les hooks ont un timeout par défaut de 5000ms. Augmentez le timeout pour les travaux plus lents :
"content:afterSave": {
timeout: 30000,
handler: async (event, ctx) => {
// Opération longue
},
},
Référence des hooks
| Hook | Déclencheur | Retour | Exclusif |
|---|---|---|---|
plugin:install | Première installation | void | Non |
plugin:activate | Plugin activé | void | Non |
plugin:deactivate | Plugin désactivé | void | Non |
plugin:uninstall | Plugin supprimé | void | Non |
content:beforeSave | Avant sauvegarde | Contenu modifié ou void | Non |
content:afterSave | Après sauvegarde | void | Non |
content:beforeDelete | Avant suppression | false pour annuler, sinon autorisé | Non |
content:afterDelete | Après suppression | void | Non |
content:afterPublish | Après publication | void | Non |
content:afterUnpublish | Après dépublication | void | Non |
content:afterRestore | Après restauration | void | Non |
content:afterSchedule | Après planification | void | Non |
content:afterUnschedule | Après déprogrammation | void | Non |
media:beforeUpload | Avant upload de fichier | Info fichier modifiée ou void | Non |
media:afterUpload | Après upload de fichier | void | Non |
cron | Tâche planifiée déclenchée | void | Non |
email:beforeSend | Avant envoi d’email | Message modifié, false ou void | Non |
email:deliver | Livrer email via transport | void | Oui |
email:afterSend | Après envoi d’email | void | Non |
comment:beforeCreate | Avant stockage commentaire | Événement modifié, false ou void | Non |
comment:moderate | Décider statut commentaire | { status, reason? } | Oui |
comment:afterCreate | Après stockage commentaire | void | Non |
comment:afterModerate | Admin change statut commentaire | void | Non |
page:metadata | Rendu de page | Contributions ou null | Non |
page:fragments | Rendu de page (natif seul) | Contributions ou null | Non |
Voir la Référence des Hooks pour les types d’événements complets et les signatures de handlers.