跳到正文
笔记库
阅读3 分钟成长中

目录、进度条、相关阅读:三样辅助,三次克制

阅读页只允许三样东西跟着读者走。它们的克制点各不相同:目录限层级、进度条限粗细、相关阅读限分数。

正文两侧的位置很贵。左边放什么、右边放什么、文末放什么,决定了读者会不会在读的过程中被拽走。

这一页只有三样辅助:目录、进度条、相关阅读。三样都有明确的"不许再往前一步"。

目录只到 h3

extractToc 在构建期扫原始 Markdown,只收 ## 和 ###:

const match = /^(#{2,3})\s+(.+?)\s*$/.exec(line)

两个细节:

  • 先剥掉围栏代码块。# 注释 在代码块里是语法,不是标题。stripFences 先把它整段删掉再扫。
  • 只到 h3。h4 在长文里会出现十几次,收进去之后目录本身变成第二篇正文,左栏从"地图"退化成"缩写版"。

左栏 130px、13px、弱化色,sticky 吸顶。它的信息密度必须明显低于正文,否则读者的视线会在两栏之间来回跳。

高亮不用 IntersectionObserver

useActiveSection 的做法很朴素:监听滚动,取"顶部已经越过 120px 线"的最后一个标题。

for (const id of ids) {
  const element = document.getElementById(id)
  if (!element) continue
  if (element.getBoundingClientRect().top - offset <= 0) current = id
}

没用 IO 的原因是它在这里不稳:IO 的 threshold 描述的是"元素露出多少",而当一节比视口还高时,多个标题会同时处于"可见"状态,谁 active 取决于回调顺序。用"最后一个越过顶线"来判定,语义和读者的感受一致。

还有一个边界:到底部时强制高亮最后一项。否则最后一节太短、永远越不过顶线,目录会一直停在倒数第二项。

进度条只有 2px

<div className="fixed inset-x-0 top-0 z-50 h-[2px] origin-left bg-brand/70"
     style={{ transform: `scaleX(${progress})` }} />

它是阅读页唯一随滚动变化的东西。用 scaleX 而不是 width:前者只动 transform,在合成器上跑;后者每帧触发布局,长文里能明显感到掉帧。

2px 是上限。再粗一点就会开始"被看见"——而它存在的意义是提供位置感,不是装饰。

相关阅读的分数可以解释

文末的「接着读」最多三条,打分规则写在 lib/content.ts:

score = 共同标签数 × 10 + (同主题 ? 4 : 0)

然后零分直接丢弃。为凑满三张卡而推荐一篇无关的,比空着更糟——读者点进去发现没关系,下次就不再信这个区块。

候选池是笔记 + 复盘,不只是笔记。只有十几篇内容时,单看同类凑不出三条;复盘里恰好有 tailwind vite mdx 这些标签,跨类型反而接得上。

排序里有一条容易被忽略的约束:不用 localeCompare。它依赖运行时的 ICU,Node 和浏览器可能给出不同顺序——预渲染和 hydration 一错位,React 就会报 mismatch。这里全是数值比较。

右栏刻意是空的

三栏里,右边 104px 只放成熟度、更新时间、标签三样。没有"热门文章"、没有"作者简介"、没有订阅框。

理由和目录一样:左右两栏的密度一旦接近正文,读者就会开始横向比较,而阅读需要的是纵向的连续。空着不是没内容可放,是决定不放。

接着读