上下篇导航(PrevNext)

自定义组件 · 连续阅读体验 · 阅读列表与仓库列表分别独立生效

动机

Quartz v4 默认没有文章底部的上下篇导航。用户在读完一篇文章后,需要回到侧边栏或首页才能找到下一篇——这是静态博客框架(Hexo、Hugo 等)早已标配的体验,Quartz 作为知识库框架缺失了这块。

因此自定义实现了 PrevNext 组件,按日期排序提供线性阅读路径,让读者可以沉浸式连续浏览。

架构总览

allFiles(全站内容,按日期排序) 全站页面按 frontmatter.date 排序 → 再按当前页 section 过滤 → 线性导航 阅读列表/(PrevNext 生效) 文章1 ← → 文章2 ← → 文章3 ← → … 仓库列表/(PrevNext 独立生效) 仓库1 ← → 仓库2 ← → 仓库3 ← → … ConditionalRender: slug?.startsWith("阅读列表/") || "仓库列表/" 两个板块分别独立渲染 PrevNext,section 过滤确保不跨板块

核心设计:全站内容按日期排序是 PrevNext 的导航基础,但通过 ConditionalRender 将渲染限定在阅读列表板块内。仓库列表页面完全不出现上下篇导航——两个内容流保持独立。

实现细节

文件清单

文件作用
quartz/components/PrevNext.tsx核心逻辑:按日期排序全站文件,找到当前页前后位置,渲染导航链接
quartz/components/styles/prevNext.scss桌面/移动端布局,悬停效果,标题截断
quartz/i18n/locales/zh-CN.ts中文本地化:"上一篇" / "下一篇"
quartz/i18n/locales/en-US.ts英文本地化:"Previous" / "Next"
quartz.layout.ts布局注册:ConditionalRender 包裹,仅阅读列表生效
quartz/styles/custom.scss隐藏 Quartz 默认插入的分隔线

核心排序逻辑

// PrevNext.tsx — section 过滤 + 全站排序 + 定位当前页
const sectionPrefix = fileData.slug?.split("/")[0]  // 提取顶层目录名
const pages = allFiles
  .filter((f) =>
    f.dates &&                          // 必须有日期
    f.slug !== "index" &&               // 排除首页
    !f.slug?.startsWith("tags/") &&     // 排除标签页
    f.slug?.startsWith(sectionPrefix + "/")  // 仅当前 section 的文件
  )
  .sort(byDateAndAlphabetical(cfg))     // 按日期降序 + 字母排序

const currentIndex = pages.findIndex((f) => f.slug === fileData.slug)

// 降序排列下,数组前一位置是更新的文章,后一位置是更老的。
// 交换索引方向使:「上一篇」→ 更老的文章,「下一篇」→ 更新的文章
const prev = pages[currentIndex + 1]   // 上一篇(更老)
const next = pages[currentIndex - 1]   // 下一篇(更新)

布局注册

// quartz.layout.ts — afterBody 区
Component.ConditionalRender({
  component: Component.PrevNext(),
  condition: (page) =>
    page.fileData.slug?.startsWith("阅读列表/") ||
    page.fileData.slug?.startsWith("仓库列表/"),
}),

设计决策

决策选择原因
是否内置自定义组件Quartz v4 无此功能,参考博客框架体验补齐
排序依据按文章 frontmatter date与侧边栏 Explorer 的排序保持一致,反映笔记整理顺序
作用范围阅读列表 + 仓库列表两个板块分别独立——各自按日期排序,互不跨越
导航方向上一篇指向更老,下一篇指向更新降序排列下索引互换:prev = current+1(更老),next = current-1(更新)。符合列表最新在最前的直觉
边界处理首篇无「上一篇」,末篇无「下一篇」自然终止,不循环,不跨板块
移动端标题截断(15 字),垂直堆叠小屏空间有限,长标题会撑破布局
首页/标签页排除(slug !== "index"这些页面没有时间线概念