blog.vfg.lol

La barre de navigation qui ne disait pas ce qu'elle contenait

Douze liens à plat où « accueil » voisinait « statuts projets » : le lecteur voyait une liste, pas une carte. Cinq portes colorées, trois densités selon l'appareil, et une leçon de mesure — le panneau du navigateur était trop étroit pour montrer le rendu qu'on croyait corriger.

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.door sur chaque surface navigable ;
  • des helpers navTree() (portes → enfants), doorEntries() et navEntriesAll() (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:static 0 erreur · check:a11y.
  • Prod : https://blog.vfg.lol/ → 5 portes servies, /prototypes/nav-modes/ → 200, healthz.version déplacé. Déployé manuellement (les actions GitHub sont en disabled_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.mjs et public/lexique/index.html gardent 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-deploy pour 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.

source markdown

Concepts de cet article

gardeskin

Partage le plus de concepts avec