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

TS
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 :

TS
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-blocks qui injecte la CSS des colonnes) — mettez 50 (ou plus haut)
  • Vous voulez tourner avant un autre (ex. pré-traiter le markdown avant que flexweg-blocks ne 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 :

TS
api.addFilter<MenuJson>("menu.json.resolved", (menu, ctx) => {
  return { ...menu, footer: [...menu.footer, myItem] };
});

Actions

TS
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 :

TS
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 :

TS
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

TS
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

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