Référence des hooks

Sur cette page

Les hooks permettent aux plugins d’intercepter et de modifier le comportement d’EmDash à des points précis du cycle de vie du contenu, des médias, des e-mails, des commentaires et des pages.

Vue d’ensemble des hooks

Le tableau suivant répertorie chaque hook, ce qui le déclenche, ce qu’il peut modifier et s’il est exclusif :

HookDéclencheurPeut modifierExclusif
content:beforeSaveAvant l’enregistrement du contenuDonnées du contenuNon
content:afterSaveAprès l’enregistrement du contenuRienNon
content:beforeDeleteAvant la suppression du contenuPeut annulerNon
content:afterDeleteAprès la suppression du contenuRienNon
content:beforePublishAvant la publication du contenuPeut annulerNon
content:beforeScheduleAvant la planification du contenuPeut annulerNon
content:beforeUnpublishAvant la dépublication du contenuPeut annulerNon
content:afterPublishAprès la publication du contenuRienNon
content:afterUnpublishAprès la dépublication du contenuRienNon
content:afterRestoreAprès la restauration du contenuRienNon
content:afterScheduleAprès la planification du contenuRienNon
content:afterUnscheduleAprès l’annulation de la planificationRienNon
media:beforeUploadAvant le téléversement d’un fichierMétadonnées du fichierNon
media:afterUploadAprès le téléversement d’un fichierRienNon
cronExécution d’une tâche planifiéeRienNon
email:beforeSendAvant la livraison de l’e-mailMessage, peut annulerNon
email:deliverLivrer l’e-mail via le transportRienOui
email:afterSendAprès une livraison d’e-mail réussieRienNon
comment:beforeCreateAvant le stockage du commentaireCommentaire, peut annulerNon
comment:moderateDécider du statut d’approbation du commentaireStatutOui
comment:afterCreateAprès le stockage du commentaireRienNon
comment:afterModerateAprès changement de statut du commentaire par un adminRienNon
page:metadataRendu du head de page publiqueContribuer des balisesNon
page:fragmentsRendu du body de page publiqueInjecter des scriptsNon
plugin:installLors de la première installation du pluginRienNon
plugin:activateLorsque le plugin est activéRienNon
plugin:deactivateLorsque le plugin est désactivéRienNon
plugin:uninstallLorsque le plugin est suppriméRienNon

Hooks de contenu

content:beforeSave

Capability: content:write

S’exécute avant l’enregistrement du contenu en base de données. Utilisez-le pour valider, transformer ou enrichir le contenu. Un hook en sandbox rejette l’enregistrement en renvoyant un résultat de hook version 1 avec une erreur SAVE_REJECTED et un reason en texte brut de 1 à 500 caractères. L’API répond SAVE_REJECTED et l’admin identifie le plugin et affiche le motif. Des résultats d’erreur vides, trop longs, mal formés ou inconnus font échouer l’enregistrement avec une réponse générique CONTENT_HOOK_ERROR.

Depuis le processus hôte, lancez ContentSaveRejectedError (exporté depuis emdash) pour rejeter un enregistrement. Toute autre exception dans l’un ou l’autre mode d’exécution fait échouer l’enregistrement avec une réponse générique qui n’expose pas le message d’exception.

import { definePlugin } from "emdash";

export default definePlugin({
	id: "my-plugin",
	version: "1.0.0",
	hooks: {
		"content:beforeSave": async (event, ctx) => {
			const { content, collection, isNew } = event;

			// Add timestamps
			if (isNew) {
				content.createdBy = "system";
			}
			content.modifiedAt = new Date().toISOString();

			// Return modified content
			return content;
		},
	},
});

Événement

interface ActorInfo {
	readonly id: string;
	readonly role: number;
}

interface ContentHookEvent {
	content: Record<string, unknown>; // Content data
	collection: string; // Collection slug
	isNew: boolean; // True for creates, false for updates
	id?: string; // ID of the existing item on updates; absent on creates
	actor?: ActorInfo; // Authenticated user that initiated the save
}

Lors d’une mise à jour, content ne contient que les valeurs de champs soumises. Chargez l’entrée stockée avec ctx.content.get(event.collection, event.id) si le hook doit la comparer. Les enregistrements REST authentifiés, d’édition visuelle et MCP incluent actor. Les écritures internes sans utilisateur authentifié l’omettent.

Valeur de retour

  • Renvoyez l’objet contenu modifié pour appliquer les changements
  • Renvoyez une enveloppe d’erreur de hook sandbox pour rejeter l’enregistrement avec un motif en texte brut borné
  • Renvoyez void pour laisser passer inchangé

