Skip to Content
MoodlePartie 8 — Les thèmes Moodle : le guide completTutoriel : créer `theme_maboite`, enfant de Boost, de A à Z

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-face dans 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 : themedesignermode ON, 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 $primary via 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.php

2. 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_boost en 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$primary dans 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-face pointe vers theme/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 dans theme/maboite/fonts/, et déclarez-les en @font-face local avec font-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, $primary est utilisable (Bootstrap l’a défini) — bien. Mais ne redéfinissez pas $primary ici (ç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.


  • Page de connexion : personnalisée en SCSS (body.pagelayout-login dans post.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.php

Boucle 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 themedesignermode activé 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-radiuspre-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-gradientextra/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

Le callback 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

Pour le RGPD (ne pas fuiter l’IP des visiteurs à un CDN tiers) et la fiabilité/performance. On télécharge les .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

À invalider le CSS compilé quand le réglage change : sans lui, modifier la couleur/preset/logo ne recompilerait pas le SCSS caché et le changement resterait invisible. C’est le « bust cache on token change ».

Q4. Que sont le preset picker et les boîtes SCSS brutes dans les réglages ?

Réponse

Le preset picker (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

Parce qu’il hérite de Boost : tous les templates et layouts viennent du parent. On ne personnalise que l’apparence (SCSS + réglages). On n’écrit du HTML (surcharge de templates/layouts, ch. 8.3) que lorsqu’il faut changer la structure d’un composant, pas seulement son style.

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.