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 :

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) : patche settings.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.

TSX
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 :

TS
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 :

TS
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é :

TSX
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 :

TS
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 :

TSX
function HomeTemplate({ site, posts }) {
  const themeConfig = site.themeConfig as MyThemeConfig;
  const showPinned = themeConfig?.home?.showPinned ?? true;
  // ...
}