自动生成侧边栏
根据目录结构自动生成侧边栏配置,减少手动维护工作。
方案一:基于文件系统
创建脚本
ts
// scripts/auto-sidebar.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
interface SidebarItem {
text: string
link: string
items?: SidebarItem[]
collapsed?: boolean
}
interface SidebarGroup {
text: string
collapsed?: boolean
items: SidebarItem[]
}
const docsDir = 'docs'
function getTitle(filePath: string): string {
try {
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
return data.title || path.basename(filePath, '.md')
} catch {
return path.basename(filePath, '.md')
}
}
function scanDirectory(dir: string, basePath: string = ''): SidebarItem[] {
const items: SidebarItem[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
// 排序:目录优先,然后按名称
entries.sort((a, b) => {
if (a.isDirectory() && !b.isDirectory()) return -1
if (!a.isDirectory() && b.isDirectory()) return 1
return a.name.localeCompare(b.name)
})
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
const relativePath = path.join(basePath, entry.name)
if (entry.isDirectory()) {
// 检查是否有 index.md
const indexPath = path.join(fullPath, 'index.md')
if (fs.existsSync(indexPath)) {
// 作为分组
items.push({
text: getTitle(indexPath),
link: `/${relativePath}/`,
items: scanDirectory(fullPath, relativePath)
.filter(item => item.link !== `/${relativePath}/`)
})
} else {
// 作为分组(无链接)
items.push({
text: entry.name,
items: scanDirectory(fullPath, relativePath)
})
}
} else if (entry.name.endsWith('.md') && entry.name !== 'index.md') {
items.push({
text: getTitle(fullPath),
link: `/${relativePath.replace('.md', '')}`
})
}
}
return items
}
function generateSidebar(): Record<string, SidebarGroup[]> {
const sidebar: Record<string, SidebarGroup[]> = {}
const entries = fs.readdirSync(docsDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith('.')) {
const fullPath = path.join(docsDir, entry.name)
sidebar[`/${entry.name}/`] = scanDirectory(fullPath, entry.name)
.map(item => ({
text: item.text,
collapsed: false,
items: item.items || (item.link ? [{ text: item.text, link: item.link }] : [])
}))
}
}
return sidebar
}
// 生成并输出
const sidebar = generateSidebar()
fs.writeFileSync('docs/.vitepress/sidebar.json', JSON.stringify(sidebar, null, 2))
console.log('侧边栏配置已生成')1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
使用生成的配置
ts
// .vitepress/config.mts
import sidebar from './sidebar.json'
export default defineConfig({
themeConfig: {
sidebar
}
})1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
方案二:运行时生成
创建插件
ts
// .vitepress/plugins/auto-sidebar.ts
import type { Plugin } from 'vite'
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
export function autoSidebarPlugin(): Plugin {
return {
name: 'vitepress-auto-sidebar',
configResolved(config) {
// 在配置解析后注入侧边栏
const docsDir = path.resolve(config.root, '..')
const sidebar = generateSidebar(docsDir)
// 注入到 VitePress 配置
const vitepressConfig = config as any
if (vitepressConfig.vitepress?.config) {
vitepressConfig.vitepress.config.themeConfig.sidebar = sidebar
}
}
}
}
function generateSidebar(docsDir: string) {
// ... 同上
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
注册插件
ts
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { autoSidebarPlugin } from './plugins/auto-sidebar'
export default defineConfig({
vite: {
plugins: [autoSidebarPlugin()]
}
})1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
方案三:基于 frontmatter 排序
ts
// scripts/auto-sidebar-with-order.ts
interface SidebarItem {
text: string
link: string
order?: number
}
function scanDirectory(dir: string): SidebarItem[] {
const items: SidebarItem[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
if (entry.name.endsWith('.md')) {
const filePath = path.join(dir, entry.name)
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
items.push({
text: data.title || entry.name.replace('.md', ''),
link: `/${path.relative('docs', filePath).replace('.md', '')}`,
order: data.order ?? 999
})
}
}
// 按 order 排序
return items.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在 frontmatter 中指定顺序:
yaml
---
title: 快速开始
order: 1
---
---
title: 安装配置
order: 2
---
---
title: 进阶用法
order: 3
---1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
完整实现示例
ts
// scripts/generate-sidebar.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
interface SidebarItem {
text: string
link?: string
items?: SidebarItem[]
collapsed?: boolean
badge?: { text: string; type: string }
}
interface SidebarConfig {
[path: string]: SidebarItem[]
}
const IGNORE_DIRS = ['.vitepress', 'public']
const IGNORE_FILES = ['index.md']
function getTitle(filePath: string): string {
try {
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
return data.title || path.basename(filePath, '.md')
} catch {
return path.basename(filePath, '.md')
}
}
function getBadge(filePath: string): { text: string; type: string } | undefined {
try {
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
if (data.badge) {
return data.badge
}
// 根据标签推断
if (data.tags?.includes('新')) return { text: '新', type: 'tip' }
if (data.tags?.includes('推荐')) return { text: '推荐', type: 'tip' }
return undefined
} catch {
return undefined
}
}
function getOrder(filePath: string): number {
try {
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
return data.order ?? 999
} catch {
return 999
}
}
function scanDir(dir: string, basePath: string = ''): SidebarItem[] {
const items: SidebarItem[] = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
.filter(e => !e.name.startsWith('.'))
// 收集文件和目录
const files: fs.Dirent[] = []
const dirs: fs.Dirent[] = []
for (const entry of entries) {
if (entry.isDirectory() && !IGNORE_DIRS.includes(entry.name)) {
dirs.push(entry)
} else if (entry.isFile() && entry.name.endsWith('.md') && !IGNORE_FILES.includes(entry.name)) {
files.push(entry)
}
}
// 处理文件
const fileItems: SidebarItem[] = files.map(file => {
const filePath = path.join(dir, file.name)
return {
text: getTitle(filePath),
link: `/${basePath}/${file.name.replace('.md', '')}`,
badge: getBadge(filePath)
}
}).sort((a, b) => getOrder(path.join(dir, a.text)) - getOrder(path.join(dir, b.text)))
items.push(...fileItems)
// 处理目录
for (const dirEntry of dirs) {
const subDir = path.join(dir, dirEntry.name)
const subItems = scanDir(subDir, `${basePath}/${dirEntry.name}`)
if (subItems.length > 0) {
const indexPath = path.join(subDir, 'index.md')
items.push({
text: fs.existsSync(indexPath) ? getTitle(indexPath) : dirEntry.name,
link: fs.existsSync(indexPath) ? `/${basePath}/${dirEntry.name}/` : undefined,
items: subItems,
collapsed: true
})
}
}
return items
}
function generateSidebar(): SidebarConfig {
const sidebar: SidebarConfig = {}
const entries = fs.readdirSync('docs', { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory() && !IGNORE_DIRS.includes(entry.name)) {
const items = scanDir(path.join('docs', entry.name), entry.name)
if (items.length > 0) {
sidebar[`/${entry.name}/`] = items
}
}
}
return sidebar
}
// 生成配置
const sidebar = generateSidebar()
const outputPath = 'docs/.vitepress/sidebar.json'
fs.writeFileSync(outputPath, JSON.stringify(sidebar, null, 2))
console.log(`✅ 侧边栏配置已生成: ${outputPath}`)
console.log(`📊 共 ${Object.keys(sidebar).length} 个模块`)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
构建时自动运行
json
// package.json
{
"scripts": {
"predev": "tsx scripts/generate-sidebar.ts",
"prebuild": "tsx scripts/generate-sidebar.ts"
}
}1
2
3
4
5
6
7
2
3
4
5
6
7