锚点id必须为ASCII字母数字加短横线、不以数字开头、全小写且严格唯一;多语言标题需统一清洗为合法id,href与id须字节级一致,DOM就位后才可生成目录并设置scroll-margin-top适配导航栏高度。

锚点 id 生成规则必须与语言无关
中文、日文、阿拉伯文标题直接转 id 会出问题,不是“编码失败”,而是浏览器根本找不到目标。id 必须是 ASCII 字母数字加短横线,且不能以数字开头。id="用户登录" 或 id="ログイン" 都无效,点击后 URL 变了但页面不动。
工程化做法是统一用脚本清洗:textContent.trim().replace(/[^a-z0-9]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase()。注意:这个逻辑必须在所有环境(服务端渲染、客户端挂载、SSR 后 hydration)中保持一致。Vue 的 mounted 和 React 的 useEffect 里执行,和 SSR 模板里生成的 id 不一致,就会导致首次加载时 #faq 跳不到位置。
- 禁止用
encodeURIComponent生成 id —— 它产出%E7%94%A8%E6%88%B7这类值,href="#%E7%94%A8%E6%88%B7"浏览器不认 - 英文带空格或标点的标题(如
"API v2.1: Error Handling")要转成api-v2-1-error-handling,不是api-v2.1-error-handling(小数点非法) - 多语言站点若共用同一套目录生成逻辑,需确保不同 locale 下的标题文本映射到相同 id —— 否则
href="#setup-guide"在 en-US 和 zh-CN 页面指向不同位置
href 和 id 值必须严格字节级一致
大小写、连字符数量、前后空格,差一个字符就断链。这不是“大概像就行”,而是浏览器匹配时完全区分大小写且不自动 trim。
常见翻车点:id="contact-us" 对应链接却写了 href="#Contact-us" 或 href="#contact--us"(两个短横线),或者服务端模板里拼了 href="#{{ section.id }}",但 JS 动态生成时用了 el.id.toLowerCase(),结果两边不一致。
立即学习“前端免费学习笔记(深入)”;
- 调试时直接在控制台运行
document.getElementById("contact-us"),返回 null 就说明目标不存在;返回元素再检查它是否真在 DOM 中(没被条件渲染隐藏或未挂载) - 用
location.hash手动设置后观察滚动行为,比点链接更快验证链路是否通 - 不要在 JS 里手拼 href 值,一律用
el.id直接取 —— 即使 el.id 是空字符串,也比猜逻辑安全
动态内容下锚点必须等 DOM 真正就位
Markdown 渲染器、CMS 富文本组件、React.lazy 异步组件插入标题后,id 属性可能晚于目录脚本执行。这时 document.querySelectorAll('h2, h3') 查不到元素,生成的目录里全是 href="#undefined"。
真正生效的前提不是“JS 加载完”,而是“目标元素已真实挂载并带合法 id”。SPA 中尤其明显:路由切换后新组件 mount,但目录没重绘;SSR 页面 hydrate 完成前,客户端脚本就跑了。
- React 推荐用
useLayoutEffect(非 useEffect)触发目录重建,确保 DOM 已同步更新 - Vue 用
nextTick+$refs.toc.$el确保节点存在后再遍历 - 纯 HTML 场景下,把脚本放在
</body>前,或监听DOMContentLoaded,别信window.onload(等图片太慢)
国际化页面的 scroll-margin-top 不能硬编码像素值
固定导航栏高度在不同语言下可能不同:中文字体行高更大、阿拉伯文右对齐影响布局、繁体字比简体字宽——导致 scroll-margin-top: 64px 在某些 locale 下仍遮挡标题。
这不是 CSS 写错,而是“64px”这个值本身不具备语义。实际需要的是“避开当前激活导航栏的视觉高度”,而这个高度由字体、行高、padding 共同决定,且随 locale 动态变化。
- 用 CSS 自定义属性:根元素设
--nav-height: 64px,各语言区块覆盖为html[lang="zh"] { --nav-height: 72px; } - JS 获取真实高度:用
getBoundingClientRect().height读导航栏 DOM,再设到目标元素的style.scrollMarginTop - 避免在
:target伪类里设 margin —— 它只响应 hash 变化,不解决滚动偏移问题



















