Hooks (filtres et actions)
Les hooks sont la façon dont les plugins observent et mutent le pipeline de publication. Il y a deux types :
Les hooks sont la façon dont les plugins observent et mutent le pipeline de publication. Il y a deux types :
- Filtres mutent une valeur. Chaque handler reçoit la valeur courante, retourne une nouvelle valeur (ou inchangée). Composables dans l'ordre des priorités.
- Actions sont des effets de bord. Chaque handler tourne ; rien n'est retourné ; un handler ne peut pas court-circuiter un autre.
Cette page est le guide pratique « comment s'abonner ». Pour la liste exhaustive de tous les hooks que le cœur déclenche, voir la Référence des hooks.
Filtres
api.addFilter<string>(
"page.head.extra", // nom du hook
(current, baseLayoutProps) => { // handler
return current + "<meta name='generator' content='My Plugin' />";
},
10 // priorité (lower runs first; default 10)
);
Filtres async vs sync
La plupart des filtres sont async — leur handler peut être async et retourner une promesse :
api.addFilter<string>("post.html.body", async (html, post, ctx) => {
const enriched = await fetchSomething(post.id);
return html + enriched;
});
Deux filtres sont sync : page.head.extra et page.body.end. Ils sont appelés depuis le rendu React inline et ne peuvent pas attendre. Les handlers async sur ces hooks tournent quand même mais leur retour est ignoré (la valeur originale persiste).
Priorité
addFilter<T>(hook, fn, priority?) accepte une priorité. Plus bas tourne en premier. Défaut : 10. Vos handlers se mettent typiquement à 10 ou 100.
Cas où la priorité importe :
- Vous voulez tourner après un autre plugin (ex. après
flexweg-blocksqui injecte la CSS des colonnes) — mettez 50 (ou plus haut) - Vous voulez tourner avant un autre (ex. pré-traiter le markdown avant que
flexweg-blocksne le parse) — mettez 1 (ou plus bas)
Type generic
addFilter<string>(...) donne au TS la signature exacte. Pour des hooks qui mutent des objets complexes :
api.addFilter<MenuJson>("menu.json.resolved", (menu, ctx) => {
return { ...menu, footer: [...menu.footer, myItem] };
});
Actions
api.addAction(
"publish.complete",
async (post, ctx) => {
await regenerateMyFiles(post, ctx);
},
100
);
Pas de retour
Une action n'est pas censée retourner quelque chose. La valeur de retour est ignorée. Si vous lancez une exception, elle est attrapée + loggée + avalée — les autres handlers tournent quand même.
ctx est déjà patché
Pour publish.complete, post.unpublished, post.deleted : ctx.posts reflète déjà l'état post-publication. Vous lisez la nouvelle vue, pas l'ancienne.
Pour publish.before : ctx.posts reflète encore l'état pré-publication.
Lire la config plugin dans un handler
Le pattern standard :
api.addAction("publish.complete", async (post, ctx) => {
const config = ctx.settings.pluginConfigs?.["my-plugin"] ?? DEFAULT_CONFIG;
if (!config.enabled) return;
// ... use config
});
ctx.settings est le SiteSettings live au moment de la publication. pluginConfigs[id] est l'objet sauvegardé via votre page de réglages.
Désabonnement
Pas d'API explicite. À chaque applyPluginRegistration(), la registry est vidée puis re-peuplée — donc si vous désactivez votre plugin via les réglages, votre handler disparaît au prochain pass.
Si vous avez besoin de désabonner pendant la vie d'une registration (rare), capturez l'index retourné par addFilter et utilisez removeFilter — mais c'est exotique.
Throw, catch, ignore
- Si un filtre throw, l'erreur est attrapée + loggée + la valeur d'origine est passée au prochain filtre dans la chaîne. La publication continue.
- Si une action throw, l'erreur est attrapée + loggée. Les autres actions tournent quand même.
- Aucun handler ne peut tuer la publication. C'est intentionnel — vous ne voulez pas qu'un plugin tiers bloque le travail principal.
Pour signaler une vraie erreur fatale à l'utilisateur, throw + appelez toast.error(...) dans votre handler :
api.addAction("publish.complete", async (post, ctx) => {
try {
await criticalWork(post);
} catch (err) {
toast.error(`My Plugin: ${err.message}`);
throw err; // re-throw pour que ce soit loggé
}
});
Exemple : un filtre qui ajoute une meta SEO
import type { PluginManifest } from "@flexweg/cms-runtime";
const manifest: PluginManifest = {
id: "my-seo-plugin",
name: "My SEO Plugin",
version: "1.0.0",
register(api) {
api.addFilter<string>("page.head.extra", (head, props) => {
const config = props.site.settings.pluginConfigs?.["my-seo-plugin"];
if (!config?.enabled) return head;
return head + `<meta name="x-my-seo" content="${config.value}" />\n`;
});
},
};
export default manifest;
Exemple : une action qui réagit à la publication
const manifest: PluginManifest = {
id: "my-webhook",
name: "My Webhook",
version: "1.0.0",
register(api) {
api.addAction("publish.complete", async (post, ctx) => {
const config = ctx.settings.pluginConfigs?.["my-webhook"];
if (!config?.url) return;
await fetch(config.url, {
method: "POST",
body: JSON.stringify({ slug: post.slug, title: post.title }),
});
});
},
};