Un hook en sandbox renvoie cette enveloppe complète pour rejeter un enregistrement :

return {
	__emdashSandboxHookResult: true,
	version: 1,
	error: {
		code: "SAVE_REJECTED",
		reason: "Add a summary before saving.",
	},
};

L’hôte trim reason et accepte entre 1 et 500 caractères.

content:afterSave

Capability: content:read

S’exécute après l’enregistrement du contenu. Utilisez-le pour des effets de bord tels que notifications, invalidation du cache ou synchronisation externe.

hooks: {
  "content:afterSave": async (event, ctx) => {
    const { content, collection, isNew } = event;

    if (collection === "posts" && content.status === "published") {
      // Notify external service
      await ctx.http?.fetch("https://api.example.com/notify", {
        method: "POST",
        body: JSON.stringify({ postId: content.id }),
      });
    }
  },
}

Événement

content:afterSave reçoit content, collection, isNew et l’actor authentifié optionnel. content est l’entrée enregistrée complète, avec son ID en base dans content.id et les champs de collection sous content.data. L’id optionnelle séparée des mises à jour content:beforeSave est absente après l’enregistrement.

Valeur de retour

Aucune valeur de retour n’est attendue.

content:beforeDelete

Capability: content:read

S’exécute avant la suppression du contenu. Utilisez-le pour valider ou empêcher la suppression.

hooks: {
  "content:beforeDelete": async (event, ctx) => {
    const { id, collection } = event;

    // Prevent deletion of protected content
    const item = await ctx.content?.get(collection, id);
    if (item?.data.protected) {
      return false; // Cancel deletion
    }

    // Allow deletion
    return true;
  },
}

Événement

interface ContentDeleteEvent {
	id: string; // Entry ID
	collection: string; // Collection slug
	permanent?: false; // Present for native plugins; omitted in the sandbox runtime
}

content:beforeDelete ne s’exécute que lorsqu’une entrée est déplacée vers la corbeille. Les plugins natifs reçoivent permanent: false ; le runtime sandbox n’envoie que id et collection. Ne branchez pas dans un hook sandbox sur ce champ. La suppression définitive contourne content:beforeDelete, le hook ne peut donc pas empêcher un administrateur de supprimer définitivement une entrée déjà en corbeille.

Valeur de retour

  • Renvoyez false pour annuler la suppression
  • Renvoyez true ou void pour autoriser

content:afterDelete

Capability: content:read

S’exécute après la suppression du contenu. Utilisez-le pour le nettoyage.

hooks: {
  "content:afterDelete": async (event, ctx) => {
    const { id, collection, permanent } = event;

    if (permanent) {
      await ctx.storage.relatedItems.delete(`${collection}:${id}`);
    }
  },
}

L’événement contient id, collection et permanent. permanent est false lors du passage à la corbeille et true lors d’une suppression définitive. Vérifiez-le avant de supprimer des données dont une entrée restaurée aurait encore besoin. Ce hook n’a pas de valeur de retour.

content:beforePublish

Capability: hooks.content-policy:register

S’exécute avant la publication du contenu. Renvoyez void pour autoriser ou { cancel: true, reason } pour rejeter. EmDash accepte 1 à 500 caractères en texte brut dans reason et renvoie PUBLISH_REJECTED en cas d’annulation explicite.

L’événement contient le content, collection et origin actuels. Les actions API, MCP et éditeur visuel authentifiées incluent aussi actor avec la source correspondante. L’origine visual-editor requiert le jeton d’action signé et éphémère d’un rendu de barre d’outils authentifié. Les entrées planifiées réexécutent ce hook à l’échéance. Un rejet du planificateur déplanifie et liste l’entrée et le motif dans le tableau de bord. Planification, publication ou suppression réussie efface l’enregistrement ; un administrateur peut rejeter un enregistrement obsolète.

content:beforeSchedule

Capability: hooks.content-policy:register

S’exécute avant qu’une entrée reçoive une heure de publication. Même format de décision et champs d’origine que content:beforePublish, ajoute scheduledAt à l’événement et renvoie SCHEDULE_REJECTED en cas d’annulation explicite.

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

content:beforeUnpublish

Capability: hooks.content-policy:register

S’exécute avant de retirer du contenu publié en direct. Même format de décision et champs d’origine que content:beforePublish, renvoie UNPUBLISH_REJECTED en cas d’annulation explicite.

