---
title: "L'architecture d'Éclair"
description: "L'architecture d'Éclair expliquée aux développeurs : chemin rapide d'index.php, cache statique, stockage à plat dans data/, classes et en-têtes de sécurité."
url: "https://eclair.academy/parcours/expert/architecture"
lang: "fr"
author: "Thierry Gustin"
published: "2026-11-02"
updated: "2026-11-02"
---

# L'architecture d'Éclair

> **L’essentiel** — L'architecture d'Éclair repose sur un seul script public, index.php, qui sert directement les pages déjà générées depuis cache/pages. Les autres requêtes passent par app/bootstrap.php puis App::run, qui aiguille vers l'installateur, l'administration, le formulaire, le connecteur MCP ou le site. Les données sont des fichiers JSON protégés dans data/, écrits de façon atomique sous verrou.

L'architecture d'Éclair tient en une règle : ne faire travailler PHP que lorsque c'est utile. Cette leçon suit une requête de bout en bout, du serveur web au cache statique, montre comment les données sont rangées sans base de données et quelle classe fait quoi. Tu pourras ensuite déboguer, personnaliser ou héberger Éclair en connaissant ses rouages.

> **Avant de commencer** — Durée : 30 minutes. Prérequis : lire du PHP 8 et connaître les bases d'HTTP (en-têtes, codes 200, 301, 304, 404). Un site installé aide à suivre : en local avec [Développement local et tests](https://eclair.academy/parcours/expert/developpement-local), ou en ligne après [Installer Éclair pas à pas](https://eclair.academy/parcours/debutant/installer-eclair).

## Ce que tu sauras faire

### ⚡ Suivre une requête

Du serveur web à index.php, puis au cache ou à l'application.

### 🔒 Lire le dossier data/

Ce que contient chaque fichier et comment il est protégé.

### 🛠 Trouver la bonne classe

Savoir où agir dans app/lib/ en quelques secondes.

## Comment une requête traverse l'architecture d'Éclair ?

