Skip to content

自动生成侧边栏

根据目录结构自动生成侧边栏配置,减少手动维护工作。

方案一:基于文件系统

创建脚本

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('侧边栏配置已生成')

使用生成的配置

ts
// .vitepress/config.mts
import sidebar from './sidebar.json'

export default defineConfig({
  themeConfig: {
    sidebar
  }
})

方案二:运行时生成

创建插件

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) {
  // ... 同上
}

注册插件

ts
// .vitepress/config.mts
import { defineConfig } from 'vitepress'
import { autoSidebarPlugin } from './plugins/auto-sidebar'

export default defineConfig({
  vite: {
    plugins: [autoSidebarPlugin()]
  }
})

方案三:基于 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))
}

在 frontmatter 中指定顺序:

yaml
---
title: 快速开始
order: 1
---

---
title: 安装配置
order: 2
---

---
title: 进阶用法
order: 3
---

完整实现示例

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} 个模块`)

构建时自动运行

json
// package.json
{
  "scripts": {
    "predev": "tsx scripts/generate-sidebar.ts",
    "prebuild": "tsx scripts/generate-sidebar.ts"
  }
}

参考链接

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献