hooks.content-policy:register n’implique pas content:read, content:write ni les actions de publication. Décisions invalides et erreurs de politique abort inattendues échouent en mode fermé sans exposer les messages d’exception.

content:afterPublish

S’exécute après qu’une entrée est publiée avec succès, y compris une publication automatique à l’heure planifiée. Utilisez-le pour un travail qui dépend de l’entrée publiée, comme notifier un service ou rafraîchir un index externe.

hooks: {
  "content:afterPublish": async (event, ctx) => {
    ctx.log.info(`Published ${event.collection}/${event.content.id}`);
  },
}

Le hook requiert la capability content:read. EmDash l’exécute après la réponse de publication ; sa valeur de retour ne peut ni modifier l’entrée ni annuler la publication. Les erreurs sont journalisées ; avec errorPolicy: "abort", les hooks de publication suivants ne s’exécutent pas.

content:afterUnpublish

S’exécute après une dépublication réussie. Utilisez-le pour retirer ou mettre à jour des copies dans des systèmes externes. La dépublication annule aussi les planifications en attente sans exécuter content:afterUnschedule.

hooks: {
  "content:afterUnpublish": async (event, ctx) => {
    ctx.log.info(`Unpublished ${event.collection}/${event.content.id}`);
  },
}

Ce hook a les mêmes exigences content:read, exécution différée, forme d’événement et contrat sans valeur de retour que content:afterPublish.

content:afterRestore

S’exécute après restauration depuis la corbeille. Requiert content:read. L’entrée restaurée est un brouillon sans planification, quel que soit son statut avant la corbeille. Retirer la planification n’exécute pas non plus content:afterUnschedule.

hooks: {
  "content:afterRestore": async (event, ctx) => {
    ctx.log.info(`Restored ${event.collection}/${event.content.id}`);
  },
}

content:afterSchedule

S’exécute après planification d’une publication future. Requiert content:read.

hooks: {
  "content:afterSchedule": async (event, ctx) => {
    ctx.log.info(`Scheduled ${event.collection}/${event.content.id}`);
  },
}

content:afterUnschedule

S’exécute après annulation de la planification du contenu. Requiert content:read.

hooks: {
  "content:afterUnschedule": async (event, ctx) => {
    ctx.log.info(`Unscheduled ${event.collection}/${event.content.id}`);
  },
}

Événement

interface ContentStateChangeEvent {
	content: Record<string, unknown>;
	collection: string;
}

Cette forme d’événement est partagée par content:afterPublish, content:afterUnpublish, content:afterRestore, content:afterSchedule et content:afterUnschedule. content est l’entrée complète après le changement d’état, avec id, slug et statut ; les champs de collection sont sous content.data.

Valeur de retour

Aucune valeur de retour n’est attendue.

Hooks médias

media:beforeUpload

Capability: media:write

S’exécute avant le téléversement d’un fichier. Utilisez-le pour valider, renommer ou rejeter des fichiers.

hooks: {
  "media:beforeUpload": async (event, ctx) => {
    const { file } = event;

    // Reject files over 10MB
    if (file.size > 10 * 1024 * 1024) {
      throw new Error("File too large");
    }

    // Rename file
    return {
      name: `${Date.now()}-${file.name}`,
      type: file.type,
      size: file.size,
    };
  },
}

Événement

interface MediaUploadEvent {
	file: {
		name: string; // Original filename
		type: string; // MIME type
		size: number; // Size in bytes
	};
}

Valeur de retour

  • Renvoyez des métadonnées de fichier modifiées
  • Renvoyez void pour laisser passer inchangé
  • Lancez une exception pour rejeter le téléversement

media:afterUpload

Capability: media:read

S’exécute après le téléversement. Utilisez-le pour traitement, vignettes ou extraction de métadonnées.

hooks: {
  "media:afterUpload": async (event, ctx) => {
    const { media } = event;

    if (media.mimeType.startsWith("image/")) {
      // Store image metadata
      await ctx.kv.set(`media:${media.id}:analyzed`, {
        processedAt: new Date().toISOString(),
      });
    }
  },
}

Événement

interface MediaAfterUploadEvent {
	media: {
		id: string;
		filename: string;
		mimeType: string;
		size: number | null;
		url: string;
		createdAt: string;
	};
}

Valeur de retour

Aucune valeur de retour n’est attendue.

Hooks de cycle de vie

Les hooks de cycle de vie ne requièrent aucune capability d’enregistrement.

plugin:install

