Blocs de thème

Les thèmes peuvent contribuer des blocs d'éditeur — blocs atomiques qui rendent au moment de la publication des sections complètes de HTML (hero, grilles de posts, CTAs, sidebars). Les blocs de thème

Les thèmes peuvent contribuer des blocs d'éditeur — blocs atomiques qui rendent au moment de la publication des sections complètes de HTML (hero, grilles de posts, CTAs, sidebars). Les blocs de thème sont scopés au thème actif : basculer de thème désenregistre les blocs de l'ancien et enregistre ceux du nouveau.

C'est ce qui permet au thème magazine d'avoir son widget Most read, au corporate son Contact form, etc. Les thèmes customs peuvent faire pareil.

Quand utiliser un bloc de thème vs un bloc de cœur

Usage À utiliser
Primitive de mise en page utile entre thèmes (paragraphe, titre, image, colonnes) Bloc de cœur (enregistré dans src/core/coreBlocks.ts)
Primitive de mise en page qui utilise une CSS ou structure spécifique au thème Bloc de thème (enregistré dans le manifest du thème)
Type de contenu réutilisable avec config (témoignage, table de tarifs) Bloc de thème si styling spécifique, bloc de plugin sinon

Déclarer des blocs dans le thème

TS
import type { ThemeManifest, BlockManifest } from "@flexweg/cms-runtime";
import { heroSplitBlock, mostReadBlock, promoCardBlock } from "./blocks";

const manifest: ThemeManifest = {
  // ...
  blocks: [
    heroSplitBlock,
    mostReadBlock,
    promoCardBlock,
  ],
};

Chaque BlockManifest a le même shape que pour les plugins. Voir Blocs de plugin pour la structure complète.

Différence avec les blocs de plugin

  • Cycle de vie : les blocs de thème sont enregistrés à l'activation du thème, désenregistrés au basculement. Les blocs de plugin sont enregistrés à l'activation du plugin, désenregistrés au reset.
  • Namespace : les blocs de thème utilisent typiquement l'id du thème comme namespace (magazine/hero-split). Les blocs de plugin utilisent l'id du plugin.
  • Rendu : pareil — le marqueur <div data-cms-block="..." data-attrs="..."></div> round-trip dans le markdown, et un filtre post.html.body du thème le remplace par du HTML.

Pattern de rendu : le filtre transforms.ts

Convention pour les thèmes avec plusieurs blocs : un fichier src/themes/<id>/blocks/transforms.ts qui enregistre un seul filtre post.html.body qui dispatche entre tous les blocs du thème par marker.

TS
import { addFilter } from "@flexweg/cms-runtime";
import { renderHeroSplit } from "./heroSplit/render";
import { renderMostRead } from "./mostRead/render";
import { renderPromoCard } from "./promoCard/render";

const MARKER_RE = /<div\s+([^>]*data-cms-block="magazine\/([a-z-]+)"[^>]*)>\s*<\/div>/g;

export function registerMagazineTransforms() {
  addFilter<string>("post.html.body", (html, post, ctx) => {
    return html.replace(MARKER_RE, (full, raw, blockName) => {
      const m = raw.match(/data-attrs=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/);
      const enc = m ? (m[1] ?? m[2] ?? m[3] ?? "") : "";
      const attrs = decodeAttrs(enc);
      switch (blockName) {
        case "hero-split": return renderHeroSplit(attrs, post, ctx);
        case "most-read": return renderMostRead(attrs, post, ctx);
        case "promo-card": return renderPromoCard(attrs, post, ctx);
        default: return full;  // marqueur inconnu — laisser tel quel
      }
    });
  });
}

Et appelé depuis le module principal du thème.

Pattern de rendu : helper getCurrentPublishContext

Si votre renderXxx(attrs) a besoin du ctx complet pour résoudre des médias ou des posts liés, utilisez getCurrentPublishContext() du runtime — il retourne le ctx live au moment du rendu :

TS
import { getCurrentPublishContext, mediaToView } from "@flexweg/cms-runtime";

export function renderHeroSplit(attrs) {
  const ctx = getCurrentPublishContext();
  const hero = attrs.heroMediaId ? ctx.media.get(attrs.heroMediaId) : null;
  const heroView = hero ? mediaToView(hero) : null;
  // ...
  return `<section class="hero-split">...</section>`;
}

Blocs avec inspector

L'inspector (sidebar droite, onglet Bloc) est un composant React qui édite les attrs du bloc. Voir Blocs de plugin.

Pour les blocs de thème complexes (ex. magazine/promo-card avec choix d'image, lien, label), l'inspector est typiquement une page entière de formulaire. Utilisez les utility classes Tailwind de l'admin (pas la CSS de votre thème — l'inspector tourne dans l'admin, pas dans le rendu public).

Le décodage des attrs

Le data-attrs est de la JSON encodée en base64 (btoa(JSON.stringify(attrs))). Pour le décoder côté rendu :

TS
function decodeAttrs<T>(encoded: string, defaults: T): T {
  try {
    return JSON.parse(atob(encoded));
  } catch {
    return defaults;
  }
}

Toujours avoir des defaults : si le JSON est corrompu ou mal-encodé, vous renvoyez quelque chose de sensé au lieu de crasher le rendu.

Cleanup côté thème

Quand l'utilisateur bascule de thème, les marqueurs magazine/hero-split restent dans le markdown des posts publiés. Au prochain rendu (par le nouveau thème), le filtre post.html.body de l'ancien thème n'est plus actif — donc les marqueurs ne sont plus remplacés. Le HTML public contient des <div data-cms-block="magazine/hero-split" data-attrs="..."></div> vides.

Solutions :

  1. Avant de basculer : éditer chaque post et remplacer manuellement par des blocs du nouveau thème
  2. Live : accepter les blocs orphelins (les <div> vides sont CSS-invisibles par défaut)
  3. Migration scripted : si vous proposez un upgrade path d'un de vos thèmes à un autre, écrivez un script qui remplace les marqueurs anciens par les marqueurs nouveaux

Blocs et page editor

L'éditeur de l'admin charge tous les blocs des plugins ET du thème actif. Donc les blocs de thème apparaissent dans le /-inserter sous leur namespace.

Si l'utilisateur insère un bloc puis bascule de thème, le bloc reste dans le markdown mais n'est plus dans l'inserter — il peut juste l'éditer via les boutons de la barre de bloc (déplacer / dupliquer / supprimer).