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
| Option | Type | Défaut | Description |
|---|---|---|---|
priority | number | 100 | Ordre d’exécution. Les valeurs plus faibles passent en premier. |
timeout | number | 5000 | Durée d’exécution maximale en millisecondes. |
dependencies | string[] | [] | 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. |
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. |
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 :
- Les hooks avec des valeurs
priorityplus basses passent en premier - À priorité égale, l’ordre est celui de l’enregistrement des plugins
- Les hooks avec
dependenciesattendent 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 :
| Type | Rendu sous forme de | 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 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
| 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 retiré | void | Non |
content:beforeSave | Avant enregistrement du contenu | Contenu modifié ou void | Non |
content:afterSave | Après enregistrement du contenu | void | Non |
content:beforeDelete | Avant suppression du contenu | false pour annuler, sinon autoriser | Non |
content:afterDelete | Après suppression du contenu | void | Non |
content:afterPublish | Après publication du contenu | void | Non |
content:afterUnpublish | Après dépublication du contenu | void | Non |
media:beforeUpload | Avant téléversement de fichier | Infos fichier modifiées ou void | Non |
media:afterUpload | Après téléversement de fichier | void | Non |
cron | La tâche planifiée se déclenche | void | Non |
email:beforeSend | Avant envoi d’e-mail | Message modifié, false ou void | Non |
email:deliver | Délivrer l’e-mail via le transport | void | Oui |
email:afterSend | Après envoi d’e-mail | void | Non |
comment:beforeCreate | Avant stockage du commentaire | Événement modifié, false ou void | Non |
comment:moderate | Décider du statut du commentaire | { status, reason? } | Oui |
comment:afterCreate | Après stockage du commentaire | void | Non |
comment:afterModerate | L’admin change le statut du commentaire | void | Non |
page:metadata | Rendu de page | Contributions ou null | Non |
page:fragments | Rendu de page (confiance) | Contributions ou null | Non |
Voir la référence des hooks pour les types d’événements complets et les signatures des gestionnaires.