Chapitre 8.2 — Tutoriel : créer theme_maboite, enfant de Boost, de A à Z
Partie 8 : Les thèmes — Chapitre 2/5 Public : dev front. On applique le modèle du 8.1 : on construit un vrai thème enfant de Boost, chaque fichier donné en entier. Version de référence : Moodle 5.2 (Boost / Bootstrap 5.3, scssphp).
⏱️ TL;DR
- On construit
theme_maboite, enfant de Boost pour la société fictive « MaBoîte » : couleur de marque#5940c1, fonte Inter.- Fichiers :
version.php,config.php(parents = boost),lib.php(callbacks SCSS : main / pre / extra),settings.php(preset picker, couleur de marque, logo, boîtes SCSS brutes),scss/(preset + variables),lang/,pix/(logo, favicon).- La couleur de marque est un réglage admin (
brandcolor) injecté dans le pre-SCSS comme$primary→ tout le site se re-teinte.- Inter est hébergée localement (pas de CDN, RGPD) et déclarée via
@font-facedans le SCSS.- Un preset picker permet de choisir le fichier de preset (clair/sombre/défaut) ; les boîtes SCSS brutes (raw pre/post) laissent l’admin ajouter du SCSS sans coder.
- On personnalise la page de connexion, un footer sur mesure, le favicon/logo.
- Itération :
themedesignermodeON, purge des caches,git/upload, on regarde.
🎯 Objectifs
- Écrire tous les fichiers d’un thème enfant fonctionnel.
- Brancher un réglage couleur de marque sur
$primaryvia le pre-SCSS. - Héberger une fonte locale (Inter) proprement.
- Offrir un preset picker et des boîtes SCSS à l’admin.
- Personnaliser login, footer, logo/favicon.
- Installer et itérer efficacement.
1. Le brief et l’arborescence
MaBoîte veut son identité : primaire #5940c1 (violet), fonte Inter, logo et favicon maison, page de connexion et footer personnalisés. On reste enfant de Boost (ch. 8.1) : on ne surcharge que le delta.
public/theme/maboite/
├── version.php
├── config.php
├── lib.php
├── settings.php
├── scss/
│ ├── preset/
│ │ └── default.scss
│ ├── pre.scss
│ └── post.scss
├── pix/
│ ├── logo.svg
│ ├── logo-compact.svg
│ └── favicon.ico
├── fonts/
│ ├── Inter-Regular.woff2
│ ├── Inter-Medium.woff2
│ └── Inter-Bold.woff2
└── lang/
└── en/
└── theme_maboite.php2. version.php
<?php
// public/theme/maboite/version.php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'theme_maboite';
$plugin->version = 2026070800;
$plugin->requires = 2026042000; // Moodle 5.2
$plugin->maturity = MATURITY_STABLE;
$plugin->release = 'v1.0';
$plugin->dependencies = [
'theme_boost' => 2026042000, // on dépend du thème parent
];⚠️ Piège : déclarez
theme_boosten dépendance. Votre thème hérite de Boost ; sans Boost installé (rare mais possible sur une instance minimale), il casserait. La dépendance rend le prérequis explicite.
3. config.php
<?php
// public/theme/maboite/config.php
defined('MOODLE_INTERNAL') || die();
$THEME->name = 'maboite';
// Héritage de Boost : on récupère tout (layouts, templates, renderers, SCSS).
$THEME->parents = ['boost'];
$THEME->sheets = []; // pas de CSS statique : tout en SCSS
$THEME->editor_sheets = [];
// SCSS principal : callback qui assemble preset + Boost (lib.php).
$THEME->scss = function($theme) {
return theme_maboite_get_main_scss_content($theme);
};
// Étages pre et extra du pipeline (ch. 8.1 §5).
$THEME->prescsscallback = 'theme_maboite_get_pre_scss';
$THEME->extrascsscallback = 'theme_maboite_get_extra_scss';
// Réglages hérités de Boost.
$THEME->layouts = []; // hérite des layouts de Boost
$THEME->requiredblocks = '';
$THEME->addblockposition = BLOCK_ADDBLOCK_POSITION_FLATNAV;
// Surcharge des renderers (ch. 8.3).
$THEME->rendererfactory = 'theme_overridden_renderer_factory';
$THEME->iconsystem = \core\output\icon_system::FONTAWESOME;
$THEME->haseditswitch = true;
$THEME->usefallback = true;
// Autorise le dark mode fourni par Boost.
$THEME->enable_dock = false;4. lib.php : les callbacks SCSS
Le fichier qui assemble le SCSS. Trois fonctions, une par point d’injection.
<?php
// public/theme/maboite/lib.php
defined('MOODLE_INTERNAL') || die();
/**
* SCSS principal : le preset choisi + le SCSS de Boost.
*
* @param theme_config $theme
* @return string
*/
function theme_maboite_get_main_scss_content($theme): string {
global $CFG;
$scss = '';
$filename = !empty($theme->settings->preset) ? $theme->settings->preset : 'default.scss';
$presetfile = "{$CFG->dirroot}/theme/maboite/scss/preset/{$filename}";
// On repart du SCSS de Boost (variables + Bootstrap + composants Moodle).
$boost = "{$CFG->dirroot}/theme/boost/scss/";
$scss .= file_get_contents("{$boost}/preset/default.scss"); // base Boost
// Puis notre preset (surcharges de haut niveau).
if (is_readable($presetfile)) {
$scss .= "\n" . file_get_contents($presetfile);
}
return $scss;
}
/**
* Pre-SCSS : VARIABLES injectées AVANT le preset (surcharge Bootstrap).
* C'est ici qu'on branche la couleur de marque sur $primary.
*
* @param theme_config $theme
* @return string
*/
function theme_maboite_get_pre_scss($theme): string {
$scss = '';
// Couleur de marque (réglage admin) → variable Bootstrap $primary.
$brand = get_config('theme_maboite', 'brandcolor');
if (!empty($brand)) {
$scss .= '$primary: ' . $brand . ";\n";
}
// Fonte de marque.
$scss .= '$font-family-sans-serif: "Inter", system-ui, -apple-system, sans-serif;' . "\n";
$scss .= '$border-radius: 0.5rem;' . "\n";
// Boîte « SCSS brut avant » laissée à l'admin (rawscsspre).
$rawpre = get_config('theme_maboite', 'scsspre');
if (!empty($rawpre)) {
$scss .= "\n" . $rawpre;
}
return $scss;
}
/**
* Extra-SCSS : RÈGLES injectées APRÈS le preset (surcharges finales).
*
* @param theme_config $theme
* @return string
*/
function theme_maboite_get_extra_scss($theme): string {
// @font-face pour héberger Inter localement + règles maison.
$fontface = theme_maboite_get_fontface_scss();
$extra = $fontface . "\n" . file_get_contents(
$GLOBALS['CFG']->dirroot . '/theme/maboite/scss/post.scss'
);
// Boîte « SCSS brut après » laissée à l'admin (rawscss).
$rawpost = get_config('theme_maboite', 'scss');
if (!empty($rawpost)) {
$extra .= "\n" . $rawpost;
}
return $extra;
}
/**
* Génère les @font-face pointant vers les woff2 hébergés dans le thème.
*/
function theme_maboite_get_fontface_scss(): string {
global $CFG;
$base = "{$CFG->wwwroot}/theme/maboite/fonts";
return "
@font-face {
font-family: 'Inter';
src: url('{$base}/Inter-Regular.woff2') format('woff2');
font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('{$base}/Inter-Medium.woff2') format('woff2');
font-weight: 500; font-style: normal; font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('{$base}/Inter-Bold.woff2') format('woff2');
font-weight: 700; font-style: normal; font-display: swap;
}
";
}Trois points clés :
brandcolor→$primarydans le pre-SCSS : le réglage admin devient la variable Bootstrap avant compilation → tout le site adopte la couleur (ch. 8.1 §5.1).- Inter hébergée localement :
@font-facepointe verstheme/maboite/fonts/*.woff2. Pas de Google Fonts CDN — c’est un enjeu RGPD (un CDN de fonte fuit l’IP des visiteurs à un tiers) et de performance/fiabilité. - Boîtes SCSS brutes (
scsspre,scss) : l’admin peut injecter du SCSS sans coder dans le thème, via les réglages (§6). Pratique pour des ajustements rapides en production.
⚠️ Piège de sécurité de fonte : n’utilisez pas
@import url('https://fonts.googleapis.com/...'). Outre le RGPD (IP des utilisateurs envoyée à Google), un CDN externe est un point de défaillance et de latence. Téléchargez les.woff2, hébergez-les danstheme/maboite/fonts/, et déclarez-les en@font-facelocal avecfont-display: swap.
5. Le SCSS : preset et post
scss/preset/default.scss — surcharges de haut niveau (au-delà des simples variables) :
// public/theme/maboite/scss/preset/default.scss
// Ajustements structurels qui ne sont pas de simples variables.
// (Le gros vient de Boost, chargé avant dans lib.php.)
.navbar {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}scss/post.scss — règles finales de marque :
// public/theme/maboite/scss/post.scss
// Le logo dans la barre de navigation.
.navbar-brand img {
max-height: 36px;
width: auto;
}
// Footer aux couleurs de marque.
#page-footer {
background: $primary; // $primary est disponible ici (compilé après Bootstrap)
color: #fff;
a { color: rgba(255, 255, 255, 0.85); }
a:hover { color: #fff; }
}
// Page de connexion : carte centrée, fond de marque.
body.pagelayout-login {
background: linear-gradient(135deg, $primary 0%, darken($primary, 15%) 100%);
.card {
border: none;
border-radius: 1rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
}scss/pre.scss (optionnel — on met surtout les variables dans le callback get_pre_scss, mais un fichier peut compléter) :
// public/theme/maboite/scss/pre.scss
// Variables statiques supplémentaires (non issues des réglages).
$enable-rounded: true;
$link-decoration: none;⚠️ Piège : dans
post.scss,$primaryest utilisable (Bootstrap l’a défini) — bien. Mais ne redéfinissez pas$primaryici (ça n’aurait aucun effet sur Bootstrap déjà compilé, ch. 8.1 §5.3). Utilisez-le, ne le réassignez pas.
6. settings.php : preset picker, couleur, logo, boîtes SCSS
<?php
// public/theme/maboite/settings.php
defined('MOODLE_INTERNAL') || die();
if ($ADMIN->fulltree) {
$settings = new theme_boost_admin_settingspage_tabs(
'themesettingmaboite',
get_string('configtitle', 'theme_maboite')
);
// --- Onglet « Général » ---
$page = new admin_settingpage('theme_maboite_general', get_string('generalsettings', 'theme_maboite'));
// 1. Preset picker : choisir le fichier de preset.
$name = 'theme_maboite/preset';
$title = get_string('preset', 'theme_maboite');
$description = get_string('preset_desc', 'theme_maboite');
$default = 'default.scss';
$choices = ['default.scss' => 'Default', 'dark.scss' => 'Dark'];
$setting = new admin_setting_configselect($name, $title, $description, $default, $choices);
$setting->set_updatedcallback('theme_reset_all_caches'); // recompile au changement
$page->add($setting);
// 2. Couleur de marque → $primary.
$name = 'theme_maboite/brandcolor';
$title = get_string('brandcolor', 'theme_maboite');
$description = get_string('brandcolor_desc', 'theme_maboite');
$setting = new admin_setting_configcolourpicker($name, $title, $description, '#5940c1');
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
// 3. Logo.
$name = 'theme_maboite/logo';
$title = get_string('logo', 'theme_maboite');
$description = get_string('logo_desc', 'theme_maboite');
$setting = new admin_setting_configstoredfile($name, $title, $description, 'logo',
0, ['maxfiles' => 1, 'accepted_types' => ['.png', '.svg', '.jpg']]);
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
$settings->add($page);
// --- Onglet « SCSS avancé » ---
$page = new admin_settingpage('theme_maboite_scss', get_string('advancedsettings', 'theme_maboite'));
// 4. SCSS brut AVANT (variables).
$setting = new admin_setting_scsscode('theme_maboite/scsspre',
get_string('rawscsspre', 'theme_maboite'),
get_string('rawscsspre_desc', 'theme_maboite'), '', PARAM_RAW);
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
// 5. SCSS brut APRÈS (règles).
$setting = new admin_setting_scsscode('theme_maboite/scss',
get_string('rawscss', 'theme_maboite'),
get_string('rawscss_desc', 'theme_maboite'), '', PARAM_RAW);
$setting->set_updatedcallback('theme_reset_all_caches');
$page->add($setting);
$settings->add($page);
}Chaque réglage appelle theme_reset_all_caches à la mise à jour : sans ça, changer la couleur ne recompilerait pas le CSS caché. admin_setting_configcolourpicker offre un sélecteur de couleur ; admin_setting_scsscode un éditeur SCSS avec coloration.
💡 Pour un dev React : c’est un panneau de thème auto-généré — un peu comme un « theme editor » de design system, mais côté serveur. Le
set_updatedcallback('theme_reset_all_caches')est le « bust cache on token change » : modifier un token (couleur) invalide le CSS compilé pour qu’il soit régénéré avec la nouvelle valeur.
7. Les chaînes de langue
<?php
// public/theme/maboite/lang/en/theme_maboite.php
defined('MOODLE_INTERNAL') || die();
$string['pluginname'] = 'MaBoîte';
$string['choosereadme'] = 'MaBoîte is a child theme of Boost with brand colours and Inter font.';
$string['configtitle'] = 'MaBoîte settings';
$string['generalsettings'] = 'General';
$string['advancedsettings'] = 'Advanced';
$string['preset'] = 'Theme preset';
$string['preset_desc'] = 'Pick a preset to broadly change the look.';
$string['brandcolor'] = 'Brand colour';
$string['brandcolor_desc'] = 'The primary colour of the site.';
$string['logo'] = 'Logo';
$string['logo_desc'] = 'Logo shown in the navigation bar.';
$string['rawscsspre'] = 'Raw initial SCSS';
$string['rawscsspre_desc'] = 'SCSS injected before everything (variables).';
$string['rawscss'] = 'Raw SCSS';
$string['rawscss_desc'] = 'SCSS injected at the end (rules).';
$string['region-side-pre'] = 'Right';⚠️ Piège : la clé
region-side-pre(et les autres régions de blocs) doit exister, sinon Moodle affiche[[region-side-pre]]là où il nomme les régions. Reprenez les clés de région de Boost.
8. Page de connexion, footer, logo, favicon
- Page de connexion : personnalisée en SCSS (
body.pagelayout-logindanspost.scss, §5). Pour aller plus loin (markup), on surcharge le template de layout login (ch. 8.3). - Footer : stylé en SCSS (
#page-footer). Pour changer son contenu, on surcharge le template/layout (ch. 8.3). - Logo : le réglage
logo(stored file) est lu par Boost via$OUTPUT->get_logo_url()dans ses templates hérités — il s’affiche automatiquement dans la navbar. Un logo « compact » (pix/logo-compact.svg) sert aux petits écrans. - Favicon : placez
pix/favicon.ico; Boost le sert via son mécanisme de favicon (surchargeable). Vous pouvez aussi le régler globalement (Apparence).
💡 Pour un dev React : remarquez qu’on obtient login + footer + logo sans écrire de HTML — uniquement en SCSS + réglages, parce qu’on hérite des templates de Boost. C’est le pouvoir de l’héritage (ch. 8.1) : le delta visuel se fait par tokens/CSS ; on ne touche au HTML (templates) que quand le CSS ne suffit pas (ch. 8.3).
9. Installer et itérer
# 1. Déposer le thème
git clone <repo> public/theme/maboite # ou copier les fichiers
# 2. Installer (détecte le nouveau plugin)
php admin/cli/upgrade.php --non-interactive
# 3. Activer le thème : Apparence → Sélecteur de thèmes → « MaBoîte »
# (ou en CLI :)
php admin/cli/cfg.php --name=theme --set=maboite
# 4. Mode dev pour itérer sur le SCSS
# Apparence → Réglages des thèmes → Activer themedesignermode
# (ou $CFG->themedesignermode = true; dans config.php)
# 5. À chaque changement SCSS, purger si designer mode OFF
php admin/cli/purge_caches.phpBoucle d’itération recommandée : themedesignermode ON en dev → éditez le SCSS → rechargez (CSS recompilé à chaque page) → une fois satisfait, désactivez themedesignermode et purgez pour repasser en mode caché (prod).
⚠️ Piège : ne laissez jamais
themedesignermodeactivé en production — chaque page recompilerait le SCSS (lent, coûteux). C’est un mode dev uniquement.
10. Récapitulatif
Vous avez un thème enfant complet : identité (version/config), pipeline SCSS branché sur des réglages (couleur, preset, logo, boîtes SCSS), fonte locale, login/footer/logo personnalisés — sans une seule ligne de HTML grâce à l’héritage de Boost. La couleur de marque #5940c1 se propage partout via $primary, Inter est servie localement (RGPD), et l’admin peut ajuster sans coder.
Ce qui manque encore : changer le HTML (markup) de composants précis, surcharger des renderers PHP, créer des layouts custom. C’est exactement l’objet du chapitre suivant — le vrai pouvoir (et le vrai coût) des thèmes.
✏️ Exercices
Exercice 1 — La couleur ne change pas.
Vous réglez brandcolor sur #e91e63 mais le site reste violet. Le réglage est bien enregistré. Deux causes ?
✅ Solution
(1) Cache du thème non invalidé : sans set_updatedcallback('theme_reset_all_caches') sur le réglage (ou sans purge), le CSS compilé garde l’ancienne valeur. Vérifiez le callback dans settings.php et/ou purgez (purge_caches) ; en dev, activez themedesignermode. (2) brandcolor non injecté dans le pre-SCSS : vérifiez que theme_maboite_get_pre_scss lit bien get_config('theme_maboite', 'brandcolor') et écrit $primary: <valeur>; avant le preset. Si $primary est mis ailleurs (post), ça n’a aucun effet.
Exercice 2 — Fonte et RGPD.
Un collègue ajoute Inter via @import url('https://fonts.googleapis.com/css2?family=Inter') dans le SCSS. Pourquoi refuser, et quelle est la bonne méthode ?
✅ Solution
Refuser car : (1) RGPD — charger la fonte depuis un CDN Google transmet l’IP (et des métadonnées) de chaque visiteur à un tiers, sans consentement, ce qui est problématique en Europe ; (2) performance/fiabilité — dépendance à un domaine externe (latence, point de panne). Bonne méthode : télécharger les fichiers .woff2, les héberger dans theme/maboite/fonts/, et les déclarer en @font-face local avec font-display: swap (comme dans theme_maboite_get_fontface_scss). La fonte est alors servie par votre propre domaine.
Exercice 3 — pre vs post. Vous voulez (a) changer le rayon des bordures globalement et (b) donner un fond dégradé au footer. Dans quel étage SCSS chaque changement va-t-il ?
✅ Solution
(a) Variable $border-radius → pre-SCSS (get_pre_scss), avant Bootstrap, pour que tous les composants héritent du nouveau rayon. (b) Règle CSS ciblant #page-footer avec un linear-gradient → extra/post-SCSS (post.scss via get_extra_scss), après Bootstrap, où l’on peut aussi utiliser $primary. Règle constante : variables en pre, règles en post.
Exercice 4 — Boîte SCSS pour l’admin.
À quoi servent les réglages scsspre et scss (boîtes SCSS brutes), et quel risque ?
✅ Solution
Ils permettent à l’administrateur d’injecter du SCSS (variables via scsspre, règles via scss) sans modifier le code du thème — pratique pour des ajustements rapides en production ou par un client non-développeur. Risque : du SCSS invalide saisi par l’admin peut casser la compilation de tout le CSS (page sans style). En dev on teste ; en prod, on documente et on restreint qui a accès à ces champs (capability de config du thème). C’est un couteau à double tranchant : puissant mais dangereux.
Exercice 5 — Sans HTML ? Expliquez comment login, footer et logo sont personnalisés dans ce tutoriel sans écrire de HTML, et à partir de quand il faudra en écrire.
✅ Solution
Grâce à l’héritage de Boost : les templates/layouts de login, footer et navbar viennent de Boost. On personnalise leur apparence en SCSS (body.pagelayout-login, #page-footer) et via des réglages (logo comme stored file lu par les templates hérités). Aucun template à réécrire. Il faudra écrire du HTML (surcharger un template Mustache ou un layout, ch. 8.3) dès qu’on veut changer la structure (markup) et pas seulement le style : par exemple ajouter des liens/colonnes dans le footer, ou repenser la disposition de la page de connexion.
🧠 Quiz de révision
Q1. Comment la couleur de marque (réglage admin) re-teinte-t-elle tout le site ?
Réponse
get_pre_scss lit get_config('theme_maboite', 'brandcolor') et l’écrit comme $primary: <valeur>; dans le pre-SCSS, avant la compilation de Bootstrap. Bootstrap dérive alors tous ses composants de cette valeur → tout le site adopte la couleur.Q2. Pourquoi et comment héberge-t-on la fonte localement ?
Réponse
.woff2, on les place dans theme/maboite/fonts/, et on les déclare en @font-face local (avec font-display: swap) injecté dans l’extra-SCSS.Q3. À quoi sert set_updatedcallback('theme_reset_all_caches') sur un réglage de thème ?
Réponse
Q4. Que sont le preset picker et les boîtes SCSS brutes dans les réglages ?
Réponse
admin_setting_configselect) laisse choisir le fichier de preset (ex. clair/sombre) qui change globalement le look. Les boîtes SCSS brutes (admin_setting_scsscode pour scsspre/scss) laissent l’admin injecter du SCSS (variables/règles) sans modifier le code — puissant mais risqué (un SCSS invalide casse la compilation).Q5. Pourquoi ce thème n’a-t-il (presque) pas de HTML ?
Réponse
Chapitre suivant : 03 — Surcharges : templates Mustache, renderers PHP, layouts — le cœur du pouvoir des thèmes : surcharger n’importe quel template de n’importe quel composant (ordre de résolution exact), surcharger les renderers PHP via la theme_overridden_renderer_factory, écrire ses propres layouts (drawers, régions de blocs), l’icon map FontAwesome, et ajouter du JS (AMD) dans un thème.