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
mkdir my-theme
cd my-theme
npm init -y
Éditez package.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"
}
}
npm install --legacy-peer-deps
Étape 2 : la config Vite
vite.config.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
{
"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 :
: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
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
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
npm run build
# → my-theme.zip dans le répertoire racine
Dans votre admin de staging :
- Ouvrez
/admin/themes - Cliquez Install theme
- Drag le ZIP
- 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
- Référence du manifest thème — toutes les options
- Pipelines CSS — SCSS vs Tailwind si vous voulez un pipeline plus riche
- Variantes d'image — déclarer les formats que consomme votre thème
- Blocs de thème — pour des sections riches façon hero, services-grid, etc.