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— lePluginContextavec 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
| Option | Type | Défaut | Description |
|---|---|---|---|
priority | number | 100 | Ordre d’exécution. Les nombres les plus bas s’exécutent en premier. |
timeout | number | 5000 | Durée d’exécution maximale en millisecondes. |
exclusive | boolean | false | Un seul plugin peut être le fournisseur actif. Utilisé pour email:deliver et comment:moderate. |
handler | function | — | 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 :
| Hooks | Capability | Raison |
|---|---|---|
content:beforeSave | content:write | Le hook peut remplacer le contenu soumis. |
content:beforePublish, content:beforeSchedule, content:beforeUnpublish | hooks.content-policy:register | Les hooks peuvent rejeter les changements d’état de publication. |
Autres hooks content:* | content:read | Leurs événements exposent du contenu ou identifient une entrée. |
media:beforeUpload | media:write | Le hook peut remplacer les métadonnées du téléversement ou l’interrompre. |
media:afterUpload | media:read | Son événement expose l’élément média stocké. |
email:beforeSend, email:afterSend | hooks.email-events:register | Les hooks inspectent les événements du cycle de vie des e-mails. |
email:deliver | hooks.email-transport:register | Le hook devient un fournisseur de transport d’e-mails. |
Tous les hooks comment:* | users:read | Les événements de commentaire peuvent contenir les coordonnées de l’auteur et des métadonnées de requête. |
page:fragments | hooks.page-fragments:register | Le 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 :
| Type | Rend | Clé 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é :
- Les hooks ayant des valeurs de
priorityplus basses s’exécutent en premier. - À priorité égale, les hooks s’exécutent dans l’ordre d’enregistrement des plugins.
- Les hooks avec
dependenciesattendent 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:beforeSavefait échouer la sauvegarde avecCONTENT_HOOK_ERROR. Retournez l’enveloppe documentéeSAVE_REJECTEDlorsque l’éditeur doit voir une raison de validation précise. - Retourner
falsedepuiscontent:beforeDeleteinterrompt 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
| Hook | Déclencheur | Retour | Exclusif |
|---|---|---|---|
plugin:install | Première installation du plugin | void | Non |
plugin:activate | Plugin activé | void | Non |
plugin:deactivate | Plugin désactivé | void | Non |
plugin:uninstall | Plugin supprimé | void | Non |
content:beforeSave | Avant la sauvegarde du contenu | Contenu modifié, enveloppe de rejet ou void | Non |
content:afterSave | Après la sauvegarde du contenu | void | Non |
content:beforeDelete | Avant le déplacement du contenu vers la corbeille | false pour annuler, sinon autoriser | Non |
content:afterDelete | Après le déplacement vers la corbeille ou la suppression définitive | void | Non |
content:afterPublish | Après la publication du contenu | void | Non |
content:afterUnpublish | Après la dépublication du contenu | void | Non |
content:afterRestore | Après la restauration du contenu | void | Non |
content:afterSchedule | Après la planification du contenu | void | Non |
content:afterUnschedule | Après l’annulation de la planification du contenu | void | Non |
media:beforeUpload | Avant le téléversement d’un fichier | Informations de fichier modifiées ou void | Non |
media:afterUpload | Après le téléversement d’un fichier | void | Non |
cron | Une tâche planifiée se déclenche | void | Non |
email:beforeSend | Avant l’envoi de l’e-mail | Message modifié, false ou void | Non |
email:deliver | Envoi de l’e-mail via un transport | void | Oui |
email:afterSend | Après l’envoi de l’e-mail | void | Non |
comment:beforeCreate | Avant l’enregistrement du commentaire | Événement modifié, false ou void | Non |
comment:moderate | Décider du statut du commentaire | { status, reason? } | Oui |
comment:afterCreate | Après l’enregistrement du commentaire | void | Non |
comment:afterModerate | L’administrateur modifie le statut du commentaire | void | Non |
byline:afterSave | Après l’enregistrement de la byline | void | Non |
byline:afterDelete | Après la suppression de la byline | void | Non |
page:metadata | Rendu de la page | Contributions ou null | Non |
page:fragments | Rendu de la page (natif uniquement) | Contributions ou null | Non |
Consultez la Référence des hooks pour les types d’événements complets et les signatures des gestionnaires.