Vous collez un lien vers une section de votre wiki interne, et votre collègue arrive… quelque part à côté.
Ou le titre est caché derrière un en-tête fixe. Ou l’ancre change à chaque déploiement parce que quelqu’un a « amélioré » la génération des slugs.
Maintenant vous déboguez des liens au lieu des systèmes.
Les liens d’ancrage façon documentation semblent triviaux — jusqu’à ce que vous les exécutiez à l’échelle, sur des années de contenu, plusieurs moteurs de rendu, le mode sombre et un design system qui adore les en-têtes fixes.
C’est une de ces « petites fonctionnalités UI » qui devient un problème d’astreinte quand vos runbooks ne sont plus deep-linkables pendant un incident.
À quoi ressemblent de bons liens d’ancrage (et pourquoi les SREs devraient s’en soucier)
Un bon site de documentation fait paraître les liens de section inévitables. Survolez un titre, une petite icône de lien apparaît, vous cliquez dessus, l’URL se met à jour,
et vous pouvez la coller dans le chat. Quand on l’ouvre, la page défile pour que le titre soit posé proprement sous l’en-tête fixe.
Pas de saut bizarre, pas de titre caché, pas de « pourquoi le navigateur est au mauvais endroit ? »
Ce n’est pas du vernis pour le plaisir du vernis. C’est une capacité opérationnelle. Les runbooks et les postmortems ne valent que par leurs deep links.
Pendant un incident chaotique, vous ne voulez pas dire « descendez jusqu’au troisième titre ‘Mitigation’ ». Vous voulez un lien qui atterrit
exactement au bon paragraphe.
Aussi : les ancres sont un contrat. Une fois que les gens les partagent, ce sont essentiellement des API. Cassez-les et vous le saurez — généralement quand
un cadre regarde un appel d’incident en direct et que quelqu’un dit : « Le lien dans le runbook est mort. »
Blague n°1 : Les liens d’ancrage sont comme les rotations d’astreinte — tout le monde les ignore jusqu’à ce qu’ils échouent, puis soudainement c’est la seule chose dont on parle.
Incontournables pour des ancres « niveau documentation »
- ID stables : les titres doivent conserver le même
identre les reconstructions et les petites modifications de texte. - Défilement avec offset : les en-têtes fixes ne doivent pas couvrir la cible.
- Affordance au survol : l’icône de permalien apparaît au survol/focus, plutôt que d’encombrer la page en permanence.
- Titre cliquable ou contrôle adjacent : l’utilisateur peut copier un lien de section sans cliquer de façon ultra-précise.
- Comportement accessible : focus clavier, énoncé pour les lecteurs d’écran, prise en charge de la réduction de mouvement.
- Fonctionne sans JavaScript : la navigation avec ancres de base doit toujours fonctionner.
Faits et historique : pourquoi les ancres fonctionnent comme elles le font
Les ancres paraissent modernes, mais le mécanisme central est ancien et têtu. C’est une bonne chose : les primitives ennuyeuses sont fiables.
Quelques faits concrets et points de contexte qui expliquent les contraintes actuelles :
- Les identifiants de fragment précèdent le CSS moderne : la portion
#fragmentd’une URL est utilisée depuis les premières normes du Web pour cibler des emplacements dans la page. - Les fragments sont côté client : le fragment n’est pas envoyé au serveur dans les requêtes HTTP, c’est pourquoi les logs serveur ne le montrent pas à moins d’instrumenter le client.
- Le HTML ancien utilisait des ancres nommées : historiquement on écrivait
<a name="foo">; le HTML moderne utiliseid="foo"sur n’importe quel élément. - Les IDs dupliqués sont un comportement indéfini : les navigateurs choisissent « le premier » ou « ce que le DOM signifie », ce qui varie et s’aggrave avec l’hydratation.
- Le CSS a obtenu une vraie solution pour les en-têtes fixes :
scroll-margin-topetscroll-padding-topexistent principalement parce que les en-têtes fixes sont devenus la norme. - Les sites de docs ont popularisé les permalinks au survol : MediaWiki puis les portails développeurs ont appris aux utilisateurs à s’attendre aux permalinks sur les titres.
- Unicode complique le slugging : vous pouvez mettre du non-ASCII dans un
id, mais l’interopérabilité et le comportement copie/coller poussent beaucoup d’équipes vers des slugs ASCII. - Les navigateurs ont maintenant le « scroll to text » : certains prennent en charge les fragments de texte (
#:~:text=), mais ce n’est pas un substitut aux IDs stables et cela peut être fragile.
Décisions de conception qui rendent les ancres ennuyeuses — dans le meilleur sens
Choisissez votre UX : titre cliquable vs bouton de permalien explicite
Deux schémas courants :
- Titre cliquable : tout le titre est un lien vers lui-même. C’est rapide et facile à découvrir. Cela peut cependant agacer les utilisateurs qui voulaient simplement sélectionner du texte.
- Bouton de permalien à côté du titre : le titre reste du texte normal ; une icône de lien apparaît au survol/focus. C’est le classique des sites de docs. C’est ma recommandation par défaut.
En production, je préfère le contrôle de permalien explicite car il sépare « naviguer/copier le lien » de « sélectionner le texte ».
Vous aurez moins de clics accidentels en surlignant des titres.
Stratégie d’offset : CSS d’abord, JavaScript en dernier
Les en-têtes fixes créent le bug d’ancrage le plus visible : le navigateur défile, mais la cible est cachée sous l’en-tête.
Vous pouvez le corriger avec des ajustements JS. Vous pouvez aussi le corriger en CSS et conserver le comportement natif du navigateur.
Utilisez le CSS autant que possible :
scroll-margin-topsur les titres : propre, local et fonctionne pour la navigation in-page normale.scroll-padding-topsur le conteneur de défilement : utile quand vous avez une mise en page avec une zone principale qui défile.
Le JavaScript ne devrait être utilisé que si vous avez des conteneurs de défilement complexes, des hauteurs d’en-tête dynamiques ou des vieux navigateurs que vous ne pouvez pas abandonner.
Traitez les IDs de titres comme un schéma
Les IDs ne sont pas de la décoration. Ce sont des identifiants stables référencés par :
- des liens internes (TOC, références croisées)
- des liens externes (chat, tickets, docs dans d’autres systèmes)
- des indexeurs de moteurs de recherche
- de l’automatisation (linters, vérificateurs de liens, extracteurs de docs)
Si vous changez un algorithme de génération d’ID, vous faites un changement cassant. Agissez en conséquence : versionnez-le, migrez, redirigez si possible, et communiquez.
Implémentation : icônes au survol, offsets, titres cliquables
Structure HTML de base
La meilleure structure est simple : les titres ont un id. À côté de chaque titre, rendez un petit lien d’ancrage
qui pointe vers #id. L’ancre doit être focalisable, avoir un libellé lisible et être visuellement subtile jusqu’au survol/focus.
cr0x@server:~$ cat heading-anchors.html
<article class="doc">
<h2 id="fast-diagnosis">
Fast diagnosis
<a class="permalink" href="#fast-diagnosis" aria-label="Permalink to Fast diagnosis">
<span aria-hidden="true">#</span>
</a>
</h2>
<p>Start with the obvious checks first.</p>
</article>
Ce « # » peut être une icône SVG en réalité. Conservez le libellé accessible, et gardez l’icône visible aria-hidden.
CSS : affordance au survol/focus et correction d’offset
Faites deux choses en CSS :
- Cacher le contrôle de permalien jusqu’à ce que le titre soit survolé ou focusé (mais le garder disponible pour les utilisateurs clavier).
- Appliquer
scroll-margin-topafin que l’ancre aboutisse sous votre en-tête fixe.
cr0x@server:~$ cat anchors.css
:root {
--sticky-header-height: 64px;
}
.doc h2, .doc h3, .doc h4 {
scroll-margin-top: calc(var(--sticky-header-height) + 12px);
position: relative;
}
.doc .permalink {
margin-left: 0.5rem;
text-decoration: none;
opacity: 0;
transition: opacity 120ms linear;
}
.doc h2:hover .permalink,
.doc h3:hover .permalink,
.doc h4:hover .permalink,
.doc .permalink:focus {
opacity: 1;
outline: none;
}
.doc .permalink:focus-visible {
opacity: 1;
outline: 2px solid currentColor;
outline-offset: 2px;
}
Si la hauteur de votre en-tête change selon les breakpoints, définissez --sticky-header-height par media query.
Ne la « mesurez en JS » que si vous y êtes absolument obligé.
Titres cliquables : la version prudente
Si vous insistez pour rendre tout le titre cliquable, faites-le sans envelopper tout le texte du titre dans une <a> qui empêche la sélection.
Un compromis raisonnable est : garder le texte du titre normal, et ajouter un pseudo-élément lien en superposition avec une zone de frappe limitée.
Une autre option : envelopper, mais ajouter du CSS qui améliore la sélection et conserve l’icône de permalien comme contrôle explicite.
Mon conseil franc : faites du contrôle de permalien la cible de clic primaire. Laissez les titres être des titres.
Comportement « Copier le lien » (et pourquoi c’est important)
Certains sites ajoutent un bouton « Copier le lien » dédié qui écrit l’URL complète dans le presse-papiers. C’est pratique pour les utilisateurs et réduit
la confusion « j’ai copié seulement le fragment ». Mais ce n’est pas obligatoire.
Si vous l’implémentez, faites-le de façon progressive : l’href de l’ancre doit continuer à fonctionner sans JS.
cr0x@server:~$ cat copy-link.js
document.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-copy-permalink]');
if (!btn) return;
const id = btn.getAttribute('data-copy-permalink');
const url = new URL(window.location.href);
url.hash = id;
try {
await navigator.clipboard.writeText(url.toString());
btn.setAttribute('data-copied', 'true');
setTimeout(() => btn.removeAttribute('data-copied'), 1200);
} catch {
// Fallback: update location so users can copy from address bar
window.location.hash = id;
}
});
Les API de presse-papiers ont des subtilités de permission dans les contextes embarqués. Votre solution de secours doit quand même fournir une URL copiable via la barre d’adresse.
Slugs stables : la partie que tout le monde sous-estime
Le travail : transformer le texte d’un titre en un id stable comme fast-diagnosis-playbook.
Le piège : les titres changent, la ponctuation change, des titres en double apparaissent, et différents renderers sluggent différemment.
L’approche correcte dépend du cycle de vie de votre contenu :
- Docs internes avec modifications fréquentes : autoriser des IDs explicites dans la source (extension Markdown) et encourager les auteurs à verrouiller des IDs pour les sections importantes.
- Docs publiques avec liens externes : la stabilité compte encore plus — préférez des IDs explicites pour les titres majeurs et versionnez l’algorithme de slugging.
Un algorithme de slug sensé (et les règles à documenter)
Choisissez des règles et n’improvisez pas après coup. Voici un ensemble pratique :
- Normaliser Unicode (NFKD) et supprimer les marques combinantes pour des slugs ASCII.
- Mettre en minuscule.
- Remplacer les séquences non alphanumériques par un seul tiret.
- Supprimer les tirets en début/fin.
- Garder un compteur de collision :
heading,heading-1,heading-2. - Permettre une surcharge explicite, par ex.
{#my-stable-id}en Markdown.
Si vous changez ces règles, vous cassez des liens. Ce n’est pas une théorie. Ça arrivera.
Vous ne pouvez pas rediriger fiablement les fragments côté serveur
Parce que les fragments ne sont pas envoyés au serveur, vous ne pouvez pas faire de redirections serveur « ancien fragment → nouveau fragment » de façon normale.
Vous pouvez faire un mapping côté client avec JavaScript au chargement de la page (lire location.hash, le mapper, définir location.hash).
C’est bancal mais parfois nécessaire pour des migrations.
Ne basez pas un modèle économique dessus. Mieux : gardez les IDs stables.
Accessibilité et UX : ne livrez pas mignon, livrez utilisable
Les permalinks de titres sont un endroit classique pour punir involontairement les utilisateurs clavier et lecteurs d’écran.
Vous ajoutez des contrôles interactifs à côté des titres, et les titres sont déjà des repères de navigation.
Exigences de base
- Clavier : le contrôle de permalien doit être atteignable par Tab et afficher un indicateur de focus visible.
- Lecteurs d’écran : le lien doit avoir un libellé significatif (par ex. « Permalien vers … »). L’icône est aria-hidden.
- Zone de frappe : ne livrez pas une cible de 10px. Rendez-la confortable sur les appareils tactiles.
- Réduction de mouvement : évitez les animations de scroll sophistiquées par défaut ; respectez
prefers-reduced-motion.
Défilement lisse : prudence
Le défilement lisse est agréable… jusqu’à ce qu’il ne le soit plus. Il peut rendre certains utilisateurs malades, et il aggrave les moments « où suis‑je » dans de longs documents.
Si vous l’activez globalement, assurez-vous que la réduction de mouvement le désactive.
cr0x@server:~$ cat motion.css
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
Toujours : les sauts d’ancre natifs sont rapides et prévisibles. Je n’active pas le défilement lisse sauf si le design l’exige.
Gestion du focus après un saut d’ancre
Quand vous cliquez un permalien, l’URL change et la page défile, mais le focus clavier peut rester sur le lien cliqué.
C’est acceptable. Ce qui ne l’est pas, c’est d’atterrir sur un titre sans contexte de focus visible si vous avez navigué via le clavier ou par script.
Une amélioration pragmatique : ajouter tabindex="-1" aux titres afin qu’ils puissent être focalisés par script, puis donner le focus à la cible sur hashchange.
Faites cela seulement si vous l’avez testé avec des technologies d’assistance réelles ; ne faites pas d’« accessibilité de façade » qui casse le comportement.
SEO et analytics : les ancres ne sont pas votre couche de routage
Les fragments d’ancre ne changent pas la route serveur, donc les crawlers et analytics les traitent différemment :
- Moteurs de recherche : indexent généralement l’URL de la page ; les fragments peuvent apparaître comme sitelinks dans certains cas mais ne sont pas une page séparée.
- Analytics : l’analytics côté serveur ne verra pas les fragments. Côté client, oui, mais vous devez l’implémenter.
- URL canoniques : ne mettez pas de fragments dans les canonicals ; les canonicals doivent pointer vers la page de base.
Si vous tenez à connaître « les sections les plus liées », ajoutez de l’instrumentation client sur hashchange et sur les clics de permalien.
Aussi : ne spammez pas les vues de page pour chaque changement de hash. Suivez-les comme événements.
Un maxim de fiabilité utile ici vient des écrits d’ingénierie de John Ousterhout. Idée paraphrasée : la complexité est la cause première de la plupart des défaillances logicielles
— John Ousterhout (idée paraphrasée).
Gardez cette fonctionnalité ennuyeuse.
Tâches pratiques : commandes, sorties et décisions
Cette section est délibérément opérationnelle. Chaque tâche inclut une commande, ce que signifie la sortie, et la décision à prendre.
Les exemples supposent une sortie de site statique dans ./dist et la source dans ./src. Adaptez au repo.
Tâche 1 : Trouver les IDs dupliqués dans le HTML construit
cr0x@server:~$ rg -n ' id="' dist | sed -n 's/.* id="\([^"]\+\)".*/\1/p' | sort | uniq -d | head
getting-started
troubleshooting
Signification de la sortie : au moins deux pages (ou une page) contiennent les mêmes valeurs de id. Dans une seule page, les duplicatas sont un bug de conformité.
Décision : si des duplicatas apparaissent dans le même fichier HTML, corrigez la gestion des collisions de slugs. Si les duplicatas se trouvent entre pages, c’est acceptable sauf si vous imbriquez des pages (SPA) ou avez un routage côté client fusionnant les DOM.
Tâche 2 : Vérifier les IDs dupliqués dans chaque fichier
cr0x@server:~$ for f in dist/**/*.html; do
> ids=$(perl -nE 'say $1 while / id="([^"]+)"/g' "$f" | sort)
> dups=$(printf "%s\n" "$ids" | uniq -d)
> if [ -n "$dups" ]; then
> echo "DUP IDs in $f"
> echo "$dups" | head
> fi
> done
DUP IDs in dist/runbook.html
mitigation
Signification de la sortie : dist/runbook.html a au moins deux éléments avec id="mitigation".
Décision : mettez à jour le générateur de slugs pour suffixer les collisions, ou exigez des IDs explicites pour les titres récurrents comme « Mitigation » et « Rollback ».
Tâche 3 : Auditer la hauteur de l’en-tête fixe dans le CSS calculé
cr0x@server:~$ rg -n 'position:\s*sticky|position:\s*fixed' src/styles -S
src/styles/header.css:14:position: sticky;
src/styles/header.css:15:top: 0;
Signification de la sortie : vous avez un en-tête fixe. Il occulte probablement les cibles d’ancre sans gestion d’offset.
Décision : définissez scroll-margin-top sur les titres ou scroll-padding-top sur le conteneur de défilement en utilisant la hauteur de l’en-tête.
Tâche 4 : Confirmer que vous appliquez des offsets quelque part
cr0x@server:~$ rg -n 'scroll-margin-top|scroll-padding-top' src -S
src/styles/anchors.css:6: scroll-margin-top: calc(var(--sticky-header-height) + 12px);
Signification de la sortie : les offsets sont implémentés en CSS.
Décision : validez la valeur de la variable selon les breakpoints ; si vous avez plusieurs en-têtes (bannière + nav), additionnez-les.
Tâche 5 : Lister les liens hash in-page et vérifier que les cibles existent
cr0x@server:~$ python3 - <<'PY'
import glob, re, sys
from collections import defaultdict
href_re = re.compile(r'href="#([^"]+)"')
id_re = re.compile(r' id="([^"]+)"')
for f in glob.glob("dist/**/*.html", recursive=True):
html = open(f, "r", encoding="utf-8").read()
hrefs = set(href_re.findall(html))
ids = set(id_re.findall(html))
missing = sorted(hrefs - ids)
if missing:
print(f"{f}: missing targets: {missing[:5]}")
PY
dist/index.html: missing targets: ['fast-diagnosis-playbook']
Signification de la sortie : la page référence #fast-diagnosis-playbook mais aucun élément ne possède cet ID (mismatch TOC, slug changé ou contenu manquant).
Décision : corrigez le renderer afin que la génération de la TOC et la génération des IDs de titres partagent la même source de vérité pour les slugs.
Tâche 6 : Détecter le churn des IDs de titres entre builds
cr0x@server:~$ git diff --name-only HEAD~1..HEAD | rg '\.md$' | head
src/docs/runbook.md
src/docs/storage.md
cr0x@server:~$ python3 - <<'PY'
import re, sys, pathlib
p = pathlib.Path("dist/runbook.html")
html = p.read_text(encoding="utf-8")
ids = re.findall(r'<h[2-4][^>]* id="([^"]+)"', html)
print("\n".join(ids[:20]))
PY
fast-diagnosis
common-mistakes
checklists
Signification de la sortie : vous pouvez capturer en snapshot les IDs par page et les comparer entre versions. Cet exemple affiche les 20 premiers IDs de titre.
Décision : ajoutez un job CI qui échoue si des IDs changent pour des titres non modifiés (vous aurez besoin d’un mapping source), ou au moins alertez sur un fort churn.
Tâche 7 : Vérifier que les contrôles de permalien ont des libellés accessibles
cr0x@server:~$ rg -n 'class="permalink"' dist | head -n 3
dist/runbook.html:42: <a class="permalink" href="#fast-diagnosis">
dist/runbook.html:88: <a class="permalink" href="#common-mistakes" aria-label="Permalink to Common mistakes">
dist/runbook.html:132: <a class="permalink" href="#checklists" aria-label="Permalink to Checklists / step-by-step plan">
Signification de la sortie : au moins un permalien manque d’un aria-label. Ce lien sera annoncé comme « lien » ou « # » sans contexte.
Décision : imposez un template d’aria-label dans votre renderer. Ne comptez pas sur les infobulles ; les lecteurs d’écran s’en moquent.
Tâche 8 : Vérifier l’existence du style :focus-visible
cr0x@server:~$ rg -n ':focus-visible' src/styles -S
src/styles/anchors.css:22:.doc .permalink:focus-visible {
Signification de la sortie : vous pensez au moins aux utilisateurs clavier.
Décision : si absent, ajoutez-le. S’il est présent, vérifiez le contraste en mode sombre. À défaut, vous aurez des plaintes de « piège clavier » en environnements entreprise.
Tâche 9 : Identifier si vous faites défiler la page ou un conteneur imbriqué
cr0x@server:~$ rg -n 'overflow:\s*(auto|scroll)' src/styles -S | head
src/styles/layout.css:31:overflow: auto;
Signification de la sortie : vous avez probablement un conteneur de défilement imbriqué (fréquent dans les shells d’app docs).
Décision : utilisez scroll-padding-top sur ce conteneur plutôt que (ou en plus de) scroll-margin-top sur les titres.
Si les sauts d’ancre n’atterrissent pas correctement, le défilement imbriqué en est souvent la cause.
Tâche 10 : Auditer le JavaScript qui « aide » le défilement d’ancre
cr0x@server:~$ rg -n 'location\.hash|hashchange|scrollIntoView' src -S
src/app/router.js:118:window.addEventListener('hashchange', onHashChange);
src/app/router.js:141:document.querySelector(hash).scrollIntoView({ behavior: 'smooth' });
Signification de la sortie : du code personnalisé intercepte la navigation par hash et fait défiler manuellement.
Décision : confirmez qu’il prend en compte les en-têtes fixes et les conteneurs imbriqués. Sinon, retirez-le et fiez-vous aux offsets CSS.
Le code de défilement manuel est une source fréquente de double-défilement, mauvais offset et violations de la réduction de mouvement.
Tâche 11 : Valider votre stratégie de collision dans le générateur
cr0x@server:~$ rg -n 'slug|permalink|heading.*id' src -S | head
src/build/slugify.js:3:function slugify(text) {
src/build/markdown.js:88: heading.id = slugify(heading.text);
cr0x@server:~$ sed -n '1,140p' src/build/slugify.js
function slugify(text) {
return text.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
module.exports = { slugify };
Signification de la sortie : ce slugify ne gère pas les collisions. Deux titres identiques produiront des IDs identiques.
Décision : implémentez le suffixage des collisions par page et ajoutez des tests. Envisagez aussi une normalisation Unicode si vous avez des titres non-ASCII.
Tâche 12 : Confirmer que les icônes de permalien apparaissent au survol et au focus clavier
cr0x@server:~$ rg -n 'opacity:\s*0|opacity:\s*1|h2:hover.*permalink|permalink:focus' src/styles/anchors.css
12: opacity: 0;
18:.doc h2:hover .permalink,
21:.doc .permalink:focus {
Signification de la sortie : l’icône est cachée par défaut et devient visible au survol et au focus. C’est le bon schéma.
Décision : vérifiez que l’icône est également visible sur les appareils tactiles (pas de survol). Une solution simple est de la rendre toujours visible sur petits écrans via une media query.
Tâche 13 : S’assurer que les entrées de la TOC correspondent aux IDs des titres
cr0x@server:~$ python3 - <<'PY'
import re, pathlib
html = pathlib.Path("dist/runbook.html").read_text(encoding="utf-8")
toc = re.findall(r'<nav[^>]*aria-label="Table of contents"[\s\S]*?</nav>', html)
if not toc:
print("No TOC nav found")
raise SystemExit(0)
toc_html = toc[0]
toc_hrefs = set(re.findall(r'href="#([^"]+)"', toc_html))
heading_ids = set(re.findall(r'<h[2-4][^>]* id="([^"]+)"', html))
missing = sorted(toc_hrefs - heading_ids)
extra = sorted(heading_ids - toc_hrefs)
print("TOC missing targets:", missing[:10])
print("Headings not in TOC:", extra[:10])
PY
TOC missing targets: []
Headings not in TOC: ['appendix-debug-notes']
Signification de la sortie : la TOC correspond aux cibles, mais un titre n’est pas inclus (peut-être intentionnel).
Décision : décidez d’inclure tous les titres ou seulement certains niveaux. Restez cohérent ; l’incohérence est ce qui embrouille les gens et casse les attentes.
Tâche 14 : Vérifier si le cache fait atterrir les gens sur de vieilles ancres
cr0x@server:~$ curl -I -s https://docs.example.invalid/runbook | sed -n '1,12p'
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=31536000, immutable
etag: "a1b2c3d4"
Signification de la sortie : un cache immuable longue durée sur le HTML est risqué si le HTML change alors que l’URL ne change pas. Les utilisateurs peuvent recevoir des pages obsolètes où les IDs ne correspondent plus aux liens actuels.
Décision : mettez un cache léger sur le HTML (ou avec revalidation), et cachez agressivement les assets (JS/CSS) avec noms fingerprintés. Si vous devez cacher le HTML à long terme, versionnez le chemin.
Playbook de diagnostic rapide
Quand les ancres « ne fonctionnent pas », les gens le décrivent mal : « Le lien est cassé. » Cela peut signifier dix modes de panne différents.
Voici l’ordre de triage pour trouver rapidement le goulot.
D’abord : l’ID cible existe-t-il dans le DOM ?
- Si vous contrôlez la page : voir la source (ou inspecter l’élément) et chercher l’ID.
- Si c’est du contenu construit : lancez une recherche grep/rg sur la sortie HTML (voir Tâche 5).
Si l’ID n’existe pas, arrêtez. Ce n’est pas un bug de scroll. C’est un mismatch de génération, un churn de slug, ou une collision.
Ensuite : l’ID est-il dupliqué ?
Les IDs dupliqués peuvent vous amener à la mauvaise section. On a l’impression que « les ancres sont instables » parce qu’on tombe parfois sur la première occurrence,
parfois l’ordre du DOM change avec l’hydratation.
Vérifiez les duplicatas par fichier (Tâche 2). Corrigez les collisions dans le générateur.
Troisième : le conteneur de défilement est-il bien celui que vous pensez ?
Si votre contenu principal défile dans un conteneur, les sauts d’ancre natifs peuvent défiler la page et non le conteneur,
ou ils peuvent sauter mais apparaître « faux » parce que le padding supérieur du conteneur n’est pas réglé.
Cherchez overflow: auto ou scroll (Tâche 9). Appliquez scroll-padding-top au conteneur de défilement réel.
Quatrième : offset d’en-tête fixe (CSS d’abord)
Si l’ancre existe et est unique, mais que le titre est caché sous l’en-tête, c’est un problème d’offset.
Utilisez scroll-margin-top sur les titres. Évitez les hacks JS sauf si vous avez une hauteur dynamique.
Cinquième : le JavaScript intercepte la navigation par hash
Le code de routeur, le code de scroll lisse ou le code analytics peuvent bloquer le comportement natif d’ancre.
Trouvez hashchange, preventDefault, et l’utilisation de scrollIntoView (Tâche 10).
Supprimez la plupart de ça. Sérieusement. La navigation d’ancre native a des décennies de robustesse. Vos 40 lignes « d’aide » n’en ont pas.
Erreurs courantes (symptôme → cause racine → correction)
Symptôme : le lien atterrit sur la bonne section, mais le titre est caché
Cause racine : l’en-tête fixe superpose le contenu et vous n’en avez pas tenu compte.
Correction : appliquez scroll-margin-top aux titres ou scroll-padding-top au conteneur de défilement. Gardez l’offset dans des variables CSS par breakpoint.
Symptôme : le lien atterrit sur la mauvaise section qui porte le même nom
Cause racine : IDs dupliqués dus à des titres répétés (ex. plusieurs « Résumé ») sans gestion des collisions.
Correction : implémentez le suffixage des collisions par page dans la génération de slugs ; encouragez des IDs explicites pour les titres opérationnels répétés.
Symptôme : les ancres fonctionnent localement mais échouent en production
Cause racine : HTML mis en cache de façon trop agressive ou pipeline de rendu différent en production qui génère des slugs différents.
Correction : alignez le slugging entre environnements ; cachez le HTML avec revalidation ; fingerprintez les assets ; ajoutez un contrôle de stabilité des IDs au build.
Symptôme : les ancres fonctionnent au rechargement complet, pas lors de la navigation dans le shell SPA
Cause racine : le routeur côté client empêche le comportement par défaut du hash, ou le contenu est injecté après la navigation donc l’élément n’existe pas encore.
Correction : à la fin de la navigation, si location.hash existe, scrollez vers la cible après le rendu. Préférez scrollIntoView sur la cible et incluez la gestion d’offset via CSS.
Symptôme : les liens de la TOC ne correspondent pas aux titres
Cause racine : la TOC est générée depuis le texte brut des titres tandis que les IDs sont générés depuis le texte traité (ou vice versa), ou la pipeline utilise deux fonctions de slug.
Correction : une seule source de vérité pour les slugs. Exportez-la comme module partagé et testez-la avec des cas de référence.
Symptôme : l’icône au survol apparaît, mais les utilisateurs clavier ne la trouvent pas
Cause racine : l’icône est cachée via display: none ou seulement montrée au survol, pas au focus.
Correction : cachez-la avec opacity/visibility, et affichez-la sur :focus / :focus-visible. Assurez-vous que Tab atteint le contrôle.
Symptôme : la copie d’un lien de section donne parfois l’URL complète, parfois seulement #fragment
Cause racine : les utilisateurs copient depuis différentes surfaces UI (barre d’adresse vs clic droit sur le lien vs sélection), et votre UI ne les guide pas.
Correction : contrôle « Copier le lien » optionnel qui copie toujours l’URL complète ; sinon gardez le permalien comme une ancre normale pour que le clic droit → copier fonctionne.
Symptôme : la page ressemble à un musée d’icônes de chaîne
Cause racine : les icônes de permalien sont toujours visibles, y compris pour les petits titres et les références API denses.
Correction : afficher au survol/focus, et activer sélectivement pour des niveaux de titres (typiquement H2–H4). Sur mobile, envisagez toujours visible mais subtil.
Blague n°2 : Si vous pensez que les IDs dupliqués « n’arriveront probablement pas », félicitations — vous venez de créer le bug le plus réutilisable de l’entreprise.
Listes de contrôle / plan pas à pas
Checklist : livrer des ancres niveau docs en une semaine
- Choisir le pattern : bouton de permalien à côté des titres (recommandé) ou titres cliquables. Décidez maintenant.
- Définir les règles de slug : documentez-les dans le repo. Incluez la gestion des collisions et le comportement Unicode.
- Implémenter une fonction slug unique : utilisée par les IDs de titres, la TOC et tout générateur de références croisées.
- Activer les IDs explicites : permettre aux auteurs d’épingler des IDs pour les sections critiques (runbooks, SOP, documents légaux).
- Offset CSS : appliquer
scroll-margin-topet définir--sticky-header-heightpar breakpoint. - Affordance survol/focus : montrer le permalien au survol et au focus-visible.
- Passage accessibilité : aria-labels, zones de frappe, style de focus, réduction de mouvement.
- Validation CI : échouer les builds sur IDs dupliqués par page et mismatch TOC-cible.
- Vérification politique de cache : éviter un cache immuable longue durée sur le HTML sauf si les chemins sont versionnés.
- Plan de migration : si vous changez la logique de slug, décidez comment préserver les anciens IDs ou les mapper côté client.
Checklist : gates CI qui attrapent vraiment les régressions d’ancres
- IDs dupliqués par fichier HTML (échec dur).
- Les hrefs de la TOC ciblent existent (échec dur).
- Les contrôles de permalien ont un
aria-label(échec dur). - Optionnel : détecter un fort churn d’IDs vs release précédente (échec soft / alerte).
- Optionnel : s’assurer qu’il n’y a pas de titres sans IDs pour certains types de docs (runbooks, handbooks).
Trois mini-histoires d’entreprise (anonymisées, techniquement exactes)
Mini-histoire 1 : l’incident causé par une fausse hypothèse
Une entreprise de taille moyenne disposait d’un « Production Runbook » interne, rendu depuis Markdown dans un shell SPA.
Ils avaient des permalinks sur les titres et une TOC. Tout le monde y faisait confiance. Ça avait survécu à plusieurs réorganisations, ce qui est essentiellement l’immortalité.
Puis ils ont redessiné la navigation en haut : un en-tête fixe est passé d’une ligne à deux, plus une bannière d’incident qui apparaissait lors d’événements majeurs.
Le changement front-end a été déployé un vendredi après-midi parce que le diff CSS semblait anodin et personne ne voulait retarder la release.
La fausse hypothèse : « les liens d’ancrage fonctionneront toujours ; le navigateur gère ça. »
Le lundi suivant, un incident. Quelqu’un a collé un lien vers la section « Disable autoscaling ». La page a chargé, a défilé, et… le titre était caché.
En temps calme c’est une gêne mineure. Pendant un incident en direct c’est devenu un bug de coordination :
trois personnes pensaient lire les mêmes instructions, mais deux lisaient en réalité la section précédente.
Le souci immédiat n’était pas la présence d’un en-tête fixe. C’était que l’offset était codé en dur pour l’ancienne hauteur d’en-tête.
Pire, la bannière d’incident n’apparaissait qu’en production, donc les tests locaux ne la voyaient jamais.
La correction fut ennuyeuse et efficace : scroll-margin-top sur les titres avec une variable CSS définie par le composant d’en-tête,
et une variable additive pour la bannière quand elle est présente. Aucun calcul JS, pas de thrash layout.
Mini-histoire 2 : l’optimisation qui s’est retournée contre eux
Une autre organisation a voulu « optimiser » le rendu des docs. Ils ont extrait la génération de slugs dans un package partagé utilisé par plusieurs produits.
Bon objectif. Puis ils ont « amélioré » l’algorithme : auparavant il préservait les tirets et compressait les espaces ; maintenant il normalisait plus la ponctuation,
retirait des stop words, et tronquait la longueur « pour la propreté ».
Le changement a réduit les IDs longs et moches. Il a aussi détruit la stabilité des permalinks.
Des titres comme « How to roll back: API gateway » et « How to roll back – API gateway » sont devenus le même ID.
Le suffixage des collisions n’avait pas été implémenté parce que la librairie était « pure » et ne gardait pas d’état par page.
Le premier signal n’a pas été un rapport de bug des lecteurs de docs. Ce sont les ingénieurs d’astreinte qui se sont plaints que les liens dans d’anciens tickets d’incident ne marchaient plus.
Voilà le côté amusant des deep links : ils sont utilisés par des gens occupés et irrités, et ce sont précisément ceux qu’il ne faut pas agacer.
Revenir sur l’algorithme fut plus difficile qu’attendu. Des pages avaient déjà été partagées avec des partenaires externes.
Ils ont fini par implémenter un mapping de fragments côté client pour les anciens IDs les plus fréquents et réintroduire l’ancien algorithme de slug
avec un flag de version. Ils ont aussi ajouté le suffixage des collisions. L’« optimisation » leur a coûté des semaines et beaucoup de crédibilité.
Mini-histoire 3 : la pratique ennuyeuse qui a sauvé la situation
Une grande entreprise disposait d’une plateforme de contenu générant plusieurs sorties : docs publiques, docs internes, PDFs et un bundle HTML hors ligne
pour environnements restreints. C’est un champ de mines pour les ancres parce que chaque renderer veut faire à sa façon.
Ils ont fait une chose extrêmement ennuyeuse : ils ont rédigé un « Contrat d’ID de titre » et l’ont traité comme une API.
Il définissait les règles de slug, le comportement des collisions et quand les IDs explicites étaient obligatoires. Il incluait aussi des cas de test :
chaînes avec ponctuation, Unicode, titres répétés et cas limites comme « C++ » et « S3 / IAM ».
Puis ils l’ont appliqué en CI sur toutes les sorties. Pas « bonne volonté ». Échec dur.
Chaque renderer devait utiliser la même fonction de slug, et chaque build exécutait un scan d’IDs dupliqués plus une vérification des cibles de la TOC.
Des mois plus tard ils ont migré le shell du site, incluant un nouvel en-tête fixe et un nouveau pipeline Markdown.
La migration a connu le chaos habituel — sauf que les permalinks sont restés stables. Les anciens tickets et runbooks ont continué à fonctionner.
Voilà à quoi ressemble le « ennuyeux mais correct » : personne ne les a félicités, et la production n’a pas pris feu.
FAQ
Dois‑je mettre l’id sur l’élément titre ou sur une ancre enfant ?
Mettez-le sur l’élément titre (ex. <h2 id="...">) sauf si votre renderer rend cela pénible.
C’est sémantiquement propre et ça fonctionne bien avec scroll-margin-top.
Ai‑je besoin de JavaScript pour que les permalinks au survol fonctionnent ?
Non. Le comportement survol/focus se fait en CSS. Le JavaScript est optionnel pour le « Copier le lien » dans le presse-papiers et pour des cas spéciaux comme le timing de navigation SPA.
Quelle est la meilleure façon de gérer les offsets d’en-tête fixe ?
CSS d’abord : scroll-margin-top sur les titres et/ou scroll-padding-top sur le conteneur de défilement.
Le défilement par JS est le dernier recours.
Pourquoi certaines ancres ne fonctionnent-elles qu’après un rechargement complet dans les SPA ?
Parce que l’élément n’est pas dans le DOM quand la navigation par hash se produit, ou le routeur empêche le comportement par défaut.
Corrigez en scrollant après le rendu et en vous assurant que votre conteneur de contenu est la cible de défilement.
Comment m’assurer que les IDs de titre ne changent pas quand le texte du titre change ?
Autorisez des IDs explicites dans l’édition (ex. extension Markdown) et utilisez-les pour les sections « stables ».
Sinon, tout système de slug basé sur le texte change quand le texte change.
Est‑ce acceptable d’utiliser des caractères non‑ASCII dans les IDs ?
Les navigateurs peuvent les gérer, mais l’interopérabilité entre outils (linters, processeurs, contextes copie/coller) est meilleure avec de l’ASCII.
Si vous avez du contenu multilingue, envisagez des IDs explicites ou une stratégie robuste de normalisation Unicode.
Puis‑je « rediriger » d’anciennes ancres vers de nouvelles ?
Pas côté serveur de la manière habituelle, parce que les fragments ne sont pas envoyés au serveur.
Vous pouvez faire un mapping côté client au chargement en lisant location.hash et en le réécrivant, mais c’est un hack de migration, pas une fondation.
L’icône de permalien doit‑elle être visible en permanence ?
Généralement non : ça ajoute du bruit. Affichez au survol et au focus-visible. Sur les appareils tactiles, envisagez toujours visible mais subtil, ou l’afficher au tap via une cible de frappe plus large.
Comment tester cela sans une suite d’automatisation navigateur complète ?
Commencez par des contrôles HTML au build : IDs dupliqués, cibles manquantes, présence d’aria-label. Puis faites une passe manuelle : tabulation clavier, atterrissage sous l’en-tête fixe, et cibles de tap mobile.
Quelle est la chose qu’on oublie le plus au sujet des ancres ?
Que les IDs deviennent des dépendances externes. Les gens les collent dans des tickets, chats, postmortems et automatisations. Les changer à la légère, c’est comme renommer une méthode d’API publique.
Conclusion : prochaines étapes réalisables cette semaine
Si vous voulez des ancres qui ressemblent à un vrai site de documentation, arrêtez de les traiter comme de la décoration.
Ce sont de la navigation, des outils de collaboration et une infrastructure de réponse aux incidents — juste avec une interface UI.
Faites ceci ensuite :
- Implémentez des offsets CSS (
scroll-margin-top) liés à la variable de hauteur de votre en-tête fixe. - Standardisez le slugging avec gestion des collisions et une implémentation partagée.
- Ajoutez des contrôles CI pour les IDs dupliqués et les mismatch TOC-cibles.
- Rendez les permalinks accessibles : libellés, focus-visible et zones de frappe raisonnables.
- Décidez d’une politique de stabilité : IDs explicites pour les runbooks et docs « pour toujours ».
Vous livrerez quelque chose qui ressemble à un site de docs. Plus important encore, vous livrerez quelque chose qui se comporte comme tel sous contrainte.