Files
ephron-ren-prd/prd-blog-toc-scroll-fix.md

7.2 KiB
Raw Permalink Blame History

博客目录跳转被导航栏遮盖问题

版本: v1.0
日期: 2026-05-06
状态: 📝 待评审


一、问题描述

1.1 现象

博客正文页(/posts/{slug}右侧有目录TOC点击目录项可以跳转到对应章节。但跳转后目标标题被顶部导航栏遮盖用户需要手动向上滚动才能看到标题。

1.2 复现步骤

  1. 访问任意博客文章(如 https://blog.ephron.ren/posts/hermes-chrome-opencode-ai-agent-bug
  2. 滚动页面使右侧目录可见
  3. 点击任意目录项(如「案例一:卡片显示不全」)
  4. 观察页面跳转后的滚动位置

预期行为:标题完整可见,位于导航栏下方
实际行为:标题被导航栏遮盖,只能看到标题下半部分

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" 的链接时,浏览器会执行以下操作:

  1. 找到 id="heading-0" 的元素
  2. 将该元素滚动到视口顶部(scrollIntoView 的默认行为)
  3. 由于导航栏是 position: fixed,它始终在视口顶部
  4. 标题被滚动到视口顶部后,立即被 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 验证方法

  1. 部署后访问博客文章
  2. 打开浏览器开发者工具
  3. 点击目录项,观察 scroll-margin-top 是否生效
  4. 检查标题是否位于导航栏下方

六、风险与注意事项

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:目录生成和高亮逻辑

B. 参考资料