Page de réglages thème
Un thème peut livrer une page de réglages accessible à quand le thème est actif. Utilisez-la pour exposer des leviers de customisation — couleurs, polices, logo, toggles de mise en page — sans forcer
Un thème peut livrer une page de réglages accessible à /theme-settings quand le thème est actif. Utilisez-la pour exposer des leviers de customisation — couleurs, polices, logo, toggles de mise en page — sans forcer l'utilisateur à forker le thème.
Pourquoi en faire une
Si votre thème va être utilisé par quelqu'un d'autre que vous (ou par vous sur plusieurs sites), la customisation runtime bat le fork. Sans page de réglages :
- Chaque changement demande d'éditer le code source
- Chaque site a besoin de son propre fork pour customiser
- Les updates upstream demandent un merge manuel
Avec une page de réglages :
- L'utilisateur ajuste palette / polices / blocs depuis l'admin
- Les updates upstream s'appliquent sans toucher au site
- Un seul code source pour N sites
Déclarer une page de réglages
Dans manifest.ts :
import { MyThemeSettingsPage, DEFAULT_CONFIG } from "./SettingsPage";
import type { MyThemeConfig } from "./SettingsPage";
const manifest: ThemeManifest<MyThemeConfig> = {
// …
settings: {
navLabelKey: "settings.title",
defaultConfig: DEFAULT_CONFIG,
component: MyThemeSettingsPage,
},
compileCss: (config) => {
// Génère la CSS finale avec les overrides utilisateur
return buildCustomCss(cssText, config.style);
},
};
Le composant reçoit { config, save } :
config: merge{ ...defaultConfig, ...stored }save(nextConfig): patchesettings.themeConfigs[<theme-id>]puis lance le sync des theme assets
Routage et UI
/theme-settings (pas /settings/plugin/...) — la page de réglages du thème est top-level. La sidebar admin affiche une entrée Theme settings quand le thème actif a settings déclaré.
Pattern : tabs internes
Pour des configs riches, structurez en onglets internes. Le thème default expose 4 onglets : Home, Single, Sidebar, Style.
const [tab, setTab] = useState<"home" | "single" | "sidebar" | "style">("home");
return (
<div>
<div className="tab-strip">
<button onClick={() => setTab("home")}>{t("tabs.home")}</button>
<button onClick={() => setTab("single")}>{t("tabs.single")}</button>
<button onClick={() => setTab("sidebar")}>{t("tabs.sidebar")}</button>
<button onClick={() => setTab("style")}>{t("tabs.style")}</button>
</div>
{tab === "home" && <HomeTab home={draft.home} onChange={onHomeChange} />}
{tab === "single" && <SingleTab single={draft.single} onChange={onSingleChange} />}
{tab === "sidebar" && <SidebarTab sidebar={draft.sidebar} onChange={onSidebarChange} />}
{tab === "style" && <StyleTab style={draft.style} onChange={onStyleChange} />}
</div>
);
Pattern : style overrides
Pour exposer des palettes / polices ajustables, le pattern type :
interface MyThemeStyle {
vars: Record<string, string>; // ex. {"--color-primary": "#3366ff"}
fontHeadline: string; // nom Google Font
fontBody: string;
}
// SettingsPage.tsx
function StyleTab({ style, onChange }) {
return (
<div>
{THEME_VAR_SPECS.map((spec) => (
<Field key={spec.name} label={t(spec.labelKey)}>
{spec.type === "color" ? (
<input
type="color"
value={style.vars[spec.name] || spec.defaultValue}
onChange={(e) => onChange({
...style,
vars: { ...style.vars, [spec.name]: e.target.value },
})}
/>
) : (
<input
type="text"
value={style.vars[spec.name] || ""}
onChange={(e) => onChange({
...style,
vars: { ...style.vars, [spec.name]: e.target.value },
})}
/>
)}
</Field>
))}
<FontSelect
value={style.fontHeadline}
onChange={(font) => onChange({ ...style, fontHeadline: font })}
/>
{/* ... */}
</div>
);
}
compileCss — c'est obligatoire si vous exposez des overrides
Sans compileCss, chaque sync des theme assets upload cssText brute (sans overrides). Les changements utilisateur seraient effacés au prochain sync.
Le compileCss(config) est la fonction qui prend les overrides + la baseline et produit la CSS finale :
function buildCustomCss(baseCssText: string, style: MyThemeStyle): string {
let css = baseCssText;
// 1. Swap font @import line
css = css.replace(
/@import\s+url\("https:\/\/fonts\.googleapis\.com[^"]+"\);/,
`@import url("https://fonts.googleapis.com/css2?family=${encodeURIComponent(style.fontHeadline)}&family=${encodeURIComponent(style.fontBody)}&display=swap");`
);
// 2. Append :root { ... } with var overrides
const varEntries = Object.entries(style.vars)
.filter(([k, v]) => v && v !== getDefault(k)) // skip defaults
.map(([k, v]) => ` ${k}: ${v};`)
.join("\n");
if (varEntries) {
css += `\n\n:root {\n${varEntries}\n}\n`;
}
return css;
}
Auto-resync après save
Quand l'utilisateur clique Save, le pattern recommandé :
async function handleSave() {
setSaving(true);
try {
await save(draft);
// applyAndUploadCustomCss = wrapper qui appelle compileCss + uploadFile
await applyAndUploadCustomCss({ themeId, baseCssText: cssText, style: draft.style });
toast.success(t("actions.saved"));
} finally {
setSaving(false);
}
}
Sans le re-upload de la CSS, l'utilisateur doit cliquer manuellement sur Sync theme assets dans la page Themes pour voir ses changements en ligne — frustrant.
i18n
navLabelKey pointe vers une clé i18n résolue depuis le namespace du thème (theme-<id>).
Bundles dans manifest.i18n :
i18n: {
en: { settings: { title: "Theme settings", ... } },
fr: { settings: { title: "Réglages du thème", ... } },
},
Dans le composant : useTranslation(\theme-${themeId}`)ou directementuseTranslation("theme-marketplace-core")`.
Stockage : themeConfigs
settings/site.themeConfigs[<theme-id>] est l'emplacement persisté. Survit aux basculements de thème — si vous re-basculez vers ce thème plus tard, ses réglages sont restaurés.
updateThemeConfig(themeId, config) est le dispatcher qui écrit (Firestore ou SQLite selon backend).
Exposé au runtime
Au moment du rendu d'un template, site.themeConfig est la config résolue (merge defaults + stored). Donc dans vos templates :
function HomeTemplate({ site, posts }) {
const themeConfig = site.themeConfig as MyThemeConfig;
const showPinned = themeConfig?.home?.showPinned ?? true;
// ...
}