S’exécute lors de la première installation d’un plugin. Utilisez-le pour la configuration initiale, créer des collections de stockage ou des données de seed.

hooks: {
  "plugin:install": async (event, ctx) => {
    // Initialize default settings
    await ctx.settings.set("enabled", true);
    await ctx.settings.set("threshold", 100);

    ctx.log.info("Plugin installed successfully");
  },
}

plugin:activate

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

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

plugin:deactivate

S’exécute lorsque le plugin est désactivé.

hooks: {
  "plugin:deactivate": async (event, ctx) => {
    ctx.log.info("Plugin deactivated");
  },
}

plugin:install, plugin:activate et plugin:deactivate reçoivent un objet événement vide. Pas de valeur de retour.

plugin:uninstall

S’exécute lors de la suppression d’un plugin. Utilisez-le pour le nettoyage.

hooks: {
  "plugin:uninstall": async (event, ctx) => {
    const { deleteData } = event;

    if (deleteData) {
      // Clean up all plugin data
      const items = await ctx.settings.list();
      for (const { key } of items) {
        await ctx.kv.delete(key);
      }
    }

    ctx.log.info("Plugin uninstalled");
  },
}

Événement

interface UninstallEvent {
	deleteData: boolean; // User chose to delete data
}

Le hook de désinstallation n’a pas de valeur de retour.

Hook cron

cron

Capability : Aucune requise

Se déclenche lorsqu’une tâche planifiée s’exécute. Planifiez avec ctx.cron.schedule(). Les expressions cron sont évaluées en UTC sur chaque hôte.

hooks: {
  "cron": async (event, ctx) => {
    if (event.name === "daily-sync") {
      const data = await ctx.http?.fetch("https://api.example.com/data");
      ctx.log.info("Sync complete");
    }
  },
}

Événement

interface CronEvent {
	name: string;
	data?: Record<string, unknown>;
	scheduledAt: string;
}

Le hook cron n’a pas de valeur de retour.

Hooks e-mail

Pour les messages envoyés par les plugins, les hooks e-mail s’exécutent dans l’ordre : email:beforeSend, puis email:deliver, puis email:afterSend. Les messages d’authentification système vont directement à email:deliver ; ils ne passent pas par email:beforeSend ni email:afterSend.

email:beforeSend

Capability: hooks.email-events:register

Hook middleware avant la livraison. Transformez les messages ou annulez la livraison.

hooks: {
  "email:beforeSend": async (event, ctx) => {
    // Add footer to all emails
    return {
      ...event.message,
      text: event.message.text + "\n\n—Sent from My Site",
    };

    // Or return false to cancel delivery
  },
}

Événement

interface EmailBeforeSendEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

Valeur de retour

  • Renvoyez le message modifié
  • Renvoyez false pour annuler la livraison

email:deliver

Capability: hooks.email-transport:register | Exclusif : Oui

Fournisseur de transport. Un seul plugin peut livrer les e-mails. Responsable de l’envoi via un service e-mail.

hooks: {
  "email:deliver": {
    exclusive: true,
    handler: async (event, ctx) => {
      await sendViaSES(event.message);
    },
  },
}

Événement et valeur de retour

interface EmailDeliverEvent {
	message: {
		to: string;
		cc?: string[];
		replyTo?: string;
		subject: string;
		text: string;
		html?: string;
	};
	source: string;
}

Le hook n’a pas de valeur de retour. source es "system" para mensajes de autenticación de EmDash y el ID del plugin para mensajes enviados por un plugin.

Les plugins peuvent définir cc et replyTo. Les fournisseurs doivent livrer les deux s’ils sont présents ; ignorer cc prive ces destinataires du message.

email:afterSend

Capability: hooks.email-events:register

Hook fire-and-forget après livraison réussie. Les erreurs sont journalisées sans propagation.

hooks: {
  "email:afterSend": async (event, ctx) => {
    await ctx.kv.set(`email:log:${Date.now()}`, {
      to: event.message.to,
      subject: event.message.subject,
    });
  },
}

Événement et valeur de retour

email:afterSend reçoit les mêmes champs message et source que email:deliver. Pas de valeur de retour.

Hooks de commentaires

Les hooks de commentaires s’exécutent dans l’ordre : comment:beforeCreate, puis comment:moderate, puis comment:afterCreate. comment:afterModerate s’exécute séparément lors d’un changement de statut par un admin ou un plugin autorisé.

Après stockage d’un commentaire, les hooks reçoivent cette forme d’enregistrement :

