7.2 KiB
7.2 KiB
博客目录跳转被导航栏遮盖问题
版本: v1.0
日期: 2026-05-06
状态: 📝 待评审
一、问题描述
1.1 现象
博客正文页(/posts/{slug})右侧有目录(TOC),点击目录项可以跳转到对应章节。但跳转后,目标标题被顶部导航栏遮盖,用户需要手动向上滚动才能看到标题。
1.2 复现步骤
- 访问任意博客文章(如
https://blog.ephron.ren/posts/hermes-chrome-opencode-ai-agent-bug) - 滚动页面使右侧目录可见
- 点击任意目录项(如「案例一:卡片显示不全」)
- 观察页面跳转后的滚动位置
预期行为:标题完整可见,位于导航栏下方
实际行为:标题被导航栏遮盖,只能看到标题下半部分
1.3 影响范围
- 影响所有博客文章页(
/posts/{slug}) - 影响所有使用目录跳转的用户
- 不影响首页、归档页等无目录页面
二、根因分析
2.1 技术细节
导航栏实现(blog/templates/base.html):
.site-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
min-height: 64px;
}
body {
padding-top: 64px; /* 补偿导航栏高度 */
}
目录实现(blog/templates/post.html):
// 生成目录链接
a.href = '#' + id; // 如 #heading-0
// 目录高亮判断
if (rect.top <= 120) {
current = heading.id;
}
2.2 问题根因
当点击 href="#heading-0" 的链接时,浏览器会执行以下操作:
- 找到
id="heading-0"的元素 - 将该元素滚动到视口顶部(
scrollIntoView的默认行为) - 由于导航栏是
position: fixed,它始终在视口顶部 - 标题被滚动到视口顶部后,立即被 fixed 导航栏覆盖
body { padding-top: 64px } 只对页面初始加载有效,对锚点跳转无效。
2.3 为什么目录高亮用了 120px?
if (rect.top <= 120) {
current = heading.id;
}
这里的 120px 是经验值(64px 导航栏 + 一些留白),用于判断标题是否「接近顶部」。但这只是高亮逻辑,不影响实际滚动位置。
三、解决方案
3.1 方案对比
| 方案 | 实现复杂度 | 兼容性 | 用户体验 | 推荐度 |
|---|---|---|---|---|
A. CSS scroll-margin-top |
⭐ 低 | 现代浏览器 | ⭐⭐⭐ 最佳 | ✅ 推荐 |
B. JS scrollIntoView |
⭐⭐ 中 | 全部 | ⭐⭐ 良好 | 备选 |
C. JS scrollTo + offset |
⭐⭐ 中 | 全部 | ⭐⭐ 良好 | 备选 |
3.2 推荐方案:CSS scroll-margin-top
原理:CSS scroll-margin-top 属性可以为元素设置滚动外边距,当该元素被滚动到视口时,会自动添加额外的顶部间距。
实现:
/* 为所有标题添加滚动外边距 */
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: 80px; /* 64px 导航栏 + 16px 留白 */
}
优点:
- 纯 CSS 实现,无需 JavaScript
- 浏览器原生支持,性能最佳
- 自动应用于所有锚点跳转(包括浏览器地址栏直接输入)
- 不影响现有 JS 逻辑
兼容性:
- Chrome 61+ ✅
- Firefox 68+ ✅
- Safari 14.1+ ✅
- Edge 79+ ✅
3.3 备选方案:JS scrollIntoView
如果需要支持旧版浏览器,可以用 JS 拦截点击事件:
// 拦截 TOC 链接点击
tocList.addEventListener('click', function(e) {
const link = e.target.closest('a');
if (!link) return;
e.preventDefault();
const targetId = link.getAttribute('href').substring(1);
const target = document.getElementById(targetId);
if (target) {
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
// 手动添加偏移
window.scrollBy(0, -80);
}
});
缺点:
- 需要处理
scrollBy的时序问题(scrollIntoView是异步的) - 可能出现闪烁(先跳到目标位置,再偏移)
- 不影响地址栏直接输入锚点的情况
四、实现细节
4.1 修改文件
blog/templates/post.html:在{% block extra_styles %}中添加 CSS
4.2 具体改动
在 .post-content h4 { ... } 之后添加:
/* 修复锚点跳转被导航栏遮盖 */
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: 80px;
}
4.3 偏移量计算
导航栏高度: 64px
额外留白: 16px
总计: 80px
如果未来导航栏高度变化,需要同步修改这个值。建议将其提取为 CSS 变量:
:root {
--nav-height: 64px;
--scroll-offset: 80px; /* nav-height + 16px */
}
.post-content h1,
.post-content h2,
.post-content h3,
.post-content h4,
.post-content h5,
.post-content h6 {
scroll-margin-top: var(--scroll-offset);
}
4.4 目录高亮逻辑调整
当前高亮逻辑使用 rect.top <= 120,这个值应该与 scroll-margin-top 保持一致:
// 当前
if (rect.top <= 120) {
// 建议改为
if (rect.top <= 80) {
或者提取为变量:
const SCROLL_OFFSET = 80; // 与 CSS --scroll-offset 保持一致
function updateTocHighlight() {
let current = '';
headings.forEach(heading => {
const rect = heading.getBoundingClientRect();
if (rect.top <= SCROLL_OFFSET) {
current = heading.id;
}
});
// ...
}
五、测试验证
5.1 测试用例
| 编号 | 测试步骤 | 预期结果 |
|---|---|---|
| T-001 | 点击目录中的 H2 标题 | 标题完整可见,位于导航栏下方 |
| T-002 | 点击目录中的 H3 标题 | 标题完整可见,位于导航栏下方 |
| T-003 | 浏览器地址栏输入 #heading-0 |
标题完整可见 |
| T-004 | 快速连续点击多个目录项 | 无闪烁,每次跳转位置正确 |
| T-005 | 移动端视口下点击目录 | 行为一致(目录在移动端隐藏,此项可跳过) |
5.2 验证方法
- 部署后访问博客文章
- 打开浏览器开发者工具
- 点击目录项,观察
scroll-margin-top是否生效 - 检查标题是否位于导航栏下方
六、风险与注意事项
6.1 风险
- 无:
scroll-margin-top是纯 CSS 属性,不影响现有布局和交互 - 兼容性:仅影响旧版浏览器(<2020 年),可忽略
6.2 注意事项
- 如果未来导航栏高度变化,需要同步修改
--scroll-offset变量 - 目录高亮逻辑的阈值应与
scroll-margin-top保持一致
七、优先级与排期
| 优先级 | 任务 | 预估时间 |
|---|---|---|
| P0 | 添加 scroll-margin-top CSS |
5 分钟 |
| P1 | 提取 CSS 变量 | 5 分钟 |
| P2 | 调整目录高亮逻辑阈值 | 5 分钟 |
总计:15 分钟
附录
A. 相关文件
blog/templates/base.html:导航栏定义blog/templates/post.html:目录生成和高亮逻辑