# 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 `

`. **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 1) Générer / mettre à jour la baseline (cas intentionnel) : - `npm run build` - `npm run test:anchors:update` 2) Vérifier sans changer la baseline (cas normal) : - `npm run build` - `npm 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.json` - `scripts/inject-anchor-aliases.mjs` - `scripts/check-anchor-aliases.mjs` - et rappelle : *alias = compat rétro de liens historiques sans JS* ## Ancres de section (H2) quand on utilise `

` Quand un chapitre est structuré en sections repliables (`
`), 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 : - `` - puis, dans le même `
`, un H2 visible dans le body. Contrat côté CSS : - `.details-anchor` a `scroll-margin-top: var(--sticky-offset)`. Contrat côté JS (EditionLayout) : - `openDetailsIfNeeded(el)` ouvre le `
` parent si nécessaire avant de scroller. ## Compat “legacy hash” : `#p--` Un hotfix de compat existe pour les anciennes ancres de paragraphes au format : - `#p--<8 hex>` Si l’ID exact n’existe plus : - on cherche le premier élément dont l’id commence par `p--` - 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 -> newId` par 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