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 :
| Hook | Déclencheur | Peut modifier | Exclusif |
|---|---|---|---|
content:beforeSave | Avant l’enregistrement du contenu | Données de contenu | Non |
content:afterSave | Après l’enregistrement du contenu | Rien | Non |
content:beforeDelete | Avant la suppression du contenu | Peut annuler | Non |
content:afterDelete | Après la suppression du contenu | Rien | Non |
media:beforeUpload | Avant le téléchargement de fichier | Métadonnées fichier | Non |
media:afterUpload | Après le téléchargement de fichier | Rien | Non |
cron | Tâche planifiée s’exécute | Rien | Non |
email:beforeSend | Avant l’envoi d’email | Message, peut annuler | Non |
email:deliver | Livrer l’email via transport | Rien | Oui |
email:afterSend | Après l’envoi réussi d’email | Rien | Non |
comment:beforeCreate | Avant l’enregistrement du commentaire | Commentaire, peut annuler | Non |
comment:moderate | Décider du statut d’approbation | Statut | Oui |
comment:afterCreate | Après l’enregistrement du commentaire | Rien | Non |
comment:afterModerate | Après changement de statut par admin | Rien | Non |
page:metadata | Rendu du head de page publique | Contribuer des tags | Non |
page:fragments | Rendu du body de page publique | Injecter scripts | Non |
plugin:install | À la première installation du plugin | Rien | Non |
plugin:activate | Lors de l’activation du plugin | Rien | Non |
plugin:deactivate | Lors de la désactivation du plugin | Rien | Non |
plugin:uninstall | Lors de la suppression du plugin | Rien | Non |
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
voidpour 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
falsepour annuler la suppression - Retourner
trueouvoidpour 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
voidpour 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
falsepour annuler la livraison - Retourner
voidpour 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
falsepour rejeter - Retourner
voidpour 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
| Option | Type | Défaut | Description |
|---|---|---|---|
priority | number | 100 | Ordre d’exécution (plus bas = avant) |
timeout | number | 5000 | Temps d’exécution maximum en millisecondes |
dependencies | string[] | [] | IDs de plugins qui doivent s’exécuter en premier |
errorPolicy | string | "abort" | "continue" pour ignorer les erreurs |
exclusive | boolean | false | Un 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 :
- Triés par
priority(croissant) - Les plugins avec
dependenciess’exécutent après leurs dépendances - 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: ... }