Créer un thème externe — tutoriel

Ce guide parcourt l'authoring d'un thème Flexweg CMS distribué en et chargé au runtime — pas de rebuild de l'admin requis.

Ce guide parcourt l'authoring d'un thème Flexweg CMS distribué en .zip et chargé au runtime — pas de rebuild de l'admin requis.

Thème vs plugin

Les thèmes possèdent le rendu côté public : chaque page HTML publiée sur le site passe à travers les templates du thème actif (base, home, single, category, author, notFound). Les plugins se posent au-dessus — ils accrochent des filtres / actions et contribuent des blocs d'éditeur, cartes de tableau de bord, pages de réglages — mais ils ne décident pas à quoi ressemble le HTML publié.

Les thèmes externes utilisent le même shape de manifest que les thèmes in-tree. La différence est purement comment le bundle arrive à l'admin (un package zippé vs du code committé au repo).

Anatomie

my-theme/
├── manifest.json
├── package.json
├── tsconfig.json
├── vite.config.ts
├── scripts/pack.mjs
├── src/
│   ├── manifest.tsx         ← export default le ThemeManifest
│   ├── theme.css            ← CSS à uploader sur Flexweg
│   ├── templates/
│   │   ├── BaseLayout.tsx
│   │   ├── HomeTemplate.tsx
│   │   ├── SingleTemplate.tsx
│   │   ├── CategoryTemplate.tsx
│   │   ├── AuthorTemplate.tsx
│   │   └── NotFoundTemplate.tsx
│   ├── components/          ← sous-composants partagés
│   └── types/cms-runtime.d.ts
└── README.md

Étape 1 : initier le projet

BASH
mkdir my-theme
cd my-theme
npm init -y

Éditez package.json :

JSON
{
  "name": "my-theme",
  "private": true,
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "build": "vite build && node scripts/pack.mjs"
  },
  "devDependencies": {
    "@types/react": "^18.3.28",
    "@vitejs/plugin-react": "^4.3.3",
    "jszip": "^3.10.1",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "react-i18next": "^15.2.0",
    "typescript": "^6.0.3",
    "vite": "^5.4.10"
  }
}
BASH
npm install --legacy-peer-deps

Étape 2 : la config Vite

vite.config.ts :

TS
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  define: {
    "process.env.NODE_ENV": JSON.stringify("production"),
  },
  build: {
    lib: {
      entry: "src/manifest.tsx",
      formats: ["es"],
      fileName: () => "bundle.js",
    },
    outDir: "dist",
    rollupOptions: {
      external: [
        "react",
        "react/jsx-runtime",
        "react-dom",
        "react-dom/client",
        "react-i18next",
        "@flexweg/cms-runtime",
      ],
      output: {
        inlineDynamicImports: true,
      },
    },
  },
});

Étape 3 : le manifest.json

JSON
{
  "id": "my-theme",
  "name": "My Theme",
  "version": "1.0.0",
  "apiVersion": "1.3.0",
  "entry": "bundle.js"
}

Étape 4 : les six templates

Voir Construire un bundle thème externe pour le code complet de chaque template.

Le BaseLayout est le plus critique — il doit contenir les deux sentinels (x-cms-head-extra, application/x-cms-body-end). Voir Vue d'ensemble.

Étape 5 : la CSS

src/theme.css :

CSS
:root {
  --color-primary: #3366ff;
  --color-text: #1a1a1a;
}

.mt-container {
  max-width: 1100px;
  margin: 0 auto;
  padding: 1rem;
}

.mt-article {
  font-family: system-ui, sans-serif;
  color: var(--color-text);
}

/* etc. */

Préfixez avec un namespace court (.mt- pour my-theme) pour éviter les collisions avec d'autres thèmes installés.

Étape 6 : le src/manifest.tsx

TSX
import cssText from "./theme.css?raw";
import { BaseLayout } from "./templates/BaseLayout";
import { HomeTemplate } from "./templates/HomeTemplate";
import { SingleTemplate } from "./templates/SingleTemplate";
import { CategoryTemplate } from "./templates/CategoryTemplate";
import { AuthorTemplate } from "./templates/AuthorTemplate";
import { NotFoundTemplate } from "./templates/NotFoundTemplate";

const manifest = {
  id: "my-theme",
  name: "My Theme",
  version: "1.0.0",
  description: "Un thème minimal d'exemple",
  cssText,
  templates: {
    base: BaseLayout,
    home: HomeTemplate,
    single: SingleTemplate,
    category: CategoryTemplate,
    author: AuthorTemplate,
    notFound: NotFoundTemplate,
  },
};

export default manifest;

Étape 7 : scripts/pack.mjs

JS
import { createWriteStream, readFileSync, existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import JSZip from "jszip";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const manifest = JSON.parse(readFileSync(resolve(root, "manifest.json"), "utf-8"));

const zip = new JSZip();
zip.file("manifest.json", readFileSync(resolve(root, "manifest.json")));
zip.file("bundle.js", readFileSync(resolve(root, "dist/bundle.js")));
zip.file("theme.css", readFileSync(resolve(root, "src/theme.css")));
const readme = resolve(root, "README.md");
if (existsSync(readme)) zip.file("README.md", readFileSync(readme));

zip
  .generateNodeStream({ type: "nodebuffer", streamFiles: true })
  .pipe(createWriteStream(resolve(root, `${manifest.id}.zip`)))
  .on("finish", () => console.log(`Packed: ${manifest.id}.zip`));

Étape 8 : build + test

BASH
npm run build
# → my-theme.zip dans le répertoire racine

Dans votre admin de staging :

  1. Ouvrez /admin/themes
  2. Cliquez Install theme
  3. Drag le ZIP
  4. L'admin valide, upload, dynamic-imports, reload

Si l'install passe, votre thème apparaît dans la liste. Cliquez Activer pour le rendre actif → cliquez Regenerate site → All HTML pages pour re-rendre tout le site avec votre thème.

Étape 9 : itérer

Modifiez le code → npm run build → drag le nouveau ZIP sur l'admin → l'upgrade in-place se fait automatiquement. Cliquez Regenerate pour voir les changements en prod.

Étape 10 : ajouter une page de réglages

Une fois le thème de base fonctionnel, ajoutez une page de réglages pour exposer des couleurs / polices ajustables. Voir Page de réglages thème.

N'oubliez pas compileCss(config) pour que les overrides utilisateur soient réellement appliqués à la CSS uploadée.

Distribution

Distribuez le ZIP comme bon vous semble : GitHub release, site web, marketplace tiers, etc. L'utilisateur final juste télécharge le ZIP et l'install via le bouton Install theme.

Pour aller plus loin