interface StoredComment {
	id: string;
	collection: string;
	contentId: string;
	parentId: string | null;
	authorName: string;
	authorEmail: string;
	authorUserId: string | null;
	body: string;
	status: string;
	moderationMetadata: Record<string, unknown> | null;
	createdAt: string;
	updatedAt: string;
}

comment:beforeCreate

Capability: users:read

Hook middleware avant stockage d’un commentaire. Enrichissez, validez ou rejetez.

hooks: {
  "comment:beforeCreate": async (event, ctx) => {
    // Reject comments with links
    if (event.comment.body.includes("http")) {
      return false;
    }
  },
}

Événement

interface CommentBeforeCreateEvent {
	comment: {
		collection: string;
		contentId: string;
		parentId: string | null;
		authorName: string;
		authorEmail: string;
		authorUserId: string | null;
		body: string;
		ipHash: string | null;
		userAgent: string | null;
	};
	metadata: Record<string, unknown>;
}

Valeur de retour

  • Renvoyez l’événement modifié
  • Renvoyez false pour rejeter
  • Devuelva void para dejar pasar

comment:moderate

Capability: users:read | Exclusif : Oui

Décide si un commentaire est approuvé, en attente ou spam. Un seul modérateur actif.

hooks: {
  "comment:moderate": {
    exclusive: true,
    handler: async (event, ctx) => {
      const score = await checkSpam(event.comment);
      return {
        status: score > 0.8 ? "spam" : score > 0.5 ? "pending" : "approved",
        reason: `Spam score: ${score}`,
      };
    },
  },
}

Événement

interface CommentModerateEvent {
	comment: { /* same as beforeCreate */ };
	metadata: Record<string, unknown>;
	collectionSettings: {
		commentsEnabled: boolean;
		commentsModeration: "all" | "first_time" | "none";
		commentsClosedAfterDays: number;
		commentsAutoApproveUsers: boolean;
	};
	priorApprovedCount: number;
}

Valeur de retour

{ status: "approved" | "pending" | "spam"; reason?: string }

Modérateur actif

EmDash inclut un modérateur intégré appliquant les paramètres de commentaires de la collection. Si un seul plugin fournit comment:moderate, EmDash le choisit à la place du modérateur intégré et mémorise le choix. Si plusieurs plugins le fournissent sans choix mémorisé, aucun n’est sélectionné et les nouveaux commentaires attendent une revue. Un choix mémorisé, y compris le modérateur intégré, reste tant que le modérateur choisi est activé.

comment:afterCreate

Capability: users:read

Hook fire-and-forget après stockage d’un commentaire. Utilisez-le pour les notifications. L’envoi d’e-mail requiert aussi email:send et un fournisseur email:deliver ; sans les deux, ctx.email est undefined.

hooks: {
  "comment:afterCreate": async (event, ctx) => {
    const recipient = event.contentAuthor?.email;
    if (event.comment.status === "approved" && recipient && ctx.email) {
      await ctx.email.send({
        to: recipient,
        subject: `New comment on "${event.content.title}"`,
        text: `${event.comment.authorName} commented: ${event.comment.body}`,
      });
    }
  },
}

Événement et valeur de retour

interface CommentAfterCreateEvent {
	comment: StoredComment;
	metadata: Record<string, unknown>;
	content: { id: string; collection: string; slug: string; title?: string };
	contentAuthor?: { id: string; name: string | null; email: string };
}

Le hook n’a pas de valeur de retour.

comment:afterModerate

Capability: users:read

S’exécute après changement de statut par un admin ou un plugin. Une transition réussie exécute le hook une fois. Les erreurs sont journalisées et n’annulent pas le changement de statut.

Événement

interface CommentAfterModerateEvent {
	comment: StoredComment;
	previousStatus: string;
	newStatus: string;
	moderator: { id: string; name: string | null };
	origin?:
		| { source: "admin"; userId: string }
		| { source: "plugin"; pluginId: string };
}

Le hook n’a pas de valeur de retour.

Hooks de page

Les hooks de page s’exécutent lors du rendu des pages publiques. Ils permettent d’injecter métadonnées et scripts.

Les deux hooks de page reçoivent le contexte de page publique actuel :

interface PublicPageContext {
	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;
}

interface PageMetadataEvent { page: PublicPageContext }
interface PageFragmentEvent { page: PublicPageContext }

page:metadata

Capability : Aucune requise

Contribuez des balises meta, Open Graph, JSON-LD ou link tags dans le head.

