VitePress 侧边栏自动生成
组件简介
这个模块将 VitePress 的 sidebar 配置从手写硬编码改为基于文件系统自动生成。它的核心思路是扫描 posts/ 目录结构,按目录层级自动映射为侧边栏分组,从 markdown 文件的 # 标题自动提取显示文本,让新增文章时无需手动修改配置。
对于个人博客来说,文章数量会随时间持续增长。如果每次新增文章都要在 config.mts 里手动加一行 sidebar 条目,配置文件会越来越臃肿,也容易漏写或写错链接。自动生成之后,只要在对应目录下创建 .md 文件,侧边栏就会自动更新。
核心功能
- 根据
posts/目录结构自动递归生成 sidebar 配置 - 从 markdown 文件第一个
#标题自动提取文章显示名 - 通过映射表将目录名转为中文显示名(如
metaphysics-idle→玄学放置) - 支持嵌套子目录自动归组(如
front-end/deploy→部署相关) - 文件按 git 首次提交时间排序(文章创建顺序),git 不可用时回退到文件系统时间
- 自动去除标题中与父级分组名重复的前缀(如
玄学放置 ·)和后缀(如组件),避免侧边栏显示冗余 - 新增文章只需创建
.md文件,无需修改配置
改造前的问题
原来的 sidebar 配置直接写在 .vitepress/config.mts 中:
sidebar: [
{
text: '玄学放置',
collapsed: true,
items: [
{ text: '01 · 项目初始化与核心组件', link: '/posts/metaphysics-idle/01-init' },
{ text: '02 · 像素图标体系', link: '/posts/metaphysics-idle/02-pixel-icons' },
// 每新增一篇都要手动加一行……
]
},
// …
]每加一篇新文章就要改配置、拼链接,容易出错且配置文件越来越长。
实现思路
整体结构
新建 .vitepress/sidebar.ts,导出一个 generateSidebar() 函数,在 config.mts 中调用:
// config.mts
import { generateSidebar } from './sidebar'
export default defineConfig({
themeConfig: {
sidebar: generateSidebar(),
}
})目录名映射
通过 dirLabelMap 维护目录名到中文显示名的映射,新增分类目录时只需加一行:
const dirLabelMap: Record<string, string> = {
'front-end': '前端',
'components': '组件',
'metaphysics-idle': '玄学放置',
'my-life': '生活',
'deploy': '部署相关',
'eight-part': '八股',
}映射表中没有的目录名会直接作为显示名兜底,不会报错。
标题提取
从 markdown 文件的第一个 # 行读取文章标题,作为 sidebar 条目的 text。如果文件没有 # 标题,则从文件名推断(去掉 .md 扩展名和数字前缀),而不是回退到完整文件路径:
function extractTitle(filePath: string): string {
try {
const content = readFileSync(filePath, 'utf-8')
const match = content.match(/^#\s+(.+)$/m)
if (match) return match[1].trim()
} catch {
// 文件不可读时走下面的兜底
}
// 无 # 标题时从文件名推断,去掉扩展名和数字前缀(如 "01-init.md" → "init")
const base = filePath.replace(/\\/g, '/').split('/').pop()!.replace(/\.md$/, '')
return base.replace(/^\d+-/, '')
}不需要在 frontmatter 中额外声明 title,沿用项目已有的"首行 H1 即标题"约定。无标题的文件也能正常显示,但建议还是加上 # 标题以获得更友好的侧边栏文本。
递归扫描
scanDir 递归扫描目录,先处理当前层级的 .md 文件,再处理子目录:
- 当前目录下有
.md文件 → 直接作为条目 - 子目录下有
.md文件 → 归为一组,用dirLabelMap映射显示名 - 子目录中还有嵌套分组 → 自动生成多级结构
排序
文章按 git 首次提交时间排序,反映真实的创作顺序。通过 git log --diff-filter=A 获取每个文件的最早提交时间,git 不可用时回退到文件系统 birthtime。首次获取后缓存结果,避免重复执行 git 命令:
function getFileCreatedTime(filePath: string): string {
const cached = fileCreatedTimeCache.get(filePath)
if (cached !== undefined) return cached
try {
const time = execSync(
`git log --diff-filter=A --follow --format=%aI -1 -- "${filePath}"`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }
).toString().trim()
fileCreatedTimeCache.set(filePath, time)
return time
} catch {
// 回退到文件系统时间
}
}目录排序通过 dirOrder 数组指定优先级,未列出的按字母排序兜底:
const dirOrder = ['front-end', 'components', 'metaphysics-idle', 'my-life']折叠控制
通过 collapsedDirs 集中管理哪些目录默认折叠:
const collapsedDirs = new Set(['front-end', 'components', 'metaphysics-idle'])内容较多的分组默认收起,用户点击展开;内容少的(如"生活"只有一篇)默认展开。
标题去重
sidebar 条目的标题自动去除与父级分组名重复的前缀和后缀,避免在已分组的情况下重复显示:
function cleanTitle(title: string, parentLabel: string): string {
// 去除 "分组名 · " 前缀(如 "玄学放置 · xxx" → "xxx")
const prefix = `${parentLabel} · `
if (title.startsWith(prefix)) return title.slice(prefix.length)
// 去除 "分组名" 后缀(如 "xxx组件" → "xxx")
if (title.endsWith(parentLabel) && title.length - parentLabel.length >= 2) {
return title.slice(0, -parentLabel.length).trim()
}
return title
}实际效果:
| 分组 | 原标题 | 去重后 |
|---|---|---|
| 玄学放置 | 玄学放置 · 项目初始化与核心组件封装 | 项目初始化与核心组件封装 |
| 组件 | VitePress 文章元信息组件 | VitePress 文章元信息 |
| 组件 | VitePress 侧边栏自动生成 | VitePress 侧边栏自动生成(不变) |
注意 cleanTitle 只影响 sidebar 的显示文本,不修改文章正文的 H1 标题。
使用方式
源码位于 .vitepress/sidebar.ts,通过以下入口接入:
- 在
config.mts中引入并调用generateSidebar()
后续新增文章时,只需在 posts/ 对应目录下创建 .md 文件即可。新增分类目录时,在 dirLabelMap 和 dirOrder 中加一行映射。
扩展方向
- 如果后续文章数量进一步增长,可以改为"多 sidebar"模式——
sidebar配置为对象,按路径前缀返回不同的侧边栏,让每个分类只显示自己的文章列表 - 如果需要对单篇文章定制显示名,可以在 frontmatter 中加
sidebarTitle字段,在extractTitle中优先读取