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 sont déclarés au moment de la définition du plugin — il n’y a pas d’enregistrement dynamique à l’exécution.
Cette page couvre les plugins sandboxés. Les plugins natifs utilisent les mêmes noms de hooks et types d’événements, mais ils utilisent le pipeline de hooks in-process et peuvent en plus enregistrer page:fragments. Le rejet de sauvegarde sandboxé et le comportement d’échec du runner isolé sont décrits ci-dessous.
Signature du hook
Chaque gestionnaire de hook prend deux arguments :
async (event, ctx) => ReturnType;
event— données sur ce qui vient de se produire (contenu en cours d’enregistrement, média téléversé, transition de cycle de vie, etc.)ctx— lePluginContextavec stockage, KV, journalisation et API protégées par capabilities
Assigner la définition à une constante typée SandboxedPlugin infère event à partir du nom du hook (le type d’événement canonique complet) et ctx comme PluginContext, de sorte que les gestionnaires n’ont pas besoin d’annotations de paramètres. Exportez cette constante en default. 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 gestionnaire simple ou encapsulé dans un objet de configuration. Préférez la forme simple sauf si le plugin prend aussi en charge une exécution in-process délibérée et a besoin des métadonnées décrites ci-dessous.
Simple
hooks: {
"content:afterSave": async (event, ctx) => {
ctx.log.info("Content saved");
},
}, Full config
hooks: {
"content:afterSave": {
priority: 100,
timeout: 5000,
handler: async (event, ctx) => {
ctx.log.info("Content saved");
},
},
}, Options de configuration
| Option | Type | Default | Description |
|---|---|---|---|
priority | number | 100 | Ordre d’exécution. Les nombres plus bas s’exécutent en premier. |
timeout | number | 5000 | Temps d’exécution maximal 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 lorsque le manifeste déclare la capability correspondante :
| Hooks | Capability | Reason |
|---|---|---|
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 de téléversement ou arrêter le téléversement. |
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 de l’e-mail. |
email:deliver | hooks.email-transport:register | Le hook devient un fournisseur de transport d’e-mail. |
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é aux plugins natifs. |
Les hooks de cycle de vie, cron et page:metadata n’ont pas de capability d’enregistrement. Déclarez la capability listée même lorsqu’un hook lit seulement son événement et n’appelle pas l’API ctx correspondante. La déclaration donne à l’opérateur une invite de consentement précise, contrôle l’API ctx et est requise lorsque le plugin s’exécute in-process. 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 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" });
},
Event: {} — Returns: 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");
},
Event: {} — Returns: 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");
},
Event: {} — Returns: 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) {
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));
}
}
},
Event: { deleteData: boolean } — Returns: 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 l’enregistrement du contenu. Renvoyez le contenu modifié, un résultat d’erreur de hook sandbox ou void pour le laisser inchangé.
Pour rejeter une sauvegarde depuis le sandbox, renvoyez un résultat de hook versionné avec une erreur SAVE_REJECTED. Définissez reason en texte brut entre 1 et 500 caractères. EmDash identifie le plugin et montre la raison à l’éditeur. Les résultats d’erreur vides, trop longs, mal formés et 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.",
},
};
}
if (typeof content.slug === "string") {
content.slug = content.slug.toLowerCase().replace(/\s+/g, "-");
}
return content;
},
Ne mettez pas de HTML dans reason. L’admin rend la valeur sous forme de texte.
Depuis le processus hôte, lancez plutôt ContentSaveRejectedError (exporté depuis emdash). L’API renvoie SAVE_REJECTED avec votre message. Toute autre exception provenant de l’un ou l’autre mode d’exécution fait échouer la sauvegarde avec une réponse générique CONTENT_HOOK_ERROR.
Event: { content, collection, isNew, id, actor } — Returns: 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). 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 l’enregistrement réussi du contenu. Utilisez-le 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 }),
});
}
},
Event: { content, collection, isNew, actor } — Returns: 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. Renvoyez 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;
},
Event: { id, collection, permanent: false } — Returns: boolean | void
Ce hook s’exécute avant qu’une entrée soit déplacée vers la corbeille. La suppression permanente d’une entrée depuis la corbeille ne réexécute pas 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}`);
},
Event: { id, collection, permanent } — Returns: 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, écriture ou action de publication sur le contenu.
Renvoyez void pour autoriser l’action ou { cancel: true, reason } pour la rejeter. La raison doit contenir 1 à 500 caractères de texte brut. Les décisions invalides et les erreurs inattendues abandonnent par défaut sans exposer l’exception. Les rejets explicites renvoient 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 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 que depuis le jeton d’action signé et de courte durée intégré dans un rendu de barre d’outils authentifié ; les requêtes API ordinaires ne peuvent pas sélectionner 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 supprimerait.
content:beforePublish
Le hook suivant exige un marqueur d’approbation avant que le contenu puisse passer 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, plugin, système et planifiée. Le contenu planifié est vérifié à nouveau lorsque son heure de publication arrive. Un rejet du planificateur annule la planification, stocke la raison sûre pour le public et liste l’entrée concernée sur le tableau de bord au lieu de réessayer le même rejet permanent à chaque tick du planificateur. Une planification, publication ou suppression réussie efface l’enregistrement. Un administrateur peut ignorer un enregistrement obsolète 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’y a pas de hook content:beforeUnschedule. Un administrateur peut toujours annuler une publication future.
content:beforeUnpublish
S’exécute avant la suppression du contenu en ligne.
content:afterPublish
S’exécute après la promotion du contenu de brouillon à en ligne. Nécessite la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnpublish
S’exécute après le retour du contenu d’en ligne à brouillon. Nécessite la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterRestore
S’exécute après la restauration de contenu depuis la corbeille. Nécessite la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterSchedule
S’exécute après la planification du contenu pour une publication future. Nécessite la capability content:read.
Event: { content, collection } — Returns: Promise<void>
content:afterUnschedule
S’exécute après l’annulation de la planification du contenu. Nécessite la capability content:read.
Event: { content, collection } — Returns: Promise<void>
Hooks de médias
media:beforeUpload
S’exécute avant le téléversement d’un fichier. Renvoyez 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}` };
},
Event: { file: { name, type, size } } — Returns: fichier modifié ou void
media:afterUpload
S’exécute après le téléversement réussi d’un fichier.
Event: { media: { id, filename, mimeType, size, url, createdAt } } — Returns: Promise<void>
Hooks de pages publiques
Ceux-ci permettent aux plugins de contribuer aux pages publiques rendues. Les templates s’inscrivent en incluant les composants <EmDashHead>, <EmDashBodyStart> et <EmDashBodyEnd> de emdash/ui.
page:metadata
Contribute des métadonnées typées à <head> — balises meta, propriétés OpenGraph, rel <link> autorisés et JSON-LD. Disponible pour les plugins sandboxés et natifs. Le noyau 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,
},
};
},
Event:
{
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;
}
}
Returns: PageMetadataContribution | PageMetadataContribution[] | null
Contribution kinds:
| Kind | Renders | Dedupe key |
|---|---|---|
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) |
La première contribution gagne pour toute clé de déduplication. <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 de plugin remplacent tout ce qui est 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 sont ce que votre hook voit sur le contexte de page), tandis que les contributions de plugin gagnent toujours via la déduplication first-wins. Le rel des liens est limité à une liste d’autorisation verrouillée par sécurité (canonical, alternate, author, license, nlweb, site.standard.document) ; href doit être HTTP ou HTTPS.
page:fragments
Contribute du HTML brut, des scripts ou des feuilles de style aux points d’insertion de page. Plugins natifs uniquement.
Les plugins sandboxés ne peuvent pas utiliser ce hook car sa sortie s’exécute comme du code first-party dans le navigateur du visiteur, hors de toute frontière de sandbox. Pour des contributions de page sûres en sandbox, utilisez page:metadata. Voir Plugins natifs : fragments de page si vous avez besoin de cette surface.
Ordre d’exécution des hooks
Lorsqu’un plugin au format sandboxé s’exécute in-process, les hooks utilisent le pipeline de hooks partagé :
- Les hooks avec des valeurs
priorityplus basses s’exécutent en premier. - À priorités égales, les hooks s’exécutent dans l’ordre d’enregistrement des plugins.
- 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"], // waits for A even if its priority would normally be later
handler: async () => {},
}
Un runner de sandbox isolé invoque les plugins sandboxés actifs dans l’ordre de chargement. Gardez les hooks indépendants : n’exigez pas qu’un plugin sandboxé s’exécute avant un autre.
Gestion des erreurs
Les échecs de hooks sandboxés dépendent du moment où le hook s’exécute :
- Une erreur lancée dans
content:beforeSavefait échouer la sauvegarde avecCONTENT_HOOK_ERROR. Renvoyez l’enveloppeSAVE_REJECTEDdocumentée lorsque l’éditeur doit voir une raison de validation spécifique. - Renvoyer
falsedepuiscontent:beforeDeletearrête le déplacement vers la corbeille. Si ce hook lance, EmDash journalise l’erreur et poursuit la suppression. - Les after-hooks de contenu s’exécutent après le succès de l’opération. Leurs erreurs sont journalisées et ne peuvent pas annuler l’opération.
- Les hooks de cycle de vie, médias, e-mail et commentaires suivent le contrat de leur opération d’origine. Utilisez la référence des hooks pour vérifier une valeur de retour spécifique avant de vous fier au comportement d’échec.
Un plugin in-process peut utiliser errorPolicy: "abort" ou "continue" dans la forme de configuration complète. Ce réglage n’est pas un contrôle de récupération portable pour un plugin sandboxé isolé.
Timeouts
Le pipeline de hooks in-process a une valeur par défaut de 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 | Trigger | Return | Exclusive |
|---|---|---|---|
plugin:install | Première installation du plugin | void | No |
plugin:activate | Plugin activé | void | No |
plugin:deactivate | Plugin désactivé | void | No |
plugin:uninstall | Plugin retiré | void | No |
content:beforeSave | Avant l’enregistrement du contenu | Contenu modifié, enveloppe de rejet ou void | No |
content:afterSave | Après l’enregistrement du contenu | void | No |
content:beforeDelete | Avant le déplacement vers la corbeille | false pour annuler, sinon autoriser | No |
content:afterDelete | Après corbeille ou suppression permanente | void | No |
content:afterPublish | Après publication du contenu | void | No |
content:afterUnpublish | Après dépublication du contenu | void | No |
content:afterRestore | Après restauration du contenu | void | No |
content:afterSchedule | Après planification du contenu | void | No |
content:afterUnschedule | Après annulation de planification | void | No |
media:beforeUpload | Avant le téléversement de fichier | Infos fichier modifiées ou void | No |
media:afterUpload | Après le téléversement de fichier | void | No |
cron | Une tâche planifiée se déclenche | void | No |
email:beforeSend | Avant la livraison de l’e-mail | Message modifié, false ou void | No |
email:deliver | Livrer l’e-mail via le transport | void | Yes |
email:afterSend | Après la livraison de l’e-mail | void | No |
comment:beforeCreate | Avant le stockage du commentaire | Événement modifié, false ou void | No |
comment:moderate | Décider le statut du commentaire | { status, reason? } | Yes |
comment:afterCreate | Après le stockage du commentaire | void | No |
comment:afterModerate | L’admin change le statut du commentaire | void | No |
page:metadata | Rendu de page | Contributions ou null | No |
page:fragments | Rendu de page (natif uniquement) | Contributions ou null | No |
Voir la référence des hooks pour les types d’événements complets et les signatures des gestionnaires.