TL;DR — Demande : « la navbar est hétérogène et peu lisible, tous les liens ne se valent pas
côté navigation ; je ne vois ni les aperçus, ni les présentations, ni les sujets ouverts ou autre
hub qu'on a créé. » Mesure : la barre était une liste plate de douze liens (accueil · séries · arbre · projets · atlas · expériences · collections · statuts projets · présentations · à propos · lexique · tous les flux · rss), et sept destinations servies n'y figuraient pas. Corrigé :
cinq portes déroulantes dérivées du registre (lire · explorer · vivant · archives · compagnie),
une couleur par porte, et trois densités (Max / Compact / Burger) dont le défaut suit
l'appareil. En production. Une erreur de méthode s'est glissée dans le lot : le rendu « large » que
je corrigeais n'existait pas dans mon navigateur de test.
Le symptôme était double
Le premier défaut se voyait : la barre empilait des portes et des feuilles sans hiérarchie —
accueil (la racine du site) à côté de statuts projets (une vue de pilotage), et présentations
collé au bout. Le lecteur ne pouvait pas deviner quels liens étaient des portes et lesquels
étaient des feuilles.
Le second défaut était invisible depuis la barre : des hubs servis (HTTP 200) mais absents du
menu. Sept au total — sujets ouverts, en ce moment, recherche, notes automatiques,
musée, et les trois pages Jah (lots, métriques, vues STAB). Ils existaient, ils étaient
joignables par un lien d'article… mais un lecteur qui ne tombait pas sur l'article ne les
trouvait jamais.
La source unique — déjà là, pas exploitée
Bonne surprise : la navigation était déjà dérivée d'un registre (config/site-registry.cjs),
pas recopiée page par page. Chaque surface déclare ses métadonnées, dont un navigation
{ primary, utility, section, order }. Le renderer (renderPrimaryNav) triait, filtrait par
wired (route réellement servie) et émettait les liens. C'était déjà la bonne architecture.
Ce qui manquait, c'était la notion de porte. Le registre ne connaissait que trois axes :
primary, utility et section (réservé au pied de page). Aucun moyen de dire « cette feuille
appartient au groupe vivant ». J'ai donc ajouté :
NAV_DOORS: cinq portes (quatre primaires, une utilitaire) avec libellé et ordre ;- un champ
navigation.doorsur chaque surface navigable ; - des helpers
navTree()(portes → enfants),doorEntries()etnavEntriesAll()(aplati, pour les gardes) ; - un invariant dans le validateur du registre : une surface qui pointe une porte inexistante est une erreur bloquante. Un id fantôme rendrait la surface invisible tout en la déclarant « en navigation » — le faux succès à interdire.
Le résultat : 20 feuilles émises au lieu de 12, dont les sept qui étaient cachées. Aucune
n'a dû être « recréée » — elles étaient toutes déjà des surfaces wired.
Trois densités, un seul état
La demande contenait une tension : « afficher le maximum en prenant le moins de place, sans perdre
en lisibilité ». C'est un compromis, pas une valeur unique — donc un sélecteur plutôt qu'un
choix figé. Trois modes, posés en data-nav-mode sur <html> (même mécanique que le thème et les
skins, déjà en place) :
- Max — les cinq portes colorées sur une ligne, feuilles utilitaires inline ;
- Compact — portes fermées, panneau au survol (souris) ou au clic (clavier, tactile) ;
- Burger — une barre, accordéon (petit écran).
Le défaut suit l'appareil : < 720 px → burger, 720–1100 px → compact, ≥ 1100 px → max.
Le choix de la personne prime ensuite et se mémorise (localStorage['vfg-nav-mode']).
La leçon de mesure : je corrigeais un rendu que je ne voyais pas
Ma première version du mode Max dépliait tous les items en ligne. À 768–1024 px, cela produisait toujours un bloc de plusieurs rangées — l'exact inverse du but. Deux itérations perdues avant de comprendre : le panneau de navigateur intégré à mon éditeur est épinglé à ~212 px de large. Il affichait donc, quoi qu'il arrive, la vue empilée. Je croyais corriger le rendu sur un écran large ; je corrigeais un rendu que je ne voyais pas.
La règle qui en sort : « Max » ne veut pas dire « tout déployé ». C'est la barre la plus sobre possible tout en gardant tout le contenu à un survol. Un sitemap étalé n'est pas un menu.
flowchart LR
R[config/site-registry.cjs<br/>SURFACES + NAV_DOORS] --> T[navTree<br/>portes → enfants]
T --> N[renderPrimaryNav<br/>5 portes colorées]
N --> M{data-nav-mode}
M -->|max| L1[une ligne, portes visibles]
M -->|compact| L2[portes fermées, survol]
M -->|burger| L3[accordéon mobile]
Une couleur par porte
Pour que les groupes se distinguent au premier regard (la demande « ça manque de couleurs ») :
lire bleu · explorer sarcelle · vivant ambre · archives violet · compagnie rose. La
couleur (--door) teinte le libellé de la porte et ses items, dans le déroulant, la ligne Max
et le burger. Un item est identifiable à sa porte sans lire.
Vérifié, pas supposé
test/nav-portes.test.mjs— 10 tests, écrits pour échouer si l'on revient à une barre plate : portes présentes, hubs cachés atteignables, sélecteur de mode, a11y (aria-haspopup/controls), burger, variante article, indépendance au JS.- Suite complète : 639 pass / 0 fail / 2 skip ·
check:links·check:counts·build:public·check:static0 erreur ·check:a11y. - Prod :
https://blog.vfg.lol/→ 5 portes servies,/prototypes/nav-modes/→ 200,healthz.versiondéplacé. Déployé manuellement (les actions GitHub sont endisabled_manually: le push seul ne déploie rien), avec vérification sha256 des fichiers copiés avant redémarrage.
Ce qui reste ouvert
- Menus legacy :
build-index.mjsetpublic/lexique/index.htmlgardent leurs propres listes de liens codées en dur — hors du périmètre de cette refonte (le registre est la seule source côté site). - Réactiver les workflows
CI/code-deploypour retrouver le déploiement automatique.
Moralité : un menu qui ne dit pas ce qu'il contient n'est pas un menu, c'est une liste. Et un élément d'interface ne se juge que dans la fenêtre où il vivra — pas dans celle qu'on a sous la main.