Construire un bundle thème externe
Les thèmes externes sont livrés en contenant un bundle ESM pré-compilé, uploadés dans Flexweg via l'UI Install theme, et chargés au runtime via dynamic — pas besoin de rebuild de l'admin.
Les thèmes externes sont livrés en .zip contenant un bundle ESM pré-compilé, uploadés dans Flexweg via l'UI Install theme, et chargés au runtime via dynamic import() — pas besoin de rebuild de l'admin.
C'est la façon de distribuer des thèmes quand vous ne maintenez pas votre propre fork.
Exemple de référence
examples/external-theme/ est un thème minimaliste complet — clonez-le, renommez-le, customisez-le. La config de build + le packaging ZIP sont déjà en place.
Structure
my-theme/
├── manifest.json ← métadonnées d'installation
├── package.json
├── tsconfig.json
├── vite.config.ts ← même config que les plugins externes
├── scripts/pack.mjs ← zippe aussi theme.css
├── src/
│ ├── manifest.tsx
│ ├── theme.css ← CSS compilée importée via ?raw
│ ├── templates/
│ │ ├── BaseLayout.tsx ← CRITIQUE : contient les sentinels
│ │ ├── HomeTemplate.tsx
│ │ ├── SingleTemplate.tsx
│ │ ├── CategoryTemplate.tsx
│ │ ├── AuthorTemplate.tsx
│ │ └── NotFoundTemplate.tsx
│ └── types/
│ └── cms-runtime.d.ts
└── README.md
manifest.json
Identique aux plugins externes :
{
"id": "my-theme",
"name": "My Theme",
"version": "1.0.0",
"apiVersion": "1.3.0",
"entry": "bundle.js"
}
L'admin lit ce fichier à l'install + à chaque boot.
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: "Pour quoi ce thème est conçu en une phrase.",
cssText,
templates: {
base: BaseLayout,
home: HomeTemplate,
single: SingleTemplate,
category: CategoryTemplate,
author: AuthorTemplate,
notFound: NotFoundTemplate,
},
};
export default manifest;
vite.config.ts
Identique aux plugins externes. Voir Construire un bundle plugin externe. Les trois pièces critiques sont les mêmes : define, external, inlineDynamicImports.
src/templates/BaseLayout.tsx — la partie CRITIQUE
import type { BaseLayoutProps } from "../types/cms-runtime";
export function BaseLayout({
site,
pageTitle,
pageDescription,
ogImage,
currentPath,
children,
}: BaseLayoutProps) {
const lang = site.settings.language ?? "en";
return (
<html lang={lang}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{pageTitle}</title>
{pageDescription && <meta name="description" content={pageDescription} />}
{ogImage && <meta property="og:image" content={ogImage} />}
<link rel="stylesheet" href={`/theme-assets/my-theme.css`} />
{/* SENTINEL OBLIGATOIRE — les plugins injectent des head tags ici */}
<meta name="x-cms-head-extra" />
</head>
<body>
{children}
{/* SENTINEL OBLIGATOIRE — les plugins injectent des scripts body-end ici */}
<script type="application/x-cms-body-end" />
</body>
</html>
);
}
Sans ces deux sentinels, les plugins comme core-seo, flexweg-favicon, flexweg-custom-code silencieusement no-op sur votre thème.
Les templates reçoivent des props serializables
Les composants de thème doivent être purs / consumers de props serializables — pas de hooks Firestore, pas de context admin. Le publisher résout les URLs, MediaView shapes, ResolvedMenuItems, etc. avant le rendu. Copiez les types canoniques du in-tree src/themes/types.ts dans votre src/types/cms-runtime.d.ts pour que le bundle soit self-contained.
Pipeline CSS — votre choix
L'admin upload manifest.cssText verbatim à /theme-assets/<id>.css. Votre pipeline CSS doit juste produire une string au build :
- CSS pure : écrivez
theme.css,import cssText from "./theme.css?raw". Plus simple. - Tailwind : pré-buildez avec
tailwindcss -i src/theme.css -o dist/theme.cssavantvite build, puis importez le fichier dist. - SCSS :
import cssText from "./theme.scss?raw"— Vite supporte Sass inline.
Scopez votre CSS avec un préfixe de classe unique (ex. .mt- pour my-theme) pour éviter les collisions quand les utilisateurs basculent de thème.
Customisation live avec compileCss
Si les réglages de votre thème exposent des couleurs / polices / spacing éditables, intégrez-les dans la CSS uploadée via compileCss :
manifest.compileCss = (config: MyThemeConfig) => {
const overrides = `:root {\n --my-primary: ${config.primaryColor};\n}\n`;
return cssText + "\n\n" + overrides;
};
L'admin appelle ceci chaque fois que theme-assets/<id>.css est uploadé (bouton Sync theme assets + save de la page de réglages du thème). Sans compileCss, chaque sync écrase les overrides utilisateur.
Build + install
npm install --legacy-peer-deps
npm run build # → my-theme.zip
Puis dans l'admin : /admin/themes → Install theme → drag le ZIP. L'admin valide, upload, dynamic-imports, reload.
scripts/pack.mjs (avec CSS)
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`));
Le thème externe ship theme.css séparément pour que l'admin puisse l'uploader à theme-assets/<id>.css dès l'install (avant le premier sync).
Tutoriel pas-à-pas
Voir Créer un thème externe — tutoriel pour un exemple complet bout-à-bout.