hooks: {
  "page:metadata": async (event, ctx) => {
    return [
      { kind: "meta", name: "generator", content: "EmDash" },
      { kind: "property", property: "og:site_name", content: event.page.siteName ?? "My Site" },
      { kind: "jsonld", graph: { "@type": "WebSite", name: event.page.siteName } },
    ];
  },
}

Types de contribution

type PageMetadataContribution =
	| { kind: "meta"; name: string; content: string; key?: string }
	| { kind: "property"; property: string; content: string; key?: string }
	| {
			kind: "link";
			rel: "canonical" | "alternate" | "author" | "license" | "nlweb" | "site.standard.document";
			href: string;
			hreflang?: string;
			key?: string;
	  }
	| {
			kind: "jsonld";
			id?: string;
			graph: Record<string, unknown> | Array<Record<string, unknown>>;
	  };

Le champ key déduplique — seule la dernière contribution avec une clé donnée est utilisée.

Renvoyez une contribution, un tableau ou null si le plugin n’ajoute rien.

page:fragments

Capability: hooks.page-fragments:register

Injectez scripts ou HTML dans les pages. Réservé aux plugins natifs.

hooks: {
  "page:fragments": async (event, ctx) => {
    return [
      {
        kind: "external-script",
        placement: "body:end",
        src: "https://analytics.example.com/script.js",
        async: true,
      },
      {
        kind: "inline-script",
        placement: "head",
        code: `window.siteId = "abc123";`,
      },
    ];
  },
}

Types de contribution

type PageFragmentContribution =
	| {
			kind: "external-script";
			placement: "head" | "body:start" | "body:end";
			src: string;
			async?: boolean;
			defer?: boolean;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "inline-script";
			placement: "head" | "body:start" | "body:end";
			code: string;
			attributes?: Record<string, string>;
			key?: string;
		}
	| {
			kind: "html";
			placement: "head" | "body:start" | "body:end";
			html: string;
			key?: string;
		};

Renvoyez un fragment, un tableau ou null si le plugin n’ajoute rien.

Configuration des hooks

Les hooks acceptent une fonction handler ou un objet de configuration :

hooks: {
  // Simple handler
  "content:afterSave": async (event, ctx) => { ... },

  // With configuration
  "content:beforeSave": {
    priority: 50,        // Lower runs first (default: 100)
    timeout: 10000,      // Max execution time in ms (default: 5000)
    dependencies: [],    // Run after these plugins
    errorPolicy: "abort", // "continue" or "abort" (default)
    handler: async (event, ctx) => { ... },
  },
}

Options de configuration

OptionTypePar défautDescription
prioritynumber100Ordre d’exécution (plus bas = plus tôt)
timeoutnumber5000Durée max d’exécution en millisecondes
dependenciesstring[][]IDs de plugins devant s’exécuter en premier
errorPolicystring"abort""continue" pour ignorer les erreurs
exclusivebooleanfalseUn seul plugin peut être le fournisseur actif (hooks provider comme email:deliver, comment:moderate)

Contexte du plugin

Tous les hooks reçoivent un objet de contexte avec accès aux API plugin :

interface PluginContext {
	plugin: { id: string; version: string };
	storage: PluginStorage;
	kv: KVAccess;
	content?: ContentAccess;
	media?: MediaAccess;
	http?: HttpAccess;
	log: LogAccess;
	site: { name: string; url: string; locale: string };
	url(path: string): string;
	users?: UserAccess;
	cron?: CronAccess;
	email?: EmailAccess;
}

Voir Capacités des plugins pour la capability requise par chaque API de contexte.

Gestion des erreurs

Les erreurs dans les hooks sont journalisées selon errorPolicy :

  • "abort" (par défaut) — arrêter l’exécution, rollback de transaction si applicable
  • "continue" — journaliser et continuer au hook suivant
hooks: {
  "content:beforeSave": {
    errorPolicy: "continue", // Don't block save if this fails
    handler: async (event, ctx) => {
      try {
        await ctx.http?.fetch("https://api.example.com/validate");
      } catch (error) {
        ctx.log.warn("Validation service unavailable", error);
      }
    },
  },
}

Ordre d’exécution

Les hooks s’exécutent dans cet ordre :

  1. Triés par priority (croissant)
  2. Les plugins avec dependencies s’exécutent après leurs dépendances
  3. À priorité égale, l’ordre est déterministe mais non spécifié
// This runs first (priority 10)
{ priority: 10, handler: ... }

// This runs second (priority 50)
{ priority: 50, handler: ... }

// This runs last (default priority 100)
{ handler: ... }