![Schéma du cycle d'une requête : réponse du cache statique ou passage par l'application Éclair](https://eclair.academy/media/2026/11/eclair-academy-architecture-fr.png)
*Le chemin rapide sert les pages déjà générées, tout le reste passe par App::run.*

Tout part d'un constat : la plupart des visites demandent une page inchangée. L'architecture d'Éclair la renvoie donc telle quelle, sans lire un seul contenu. Voici le trajet d'une requête.

1. **Le serveur web.** Sur Apache ou LiteSpeed, le `.htaccess` envoie à `index.php` toute adresse qui n'est pas un fichier existant. Il répond 404 pour `app/`, `data/`, `cache/`, les fichiers cachés (sauf `.well-known`) et tout `.php` autre qu'`index.php`, et transmet l'en-tête `Authorization` au connecteur MCP.
2. **Le chemin rapide d'index.php.** Pour un GET ou un HEAD, `index.php` lit `cache/runtime.php`, puis cherche la page dans `cache/pages/` sous le `md5` du chemin. S'il la trouve, il renvoie ses en-têtes enregistrés, son ETag et `X-Cache: HIT`, ou un `304 Not Modified` si le navigateur a déjà cette version.
3. **L'amorçage.** Sinon, `app/bootstrap.php` vérifie la version de PHP (8.1 au moins), charge `eclair-config.php` s'il existe, définit `ECLAIR_DATA` et `ECLAIR_DEBUG`, enregistre le chargement automatique des classes `Eclair\…`, puis appelle `Eclair\App::run()`.
4. **L'aiguillage.** `App::run()` installe la gestion des erreurs et confie la requête au bon module (tableau ci-dessous).
5. **Le rendu.** `Site::handle()` n'accepte que GET et HEAD, impose une seule adresse par contenu (barre finale, majuscules, `index.php` : redirection 301), sert les fichiers générés (`robots.txt`, `sitemap.xml`, `llms.txt`, flux, versions `.md`), puis cherche le contenu. Une page rendue avec le statut 200 est enregistrée par `Cache::put()`.

| Chemin demandé | Module qui répond |
| --- | --- |
| `/__eclair/ping` | test de réécriture, preuve d'identité (`Loopback`) |
| tout chemin, avant l'installation | `Installer::handle()` |
| l'adresse de l'administration et ses sous-pages | `Admin::handle()` |
| `/_eclair/contact` | `Contact::handle()`, le formulaire de contact |
| `/_eclair/edit/<id>` | redirection vers l'éditeur, si connecté |
| `/_eclair/…` et `/.well-known/…` | `Mcp::handle()`, muet tant que le connecteur est désactivé |
| tout le reste | `Site::handle()` |

La recherche, la page 404, l'aperçu de l'apparence, le mode Visuel et l'administration ne sont jamais mis en cache. Pour voir le chemin rapide à l'œuvre, interroge deux fois la même page :

```bash
# 1er appel : la page est générée par l'application, puis enregistrée
curl -sI https://exemple.fr/contact | grep -i -E 'etag|x-cache'
# ETag: "0745734655e7d523"

# 2e appel : servie par index.php depuis cache/pages/, sans charger l'application
curl -sI https://exemple.fr/contact | grep -i -E 'etag|x-cache'
# ETag: "0745734655e7d523"
# X-Cache: HIT

# Le fichier de cache porte le md5 du chemin demandé
php -r 'echo md5("/contact"), PHP_EOL;'   # 90609cfd31a32bdbd3d81843c57d356b
```

## Le stockage à plat : la classe Store

L'architecture d'Éclair se passe de base de données : chaque donnée est un fichier `data/<nom>.php` qui commence par la ligne `<?php exit; ?>`, suivie de JSON lisible. Appelé depuis le web, il ne renvoie rien, même sur un serveur qui ignorerait les `.htaccess`. Voici le début d'un vrai fichier de menus :

```php
<?php exit; ?>
{
    "main": [
        {
            "type": "content",
            "id": "af59e5ba0a",
            "label": "",
            "url": ""
        }
    ]
}
```

Quatre mécanismes rendent ce stockage sûr, même sur un mutualisé chargé :

- **Écriture atomique** : `Store::atomic()` écrit un fichier temporaire puis le renomme, si bien qu'un lecteur ne voit jamais un fichier à moitié écrit. Les fichiers reçoivent les droits 0600, les dossiers de `data/` et `cache/` 0700.
- **Aucun code injectable** : `Store::write()` encode le JSON avec `JSON_HEX_TAG` et `JSON_HEX_AMP`. Un `<` devient `\u003C` : la séquence `<?php` ne peut pas apparaître dans les données.
- **Verrou** : `Store::locked()` pose un verrou exclusif (`flock`) sur `data/.lock` pendant chaque opération lire-modifier-écrire.
- **Noms contrôlés** : `Store::file()` n'accepte que minuscules, chiffres, tirets et barres obliques. Impossible de sortir de `data/`.

Toutes les classes qui écrivent suivent ce motif, `Redirects::add()` par exemple :

```php
use Eclair\Store;

$menus = Store::read('menus', []);           // lecture, avec une valeur par défaut
Store::locked(function (): void {            // lire, modifier, écrire sans course
    $list = Store::read('redirects', []);
    // … modifier $list …
    Store::write('redirects', $list);        // fichier temporaire, 0600, renommage
});
```

## Les fichiers du dossier data/

La dernière colonne indique ce que garde la sauvegarde ZIP de la page **Sécurité** : secrets et données passagères en sont exclus.

| Fichier ou dossier | Contenu | Sauvegarde ZIP |
| --- | --- | --- |
| `site.php` | réglages, fusionnés avec `Settings::DEFAULTS` | oui |
| `users.php` | comptes, empreintes Argon2id, double authentification | oui |
| `keys.php` | secret du site et clé IndexNow, créés à l'installation | oui |
| `index.php` | résumé de chaque contenu, pour lister et router vite | oui |
| `content/<id>.php` | une page, un article ou un produit, avec ses blocs | oui |
| `revisions/<id>/` | les 20 dernières versions de ce contenu | oui |
| `search.php`, `media.php` | texte de recherche, registre de la médiathèque | oui |
| `menus.php`, `categories.php`, `redirects.php`, `custom-css.php` | menus, catégories, redirections, CSS personnalisé | oui |
| `messages/`, `inbox.php` | messages du formulaire, 500 au plus | oui |
| `logs/` | journaux d'erreurs et de sécurité, bornés à 1 Mo | oui |
| `vault.php` | clés d'API chiffrées | non |
| `ai/` | conversations de l'assistant, propositions, connexions MCP | non |
| `sessions/`, `limits/` | sessions de l'administration, compteurs anti-bruteforce | non |

Les mises à jour ne touchent jamais à `data/`, que la constante `ECLAIR_DATA` peut même placer hors du dossier public.

## Les classes principales d'app/lib/

Toutes les classes vivent dans l'espace de noms `Eclair\`, un fichier par classe, sans framework ni Composer :

| Classe | Rôle |
| --- | --- |
| `App` | aiguillage des requêtes non servies par le cache |
| `Req`, `Http` | lecture de la requête, réponses et redirections |
| `Store`, `Settings` | stockage à plat, réglages et valeurs par défaut |
| `Content`, `Blocks` | contenus, index, révisions ; rendu des 18 blocs |
| `Site`, `Theme` | routeur public ; gabarits et CSS en ligne |
| `Cache` | pages statiques et `cache/runtime.php` |
| `Seo`, `Analyzer` | données structurées, sitemap, llms.txt ; scores |
| `Security`, `Session`, `Auth` | en-têtes, jetons anti-CSRF, sessions, connexion |
| `Media` | médiathèque, ré-encodage et WebP |
| `Admin` et `Admin…` | écrans et actions de l'administration |
| `Ai`, `AiChat`, `AiTools`, `Mcp` | assistant IA, outils, connecteur MCP et OAuth |
| `Integrity`, `Log`, `Keys`, `Vault`, `Loopback` | intégrité, journaux, secrets, coffre, vérification d'adresse |
| `I18n` | traductions de l'administration |

## Pourquoi le cache se vide à chaque modification ?

Un titre modifié apparaît aussi dans un menu, une carte d'article, un fil d'Ariane. Plutôt que de deviner ces dépendances, l'architecture d'Éclair vide tout : `Cache::purge()` suit chaque enregistrement d'un contenu, des réglages, des menus, des catégories, des médias, d'un profil ou du CSS personnalisé. Il supprime les fichiers de `cache/pages/` et réécrit `cache/runtime.php` ; chaque page se régénère à sa première visite.

```php
<?php exit; ?>
{"on":true,"expires":0,"host":"exemple.fr","https":true}
```

Ce petit fichier est lu par `index.php` à chaque requête :

- `on` : cache actif (**Cache statique des pages** coché, mode débogage éteint) ;
- `expires` : date du prochain article programmé, après laquelle `index.php` ignore le cache, que `Cache::checkExpiry()` vide ;
- `host` et `https` : hôte et protocole officiels, une fois l'adresse vérifiée, pour ne jamais court-circuiter les redirections.

> **Vider le cache à la main** — **Réglages › Avancé › Vider le cache** fait la même chose que `Cache::purge()`. C'est la dernière étape de chaque mise à jour d'Éclair.

## En-têtes de sécurité et CSP

`Security::headers()` produit trois jeux d'en-têtes : `site`, `admin` et `text` (sitemap, `llms.txt`, flux). Ils dépendent des réglages, jamais de la requête : personne ne peut empoisonner le cache avec un en-tête forgé. Ceux d'une page publique servie en HTTP, relevés sur Éclair 1.8 :

```text
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'sha256-…'; img-src 'self' data: https:; media-src 'self' https:; frame-src https://www.youtube-nocookie.com https://player.vimeo.com; connect-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=()
Cross-Origin-Opener-Policy: same-origin
```

L'empreinte `sha256` est calculée par `Theme::hash()` sur le CSS inséré dans la page : seule cette balise `<style>` est autorisée, aucun script en ligne n'est permis, et le seul script public est `assets/site/site.js`. En HTTPS s'ajoutent `upgrade-insecure-requests` et, si l'option HSTS est cochée, `Strict-Transport-Security`. L'administration est plus stricte encore : aucun style en ligne, `frame-ancestors 'none'`, `noindex` et `Cache-Control: no-store, private`.

## Les traductions

- **Administration** : chaque texte s'écrit en français dans le code, avec `t()`. `I18n::t()` cherche sa traduction dans `app/lang/<langue>.json.gz`, compilé par `tools/i18n.php` et indexé par l'empreinte CRC32 du texte français. Sans l'extension zlib, l'administration reste en français.
- **Site public** : `Theme::t('clé', $lang)` lit le tableau `Theme::STRINGS` (« Sommaire », « L'essentiel », « Lire en »…) en six langues, et `Theme::words()` donne les mots des adresses (page, auteur, recherche, boutique) dans la langue du site.

> **Ne modifie pas app/ à la légère** — Tout fichier d'`app/` est remplacé à la prochaine mise à jour et signalé par le contrôle d'intégrité. Pour le style, utilise le CSS personnalisé ; pour la structure du HTML, suis [Personnaliser les gabarits du thème](https://eclair.academy/parcours/expert/personnaliser-theme).

## À retenir

- Dans l'architecture d'Éclair, une page déjà générée est servie par `index.php` depuis `cache/pages/`, sans charger l'application : `X-Cache: HIT` le prouve.
- Le reste passe par `app/bootstrap.php` puis `App::run()`, qui aiguille vers l'installateur, l'administration, le contact, le MCP ou le site.
- Les données sont des fichiers JSON derrière `<?php exit; ?>`, écrits de façon atomique, en 0600 et sous verrou.
- Le cache se vide entièrement à chaque modification et se reconstruit à la visite suivante.
- Les en-têtes de sécurité ne dépendent que des réglages : le cache ne peut pas être empoisonné.

## Pour aller plus loin

- [Développement local et tests](https://eclair.academy/parcours/expert/developpement-local) : lancer Éclair sur ta machine et rejouer ses 550 vérifications.
- [Personnaliser les gabarits du thème](https://eclair.academy/parcours/expert/personnaliser-theme) : agir sur le rendu sans casser la CSP.
- [Nginx, VPS, proxy et CDN](https://eclair.academy/parcours/expert/nginx-vps) : reproduire les règles du `.htaccess` hors d'Apache.
- [Sécurité avancée et maintenance](https://eclair.academy/parcours/expert/securite-avancee) : intégrité, sauvegardes et mises à jour.
- Documentation PHP : [flock()](https://www.php.net/manual/fr/function.flock.php) et [rename()](https://www.php.net/manual/fr/function.rename.php), au cœur des écritures de `Store`.
- OWASP : [aide-mémoire sur la Content Security Policy](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html).

## Questions fréquentes

### Éclair peut-il fonctionner sans son cache statique ?

Oui : décoche **Cache statique des pages** dans **Réglages › Avancé**, ou active le mode débogage. Chaque visite génère alors la page, ce qui coûte plus de temps serveur.

### Pourquoi stocker du JSON dans des fichiers .php ?

Pour que la ligne `<?php exit; ?>` protège chaque fichier même si le serveur ignore le `.htaccess`. Appelé depuis le web, il n'affiche rien, tandis qu'Éclair lit le JSON qui suit cette première ligne.

### Que se passe-t-il si deux personnes enregistrent en même temps ?

Les écritures passent par `Store::locked()`, qui pose un verrou exclusif sur `data/.lock`. Les deux enregistrements s'exécutent donc l'un après l'autre, et l'ancienne version rejoint l'historique des révisions.

### Peut-on placer le dossier data/ hors du dossier public ?

Oui. Déplace d'abord le dossier, puis indique son nouvel emplacement avec `define('ECLAIR_DATA', '/chemin/vers/eclair-data');` dans `eclair-config.php`, à côté d'`index.php`. Le chemin rapide n'en dépend pas : il ne lit que `cache/`.
