Un CMS qui refuse
de produire du mauvais code
Le site est écrit en JSON et converti en HTML par un programme qui connaît les règles du référencement, de l'accessibilité et des moteurs de rendu. Ce qui serait fautif ne peut pas être écrit — et ce qui passe malgré tout est arrêté sur les octets produits.
Un site décrit, pas codé
Le site de Kaina Expérience n'existe nulle part sous forme de HTML écrit à la main. Il existe sous forme de fichiers JSON — des textes, des jetons de design, une arborescence de blocs — et un programme de 7 884 lignes qui les convertit en pages.
La conséquence tient en une phrase : le rédacteur ne peut pas écrire une faute qu'il ne peut pas exprimer. Il n'écrit pas <h3>, il place une section dans une autre. Il n'écrit pas #6e6e6e, il nomme une couleur. Le niveau de titre et la valeur exacte sont calculés.
Ce document est engendré depuis le dépôt. Les chiffres qui suivent — 25 composants, 29 jetons, 1 776 textes — sont lus dans les fichiers, jamais recopiés.
Ce mécanisme est né d'un constat : trois chiffres écrits à la main ici avaient déjà divergé du code — 4 140 lignes annoncées pour 7 884 réelles, 1 568 textes pour 1 776, deux langues pour 4. Un chiffre recopié est un chiffre qui finira par mentir.
Rendre la faute inexprimable
La différence entre détecter une faute et la rendre impossible est tout le sujet. Un CMS qui affiche « attention, contraste faible » laisse publier. Ici, la compilation s'arrête.
Trois exemples réels, tous trouvés sur l'ancien site.
Le clamp() qui rétrécit au zoom
L'ancien site contenait 35 clamp() écrits à la main sans composante rem. Mesuré au zoom 200 % : le H1 passait de 152 px à 79 px — il rétrécissait. Échec WCAG 1.4.4, technique F94. Invisible à l'œil, impossible à repérer en relisant le code.
Dans le langage à jetons, le rédacteur n'écrit pas de clamp() : il écrit titre_xl. Et un jeton de taille qui ne contiendrait pas de rem arrête la compilation.
for famille in FAMILLES_TEXTE:
for nom, obj in self.brut.get(famille, {}).items():
v = str(obj["valeur"])
if "clamp(" in v or "vw" in v:
if "rem" not in v and "em" not in v:
fautes.append(
f"tailles.{nom} = {v}\n"
" Aucune composante rem : au zoom 200 % "
"ce texte ne grossira pas.")Le jeton qui n'existait pas
Un gabarit contenait color:var(--gris). Le vrai nom est --c-gris. CSS ignore silencieusement une variable inconnue : la règle était morte depuis toujours, sans erreur nulle part, sans effet visible.
Ici, un nom de jeton inconnu ne produit pas une règle morte : il produit un arrêt, avec la liste des noms valides.
Les deux h1 sur une même page
Cette faute-là, je l'ai commise en construisant ce CMS. Le composant appel était marqué « sectionnant » : il recevait donc un niveau de titre, et sur une page sans autre section titrée il obtenait le niveau 1. Deux <h1>.
Le contrôle de hiérarchie ne l'a pas vue, parce qu'il examinait l'arbre et ne comptait que les types qu'il connaissait. Correction en deux temps : le type est sorti des sectionnants, et un second contrôle a été ajouté sur les octets produits. C'est le cas qui justifie la double vérification — un contrôle sur le produit fini n'a pas d'angle mort de ce genre.
Quatre choses qui changent séparément
Les textes, l'architecture, l'apparence et les images changent à des rythmes différents, pour des raisons différentes, et par des personnes différentes. Elles sont donc dans quatre fichiers différents.
C'est cette séparation qui rend la traduction possible : txt_de.json est une copie de txt_fr.json dont on change les valeurs. La structure est partagée, donc elle ne peut pas diverger d'une langue à l'autre.
| Fichier | Contenu | Qui le modifie | Effet d'une modification |
|---|---|---|---|
| site/textes/ | les mots | rédacteur, traducteur | une ligne de HTML |
| site/structure/ | l'architecture | concepteur, agent IA | la page concernée |
| site/design/ | l'apparence | graphiste | tout le site, 2 lignes de CSS |
| site/medias/ | les images | photographe | toutes les pages qui l'utilisent |
Mesuré. Changer un texte dans txt_fr.json modifie exactement une ligne du HTML produit. La mise en page n'est pas concernée — c'est la garantie que la séparation est réelle et pas seulement annoncée.
De l'intention aux octets
Dix étapes, dont sept peuvent refuser de continuer. Aucune n'est un avertissement.
site/*.json
│
├── 1 chargement des sources ......... clés manquantes ?
├── 2 vérification des jetons ........ taille sans rem ?
├── 3 calcul des contrastes .......... sous 4,5:1 ?
├── 4 construction de l'arbre ........ composant inconnu ?
├── 5 calcul des niveaux de titre .... profondeur > 6 ?
├── 6 rendu des composants ........... propriété non déclarée ?
├── 7 assemblage du <head> ........... charset après 1024 o ?
├── 8 contrôle du CSS produit ........ règle qui casse un moteur ?
├── 9 contrôle du HTML produit ....... deux h1 ? saut de niveau ?
└── 10 annexes ........................ sitemap, robots, 404
│
dist/*.html
Ce qui a été mesuré
Un contrôle qui ne trouve jamais rien ne prouve rien. Le même contrôle a donc été exécuté sur l'ancien site : il y relève 75 problèmes. Sur le nouveau : zéro.
C'est la seule manière de savoir qu'un contrôle n'est pas complaisant.
| Mesure | Ancien site | Nouveau |
|---|---|---|
| Gabarits HTML à maintenir | 13 fichiers, 274 Ko | 0 |
| HTML produit (13 pages) | 816 Ko | 315 Ko |
Attributs style= en ligne | 136 | 0 |
Texte masqué en -9999px | 14 | 0 |
Éléments <article> sémantiques | 131 | 190 |
Noms accessibles (aria-label) | 65 | 195 |
| Défauts relevés au contrôle | 75 | 0 |
| Fidélité du contenu | — | 100 % (1 534 fragments) |
Les 75 problèmes de l'ancien site se répartissent en 28 fautes de compatibilité navigateur, 27 d'accessibilité, 14 d'ordre du <head> et 6 d'images prioritaires concurrentes. Aucun n'était visible à l'œil sur la page rendue.