# 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