---
title: "Personnaliser les gabarits du thème"
description: "Modifie les gabarits du thème d'Éclair (layout.php, single.php, list.php…) en respectant la CSP, les mises à jour et le contrôle d'intégrité."
url: "https://eclair.academy/parcours/expert/personnaliser-theme"
lang: "fr"
author: "Thierry Gustin"
published: "2026-11-02"
updated: "2026-11-02"
---

# Personnaliser les gabarits du thème

> **L’essentiel** — Les gabarits du thème sont les fichiers PHP d'app/theme (layout.php, single.php, list.php…) qui reçoivent un tableau $v et produisent le HTML public. Préfère le CSS personnalisé quand il suffit, car il est rangé dans data/ et survit aux mises à jour. Un gabarit modifié doit respecter la CSP, sans script ni style en ligne, et se réapplique après chaque mise à jour.

Les gabarits du thème décident du HTML de chaque page publique : en-tête, menus, article, listes, fiche produit. Les modifier ouvre tout ce que le CSS personnalisé ne permet pas, au prix d'une maintenance à chaque mise à jour. Cette leçon détaille les fichiers d'`app/theme`, les variables reçues, les contraintes de sécurité et l'exemple de cette académie.

> **Avant de commencer** — Durée : 40 minutes. Prérequis : PHP et HTML, un site de test en local ([Développement local et tests](https://eclair.academy/parcours/expert/developpement-local)), la leçon [L'architecture d'Éclair](https://eclair.academy/parcours/expert/architecture) pour le cycle de rendu, et [Le CSS personnalisé](https://eclair.academy/parcours/intermediaire/css-personnalise) pour savoir ce qu'il permet déjà.

## Ce que tu sauras faire

### 🎨 Choisir le bon niveau

CSS personnalisé, feuille du thème ou gabarit PHP.

### 🛠 Modifier un gabarit

Avec les variables de $v et les fonctions d'Éclair.

### 🛡 Rester compatible

CSP stricte, mises à jour et contrôle d'intégrité.

## CSS personnalisé ou gabarits du thème ?

Commence toujours par la solution la plus légère. Le CSS personnalisé couvre la grande majorité des retouches, et le `<body>` porte des classes qui permettent de cibler un contexte sans toucher au PHP : `theme-<nom>`, `hdr-center`, `cards-list`, puis `is-post`, `is-page`, `is-wide`, `is-list`, `is-shop`, `is-product`, `is-search` ou `is-404` selon la page.

| Critère | CSS personnalisé | Gabarits du thème |
| --- | --- | --- |
| Où | **Réglages › Apparence › CSS personnalisé** | fichiers d'`app/theme/` |
| Rangé dans | `data/custom-css.php` | `app/`, remplacé à chaque mise à jour |
| Après une mise à jour | conservé | à réappliquer |
| Contrôle d'intégrité | rien à signaler | fichiers « modifiés » ou « inconnus » |
| Limites | 64 Ko, sans `<` ni `@import` | HTML libre, dans le respect de la CSP |
| Idéal pour | couleurs, espacements, style d'un bloc | structure du HTML, nouveaux éléments |

## Les fichiers d'app/theme

| Fichier | Rôle | Variables principales de `$v` |
| --- | --- | --- |
| `layout.php` | squelette commun : `<head>`, en-tête, menus, pied de page | `lang`, `head`, `css`, `content`, `current`, `id`, `class` |
| `single.php` | page ou article | `item`, `html`, `toc`, `author`, `category`, `crumbs`, `related`, `translations`, `featured`, `words`, `h1_in_content` |
| `list.php` | blog, catégorie, auteur, recherche, catalogue | `title`, `description`, `entries`, `crumbs`, `page`, `pages`, `prev`, `next`, `query`, `empty`, `chips` |
| `cards.php`, `products.php` | grilles de cartes d'articles et de produits | `entries`, `lang`, `level` |
| `product.php` | fiche produit | `item`, `entry`, `html`, `images`, `offers`, `crumbs`, `related` |
| `404.php` | page introuvable | `lang` |
| `theme.css` | styles de base, insérés dans chaque page | repères `/*dark*/` et `/*shop*/` |
| `themes/*.css` | les 5 thèmes optionnels | ajoutés après `theme.css` |
| `directions.css` | morceaux des directions artistiques | seuls ceux de la direction choisie |

Les variables CSS de `theme.css` (`--bg`, `--bg-soft`, `--card`, `--text`, `--muted`, `--line`, `--radius`, `--shadow`, `--text-w`, `--wide-w`) et celles calculées depuis les réglages (`--accent`, `--on-accent`, `--link`, `--font`, `--font-head`) restent ta meilleure prise : utilise-les dans tes gabarits pour suivre le thème, le mode sombre et la couleur choisie.

## Comment un gabarit reçoit ses données ?

`Theme::render($vue, $v)` exécute d'abord le gabarit de la vue (`single`, `list`…), place son HTML dans `$v['content']`, puis exécute `layout.php` avec le même tableau. Le squelette voit donc toutes les variables de la page : `$v['item']` ou `$v['translations']` sont disponibles dans l'en-tête d'un article. Pour une page en français, `Theme::typography()` ajoute enfin les espaces insécables.

`Theme::partial($nom, $v)` inclut `app/theme/<nom>.php` dans une fonction isolée : seul `$v` y est visible. Tu peux ainsi découper ton thème en petits fichiers (`Theme::partial('lang-switch', $v)`), en sachant que chaque nouveau fichier apparaîtra parmi les « fichiers inconnus » du contrôle d'intégrité.

| Outil | Usage dans un gabarit |
| --- | --- |
| `e($valeur)` | échapper tout texte ou attribut, sans exception |
| `path_to('/chemin')` | adresse interne, compatible avec un sous-dossier |
| `asset('site/site.js')` | adresse d'un fichier statique avec son empreinte de version |
| `Theme::t('clé', $lang)` | textes du thème en six langues (`read_in`, `toc`, `menu`…) |
| `Menus::items('main')` | liens du menu : `label`, `href`, `path` et `external` |
| `Media::get()`, `Media::picture()` | image de la médiathèque, en WebP responsive |
| `Content::path($entree)` | adresse publique d'un contenu |

Exemple : afficher les traductions d'un article dans l'en-tête, à partir des liens créés dans l'éditeur.

```php
<?php // layout.php, dans <header> (ajoute « use Eclair\Content; » en tête du fichier) ?>
<?php if (!empty($v['translations'])): ?>
  <nav class="lang-switch" aria-label="<?= e(Theme::t('read_in', $lang)) ?>">
    <?php foreach ($v['translations'] as $code => $t): ?>
      <a href="<?= e(path_to(Content::path($t))) ?>" hreflang="<?= e($code) ?>" lang="<?= e($code) ?>"<?= $t['id'] === ($v['id'] ?? '') ? ' aria-current="true"' : '' ?>><?= e(strtoupper($code)) ?></a>
    <?php endforeach ?>
  </nav>
<?php endif ?>
```

Le style de `.lang-switch` va dans le CSS personnalisé ou dans `theme.css`, jamais dans un attribut `style`.

## Les contraintes de la CSP

Les pages publiques sont servies avec une Content-Security-Policy stricte, calculée par `Security::headers()`. Elle s'applique à tes gabarits comme au reste :

- **Scripts** : `script-src 'self'`. Aucun `<script>` en ligne, aucun attribut `onclick`, aucun lien `javascript:`. Un fichier `.js` servi par ton domaine reste permis.
- **Styles** : `style-src 'self'` plus l'empreinte `sha256` du CSS d'Éclair. La seule balise `<style>` acceptée est celle de `layout.php`, remplie par `Theme::css()`. Un second bloc `<style>` ou un attribut `style=""` sont bloqués.
- **Polices et ressources** : `default-src 'self'`. Une police chargée depuis un service tiers est refusée : héberge-la sur ton domaine, comme les directions artistiques le font dans `media/fonts/`.
- **Cadres** : seuls les lecteurs `youtube-nocookie.com` et `player.vimeo.com` sont autorisés.

> **Ne change pas la CSP** — La politique vit dans `app/lib/Security.php`, au cœur d'Éclair. L'assouplir t'oblige à modifier le noyau, perdu à chaque mise à jour, et affaiblit la protection de tout le site. Adapte plutôt ton HTML.

## Performance des gabarits

Pour une page mise en cache, un gabarit ne s'exécute qu'au premier affichage : la suite est servie par le cache statique. Ce n'est pas une raison pour l'alourdir. Lis les listes dans l'index (`Content::index()`, `Content::published()`) plutôt que d'ouvrir chaque contenu, garde `Media::picture()` pour les images (dimensions, `srcset` WebP, chargement différé) et préfère les éléments HTML natifs, `<details>` ou l'attribut `popover`, à du JavaScript. Chaque Ko ajouté à `theme.css` est envoyé avec chaque page, puisque le CSS est inséré dans le HTML.

Garde aussi les appels `Blocks::ea()` de `single.php` (attributs `data-ef`) : ils n'apparaissent que dans l'aperçu de l'administration et permettent au mode Visuel de modifier sur place le titre, l'image à la une et « L'essentiel ».

## Mises à jour, intégrité et manifest.php

La procédure de mise à jour supprime `app/` puis le remplace : tes gabarits disparaissent avec lui. Organise-toi dès le premier jour.

1. Range tes gabarits modifiés dans un dépôt Git, hors du site en ligne.
2. Après chaque mise à jour, compare les gabarits d'origine de l'ancienne et de la nouvelle version, pour repérer une variable ajoutée ou un correctif.
3. Reporte ces changements dans tes fichiers, teste en local, puis renvoie-les.
4. Lance **Sécurité › Intégrité des fichiers › Analyser maintenant** : seuls tes fichiers doivent apparaître parmi les modifiés ou les inconnus.

```bash
# Ce qui a changé dans les gabarits d'origine entre deux versions
diff -ru eclair-1.7.0/eclair/app/theme eclair-1.8.0/eclair/app/theme
```

Le contrôle d'intégrité compare chaque fichier à son empreinte dans `app/manifest.php`. Une liste conforme à tes modifications est normale ; un fichier inattendu mérite une enquête. Pour un parc de sites, une variante plus robuste consiste à entretenir ta propre copie du dépôt et à produire ton archive avec `php tools/build.php` : son manifeste porte les empreintes de tes gabarits, et le contrôle reste propre. Les tests de rendu public de `tests/run.php` signaleront alors tes changements : c'est attendu, ils comparent au rendu d'origine.

## L'exemple de cette académie

Éclair Academy est construite avec Éclair 1.8 et des gabarits du thème personnalisés. Le thème standard partage les mêmes menus entre toutes les langues ; nos gabarits ajoutent quatre comportements, en respectant les contraintes décrites plus haut (aucun script en ligne, aucun attribut `style`, rendu mis en cache comme d'habitude).

| Fichier | Rôle |
| --- | --- |
| `app/theme/academy.php` | Classe `Academy` : langue d'une adresse, filtrage des menus, icônes, parcours et position d'une leçon |
| `app/theme/layout.php` | En-tête avec sélecteur de langue, menu mobile plein écran, pied de page avec les quatre parcours |
| `app/theme/single.php` | En-tête de leçon (pastille, « Leçon 3 sur 9 », plan de la ligne), sommaire latéral, navigation précédente et suivante |
| `app/theme/list.php` et `404.php` | Recherche et page introuvable dans la langue demandée |
| `assets/site/academy.js` | Bouton « Copier » des blocs de code et repère de lecture du sommaire (chargé en différé) |

- **Menus filtrés par langue** : un seul menu principal contient les liens des trois langues ; `Academy::menu()` ne garde que ceux dont l'adresse appartient à la langue de la page (le français à la racine, `/en/…` et `/es/…`). Un émoji en tête de libellé, comme « 🧭 Parcours », devient une icône dessinée dans le menu mobile.
- **Sélecteur de langue** : il s'appuie sur les traductions reliées dans l'éditeur (`$v['translations']`) et mène à la même page dans l'autre langue, ou à l'accueil de cette langue si la page n'est pas traduite.
- **Menu mobile plein écran sans JavaScript** : un bouton `popovertarget="site-menu"` ouvre `<nav id="site-menu" popover>`, que le navigateur gère seul (touche Échap, clic à l'extérieur). Sur grand écran, le CSS remet le menu dans l'en-tête.
- **Navigation de leçon automatique** : `Academy::stations()` lit la première liste numérotée de la page du parcours ; l'ordre de ses liens donne le numéro de chaque leçon, le plan de la ligne et les liens « Leçon précédente » et « Leçon suivante ». Pour réordonner un parcours, on modifie simplement cette liste dans l'éditeur.

Le cœur du filtrage tient en quelques lignes :

```php
<?php
// app/theme/academy.php (extrait)
public static function langOf(string $path): string
{
    foreach (['en', 'es'] as $code) {
        if ($path === '/' . $code || str_starts_with($path, '/' . $code . '/')) {
            return $code;
        }
    }
    return 'fr';
}

/** Éléments d'un menu dans la langue de la page ; les liens externes s'affichent partout. */
public static function menu(array $items, string $lang): array
{
    $out = [];
    foreach ($items as $item) {
        $path = (string) ($item['path'] ?? '');
        if ($path !== '' && self::langOf($path) !== $lang) {
            continue;
        }
        [$icon, $label] = self::splitIcon((string) $item['label']);
        $out[] = $item + ['icon' => $icon, 'text' => $label];
    }
    return $out;
}
```

Pour que la page Sécurité reste au vert, le script de construction de l'académie recalcule l'empreinte de ces six fichiers dans `app/manifest.php` (les 32 premiers caractères de leur SHA-256, comme le manifeste d'origine). Après une mise à jour d'Éclair, il suffit de recopier ces fichiers depuis l'archive de l'académie, puis de vider le cache.

## À retenir

- Essaie d'abord le CSS personnalisé : il est rangé dans `data/` et survit aux mises à jour.
- Les gabarits du thème reçoivent tout dans `$v`, et `layout.php` voit les mêmes variables que la vue.
- Échappe chaque valeur avec `e()` et construis les adresses avec `path_to()`.
- La CSP interdit scripts et styles en ligne : le CSS va dans les feuilles du thème, le comportement dans le HTML natif.
- Un gabarit modifié est remplacé à chaque mise à jour : versionne-le et compare-le aux nouvelles versions.

## Pour aller plus loin

- [Le CSS personnalisé](https://eclair.academy/parcours/intermediaire/css-personnalise) : tout ce qu'on peut faire sans toucher au PHP.
- [Créer un site multilingue](https://eclair.academy/parcours/intermediaire/site-multilingue) : les traductions reliées sur lesquelles s'appuie le sélecteur de langue.
- [Performance : garder 100 sur 100](https://eclair.academy/parcours/expert/performance) : mesurer l'effet de tes gabarits.
- [Sécurité avancée et maintenance](https://eclair.academy/parcours/expert/securite-avancee) : la procédure de mise à jour et le contrôle d'intégrité.
- MDN : [l'API Popover](https://developer.mozilla.org/fr/docs/Web/API/Popover_API), pour un menu sans JavaScript.
- OWASP : [aide-mémoire sur la Content Security Policy](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html).

## Questions fréquentes

### Mes gabarits survivent-ils à une mise à jour d'Éclair ?

Non : la mise à jour remplace tout le dossier `app/`, gabarits compris. Garde tes fichiers dans un dépôt Git et réapplique-les après chaque version, en vérifiant les changements d'origine.

### Puis-je ajouter mon propre fichier JavaScript ?

La CSP accepte un fichier servi par ton domaine, jamais un script en ligne. Choisis bien son emplacement : `assets/` est remplacé à chaque mise à jour, et le contrôle d'intégrité signale dans `media/` tout ce qui n'est ni une image ni une police. Essaie d'abord une solution en HTML natif.

### Comment ajouter un texte traduit absent de Theme::t ?

`Theme::t()` renvoie la clé elle-même quand elle n'existe pas. Déclare dans ton gabarit un petit tableau de textes par langue, indexé par `$lang`.

### Le mode Visuel fonctionne-t-il avec mes gabarits ?

Oui, si tu gardes la sortie des blocs (`$v['html']`) et les attributs `Blocks::ea()` de `single.php`. Ils restent invisibles sur le site public.
