真正能用的方案是用 javascript 扫描页面 h2~h4 标题,用 mutationobserver 监听 dom 变化以保持同步,清理文本生成 id,结合 intersectionobserver 观察章节容器实现精准高亮,并用 scroll-margin-top 修复锚点偏移。

HTML里怎么让侧边栏目录自动对应标题层级
靠手写 <ul><li></ul> 套来套去,不仅容易漏级、错序,还会在改正文标题时彻底脱节。真正能用的方案是:用 JavaScript 扫描页面里的 <h2></h2>~<h4></h4>,生成带锚点的嵌套列表,再插入到侧边栏容器里。
关键不是“生成目录”,而是“保持同步”——正文增删标题、拖动顺序、甚至用 JS 动态加载内容后,目录都得跟着变。所以不能只在页面加载时跑一次 document.querySelectorAll('h2, h3, h4') 就完事。
- 必须监听 DOM 变化(用
MutationObserver),尤其当博客用 Markdown 渲染器(如marked或remark)异步生成标题时 - 标题文本里含 HTML 标签(比如
<code>foo)要先用textContent提取纯文本,否则生成的目录项会显示<code>foo</code> - 为每个标题补上
id(如果原生没有):用正则清理标题文字(去掉空格、中文、标点),转成小写连字符格式,比如 “JS 中的 event loop” →js-zhong-de-event-loop
为什么用 position: sticky 做侧边栏滚动跟随很危险
表面看 position: sticky; top: 1rem; 确实能让目录一直停在视口里,但实际踩坑率极高:只要父容器有 overflow: hidden|auto、或用了 transform、甚至某些 Flex/Grid 布局,sticky 就直接失效,目录会跟着页面一起滚走。
更稳的替代方案是监听 scroll 事件 + getBoundingClientRect() 手动控制定位,虽然多几行代码,但行为完全可控。
立即学习“前端免费学习笔记(深入)”;
- 别把侧边栏放在
<main></main>内部——它应该和<header></header>并列,避免被任何局部 overflow 截断 - 用
requestAnimationFrame节流滚动监听,否则快速滚动时 CPU 占用飙升 - 判断是否该“吸顶”的依据不是固定像素值,而是侧边栏顶部距离视口顶部的距离(
rect.top )
IntersectionObserver 能不能用来高亮当前章节
能,而且比轮询 scrollTop 更轻量、更准确。但它对“当前章节”的定义很关键:不是看哪个 <h2></h2> 最靠近顶部,而是看哪个章节区域(<section></section> 或两个 <h2></h2> 之间的内容块)占满了视口中间 60% 区域。
直接拿 IntersectionObserver 观察所有 <h2></h2> 元素,反而会误判——因为标题本身高度小,很容易刚进视口就触发,而用户其实还没看到正文。
- 观察目标应该是每个章节的容器(比如
<article class="chapter"></article>),不是标题本身 -
threshold: [0.3, 0.6]比单个0.5更可靠:既防抖,又能覆盖不同屏幕尺寸下的可视比例变化 - 高亮逻辑要加防抖:同一时间只允许一个
active类存在,避免多个标题同时亮起
静态博客生成器里怎么绕过 JS 实现基础 TOC
如果你用的是 Hugo / Jekyll / Hexo 这类静态生成器,根本不需要运行时 JS —— 它们在构建阶段就能解析 Markdown 的标题,生成完整的 HTML 目录结构。优势是秒出、无水合问题、SEO 友好。
但代价是:一旦你用客户端 JS 改了页面内容(比如展开折叠区块、切换暗色模式导致标题样式重绘),这个预生成的 TOC 就和实际 DOM 对不上了。
- Hugo 用
{{ .TableOfContents }},但默认只输出<nav></nav>内容,不带 class;需配合 CSS 重置层级缩进(nav ul { padding-left: 1.2em; }) - Jekyll 的
{% toc %}插件默认不支持h4,要手动改插件源码或换用jekyll-toc并配置min_level: 2和max_level: 4 - Hexo 的
生成的锚点是#%E6%A0%87%E9%A2%98这种 URL 编码,需确保主题里a[href^="#"]的平滑滚动逻辑能正确 decode
最麻烦的其实是锚点跳转后,滚动位置偏移——比如被 fixed header 挡住标题。这和 TOC 生成方式无关,但几乎所有方案都会撞上,得统一用 scroll-margin-top 修,比如给每个 <h2></h2> 加 style="scroll-margin-top: 64px;"。











