4.5 KiB
Contrat des ancres (paragraphes opposables)
Source de vérité du sélecteur
Le site garantit la citabilité des paragraphes via des IDs injectés sur les balises <p>.
Sélecteur contractuel :
.reading p[id^="p-"]
Tout outillage (scripts, tests, docs) doit utiliser ce sélecteur comme référence.
Ce que le test vérifie
Le test compare, page par page, la liste des IDs de paragraphes présents dans dist/ contre une baseline versionnée.
- Ajouts d’IDs : généralement OK (nouveaux paragraphes).
- Suppressions / churn élevé : alerte (risque de casser des citations existantes).
Fichier baseline
tests/anchors-baseline.json
Commandes
- Générer / mettre à jour la baseline (cas intentionnel) :
npm run buildnpm run test:anchors:update
- Vérifier sans changer la baseline (cas normal) :
npm run buildnpm run test:anchors
Politique d’échec (pragmatique)
Le test échoue si le churn d’une page dépasse un seuil (défaut : 20%) sur une page “suffisamment grande”.
Aliases build-time
src/anchors/anchor-aliases.jsonscripts/inject-anchor-aliases.mjsscripts/check-anchor-aliases.mjs- et rappelle : alias = compat rétro de liens historiques sans JS
Ancres de section (H2) quand on utilise <details>
Quand un chapitre est structuré en sections repliables (<details>), on utilise une ancre “technique” dédiée pour garantir :
- une cible stable même si le H2 est dans un bloc replié,
- un scroll-offset correct (header + bandeau),
- une détection fiable pour le bandeau “reading-follow”.
Contrat côté HTML rendu :
- présence d’un élément de type :
<span class="details-anchor" id="..."></span>
- puis, dans le même
<details>, un H2 visible dans le body.
Contrat côté CSS :
.details-anchorascroll-margin-top: var(--sticky-offset).
Contrat côté JS (EditionLayout) :
openDetailsIfNeeded(el)ouvre le<details>parent si nécessaire avant de scroller.
Compat “legacy hash” : #p-<idx>-<hash>
Un hotfix de compat existe pour les anciennes ancres de paragraphes au format :
#p-<index>-<8 hex>
Si l’ID exact n’existe plus :
- on cherche le premier élément dont l’id commence par
p-<index>- - puis scroll avec offset.
But : éviter les “liens morts” historiques quand une régénération d’IDs a eu lieu.
Limite : c’est un fallback de dernier recours (moins déterministe qu’un alias explicite).
Le mécanisme recommandé reste : src/anchors/anchor-aliases.json + injection au build.
Dernière mise à jour : 2026-01-29
Ce document formalise comment on évite de casser les liens profonds (URLs avec #ancre).
1) Principe
Les IDs d’ancres générés (ou dérivés) peuvent changer :
- réécriture
- insertion/suppression de paragraphes
- re-slug d’un titre
➡️ Donc : on ne “devine” pas un fallback par index en runtime. ➡️ On fait un aliasing déterministe à la build, versionné.
2) Le mapping d’alias
- Fichier versionné (ex) :
src/anchors/anchor-aliases.json - Format :
oldId -> newIdpar page
Ex en json : { "/archicratie/archicrat-ia/chapitre-4/": { "p-8-ancien": "p-8-nouveau" } }
3) Injection à la build (dans dist)
Script : scripts/inject-anchor-aliases.mjs
Moment : postbuild (après astro build, avant pagefind)
Ce script injecte dans dist/**/index.html un juste avant l’élément portant id="newId".
4) Contrôles (incassables)
A) Vérifier qu’on n’a pas d’IDs dupliqués en HTML
Script : scripts/audit-dist.mjs
Robustesse :
ignore <script> et <style> (évite faux positifs)
exige un espace avant id= (évite data-id=)
Exécution : npm run audit:dist
B) Vérifier que les aliases sont vraiment présents dans dist
Script : scripts/verify-anchor-aliases-in-dist.mjs
Exécuté dans npm test
5) Mise à jour assistée (si besoin)
Pour générer/mettre à jour des aliases à partir des deltas d’ancres : npm run test:anchors:update
6) ⚠️ Artefacts macOS (PaxHeader / ._ / DS_Store)
Quand on transfère une archive depuis macOS, on peut importer des dossiers/fichiers parasites :
PaxHeader/
._*
.DS_Store
Ces artefacts peuvent polluer le build (Astro croit voir un “vrai” contenu MDX).
➡️ Recommandation : .dockerignore strict :
macOS / archives
.DS_Store .* **/.* PaxHeader **/PaxHeader
Node
node_modules dist