Référence des Hooks

Sur cette page

Les hooks permettent aux plugins d’intercepter et de modifier le comportement d’EmDash à des points spécifiques du cycle de vie du contenu, des médias, des emails, des commentaires et des pages.

Aperçu des hooks

Le tableau suivant liste 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 de contenuNon
content:afterSaveAprès l’enregistrement du contenuRienNon
content:beforeDeleteAvant la suppression du contenuPeut annulerNon
content:afterDeleteAprès la suppression du contenuRienNon
media:beforeUploadAvant le téléchargement de fichierMétadonnées fichierNon
media:afterUploadAprès le téléchargement de fichierRienNon
cronTâche planifiée s’exécuteRienNon
email:beforeSendAvant l’envoi d’emailMessage, peut annulerNon
email:deliverLivrer l’email via transportRienOui
email:afterSendAprès l’envoi réussi d’emailRienNon
comment:beforeCreateAvant l’enregistrement du commentaireCommentaire, peut annulerNon
comment:moderateDécider du statut d’approbationStatutOui
comment:afterCreateAprès l’enregistrement du commentaireRienNon
comment:afterModerateAprès changement de statut par adminRienNon
page:metadataRendu du head de page publiqueContribuer des tagsNon
page:fragmentsRendu du body de page publiqueInjecter scriptsNon
plugin:installÀ la première installation du pluginRienNon
plugin:activateLors de l’activation du pluginRienNon
plugin:deactivateLors de la désactivation du pluginRienNon
plugin:uninstallLors de la suppression du pluginRienNon

Hooks de Contenu

content:beforeSave

S’exécute avant l’enregistrement du contenu dans la base de données. Utilisez pour valider, transformer ou enrichir le contenu.

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;
		},
	},
});

Event

interface ContentHookEvent {
	content: Record<string, unknown>; // Content data
	collection: string; // Collection slug
	isNew: boolean; // True for creates, false for updates
}

Valeur de retour

  • Retourner l’objet de contenu modifié pour appliquer les changements
  • Retourner void pour passer sans modification

content:afterSave

S’exécute après l’enregistrement du contenu. Utilisez pour les effets secondaires comme les notifications, l’invalidation du cache ou la 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 }),
      });
    }
  },
}

Valeur de retour

Aucune valeur de retour attendue.

content:beforeDelete

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

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;
  },
}

Event

interface ContentDeleteEvent {
	id: string; // Entry ID
	collection: string; // Collection slug
}

Valeur de retour

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

content:afterDelete

S’exécute après la suppression du contenu. Utilisez pour les tâches de nettoyage.

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

    // Clean up related data
    await ctx.storage.relatedItems.delete(`${collection}:${id}`);
  },
}

Hooks de Médias

media:beforeUpload

S’exécute avant le téléchargement d’un fichier. Utilisez 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,
    };
  },
}

Event

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

Valeur de retour

  • Retourner les métadonnées de fichier modifiées pour appliquer les changements
  • Retourner void pour passer sans modification
  • Lever une exception pour rejeter le téléchargement

media:afterUpload

S’exécute après le téléchargement d’un fichier. Utilisez pour le traitement, les miniatures ou l’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(),
      });
    }
  },
}

Event

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

Hooks de Cycle de Vie

plugin:install

S’exécute lors de la première installation d’un plugin. Utilisez pour la configuration initiale, la création de collections de stockage ou les données d’amorçage.

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

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

plugin:activate

S’exécute lors de l’activation d’un plugin (après installation ou réactivation).

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

plugin:deactivate

S’exécute lors de la désactivation d’un plugin.

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

plugin:uninstall

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

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

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

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

Event

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

Hook Cron

cron

Déclenché lorsqu’une tâche planifiée s’exécute. Planifiez des tâches avec ctx.cron.schedule().

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");
    }
  },
}

Event

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

Hooks d’Email

Les hooks d’email s’exécutent dans l’ordre : email:beforeSend, puis email:deliver, puis email:afterSend.

email:beforeSend

Capacité : hooks.email-events:register

Hook middleware qui s’exécute 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
  },
}

Event

interface EmailBeforeSendEvent {
	message: { to: string; subject: string; text: string; html?: string };
	source: string;
}

Valeur de retour

  • Retourner le message modifié pour transformer
  • Retourner false pour annuler la livraison
  • Retourner void pour passer sans modification

email:deliver

Capacité : hooks.email-transport:register | Exclusif : Oui

Le fournisseur de transport. Un seul plugin peut livrer les emails. Responsable de l’envoi réel du message via un service d’email.

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

email:afterSend

Capacité : hooks.email-events:register

Hook fire-and-forget après une livraison réussie. Les erreurs sont enregistrées mais ne se propagent pas.

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

Hooks de Commentaires

Les hooks de commentaires s’exécutent dans l’ordre : comment:beforeCreate, puis comment:moderate, puis comment:afterCreate. Le hook comment:afterModerate se déclenche séparément lorsqu’un admin change le statut d’un commentaire.

comment:beforeCreate

Capacité : users:read

Hook middleware avant l’enregistrement d’un commentaire. Enrichissez, validez ou rejetez les commentaires.

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

Event

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

  • Retourner l’événement modifié pour transformer
  • Retourner false pour rejeter
  • Retourner void pour passer

comment:moderate

Capacité : users:read | Exclusif : Oui

Décider si un commentaire est approuvé, en attente ou spam. Un seul fournisseur de modération est 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}`,
      };
    },
  },
}

Event

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 }

comment:afterCreate

Capacité : users:read

Hook fire-and-forget après l’enregistrement d’un commentaire. Utilisez pour les notifications.

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

comment:afterModerate

Capacité : users:read

Hook fire-and-forget lorsqu’un admin change manuellement le statut d’un commentaire.

Event

interface CommentAfterModerateEvent {
	comment: { id: string; /* ... */ };
	previousStatus: string;
	newStatus: string;
	moderator: { id: string; name: string | null };
}

Hooks de Page

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

page:metadata

Capacité : Aucune requise

Contribuer des meta tags, propriétés Open Graph, données structurées JSON-LD ou tags de lien au head de la page.

hooks: {
  "page:metadata": async (event, ctx) => {
    return [
      { kind: "meta", name: "generator", content: "EmDash" },
      { kind: "property", property: "og:site_name", content: event.page.siteName },
      { 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 les contributions — seule la dernière contribution avec une clé donnée est utilisée.

page:fragments

Capacité : hooks.page-fragments:register

Injecter des scripts ou HTML dans les pages. Disponible uniquement pour les 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;
		};

Configuration des Hooks

Les hooks acceptent soit une fonction handler soit 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

OptionTypeDéfautDescription
prioritynumber100Ordre d’exécution (plus bas = avant)
timeoutnumber5000Temps d’exécution maximum en millisecondes
dependenciesstring[][]IDs de plugins qui doivent s’exécuter en premier
errorPolicystring"abort""continue" pour ignorer les erreurs
exclusivebooleanfalseUn seul plugin peut être le fournisseur actif (pour les hooks de modèle fournisseur comme email:deliver, comment:moderate)

Contexte de Plugin

Tous les hooks reçoivent un objet de contexte avec accès aux APIs du 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 Aperçu des Plugins — Contexte de Plugin pour les exigences de capacités et les détails des méthodes.

Gestion des Erreurs

Les erreurs dans les hooks sont enregistrées et traitées selon errorPolicy :

  • "abort" (défaut) — Arrêter l’exécution, annuler la transaction si applicable
  • "continue" — Enregistrer l’erreur 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. Au sein de la même priorité, 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: ... }