Skip to content

VitePress侧边栏自动生成实现

前言

在使用VitePress搭建博客时,一个常见的需求是根据文章自动生成侧边栏导航。本文将详细介绍如何实现一个支持多语言、按年月分组的侧边栏自动生成工具。

实现目标

  1. 自动扫描文章目录,提取文章信息
  2. 支持中英文两种语言
  3. 按年月对文章进行分组展示
  4. 自动生成侧边栏配置数据

核心实现

1. 数据结构设计

首先定义所需的接口类型:

typescript
// generate-sidebar.ts
// 文章元数据接口
interface ArticleMeta {
  title: string
  date: string
  tags?: string[]
  excerpt?: string
  url: string
  lang: string
}

// 侧边栏项目接口
interface SidebarItem {
  text: string
  link?: string
  items?: SidebarItem[]
  collapsed?: boolean
}

// 侧边栏配置接口
interface SidebarConfig {
  [path: string]: SidebarItem[]
}

2. 文章扫描与解析

使用fast-glob库扫描文章目录,并通过gray-matter解析文章frontmatter:

typescript
// generate-sidebar.ts
const files = await glob(['zh/**/*.md', 'en/**/*.md'], {
  cwd: srcDir,
  ignore: [
    'node_modules/**',
    '.vitepress/**'
  ],
  absolute: false,
  onlyFiles: true
})

// 解析文章信息
for (const file of files) {
  const content = fs.readFileSync(fullPath, 'utf-8')
  const { data: frontmatter } = matter(content)
  
  // 跳过非文章页面
  if (frontmatter.noneArticle) continue
  
  const article: ArticleMeta = {
    title: frontmatter.title || path.basename(file, '.md'),
    date: frontmatter.date || new Date().toISOString(),
    tags: frontmatter.tags || [],
    excerpt: frontmatter.excerpt,
    url: `/${file.replace(/index\.md$/, '')}`,
    lang: file.startsWith('zh/') ? 'zh' : 'en'
  }
}

TIP

对于不需要出现在侧边栏中的特殊页面,可以在frontmatter中添加noneArticle: true标记。 例如:

markdown
---
layout: Articles
title: 文章
noneArticle: true
---

3. 文章分组处理

按语言、年、月对文章进行分组:

typescript
// generate-sidebar.ts
const articles: {
  [lang: string]: {
    dates: { [year: string]: { [month: string]: ArticleMeta[] } }
  }
} = {
  zh: { dates: {} },
  en: { dates: {} }
}

// 将文章按日期分组
const date = new Date(article.date)
const year = date.getFullYear().toString()
const month = (date.getMonth() + 1).toString().padStart(2, '0')

if (!articles[lang].dates[year]) {
  articles[lang].dates[year] = {}
}
if (!articles[lang].dates[year][month]) {
  articles[lang].dates[year][month] = []
}
articles[lang].dates[year][month].push(article)

4. 生成侧边栏配置

将分组后的文章数据转换为VitePress侧边栏配置格式:

typescript
// generate-sidebar.ts

const MONTH_NAMES = [
  'January', 'February', 'March', 'April', 'May', 'June',
  'July', 'August', 'September', 'October', 'November', 'December'
];

const sidebarConfig: SidebarConfig = {}

for (const lang of ['zh', 'en']) {
  const langPrefix = `/${lang}/articles/`
  const isZh = lang === 'zh'

  sidebarConfig[langPrefix] = Object.entries(articles[lang].dates)
    .sort(([a], [b]) => b.localeCompare(a)) // 年份降序
    .map(([year, months]) => ({
      text: isZh ? `${year}年` : year,
      items: Object.entries(months)
        .sort(([a], [b]) => b.localeCompare(a)) // 月份降序
        .map(([month, posts]) => ({
          text: isZh 
            ? `${month}月 (${posts.length})` 
            : `${MONTH_NAMES[parseInt(month) - 1]} (${posts.length})`,
          collapsed: false,
          items: posts
            .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
            .map(post => ({
              text: post.title,
              link: post.url
            }))
        }))
    }))
}

5. 配置文件生成

将生成的配置数据写入JSON文件:

typescript
// generate-sidebar.ts
const dataDir = path.resolve(__dirname, '../data')
if (!fs.existsSync(dataDir)) {
  fs.mkdirSync(dataDir, { recursive: true })
}

fs.writeFileSync(
  path.join(dataDir, 'sidebar-data.json'),
  JSON.stringify(sidebarConfig, null, 2)
)

使用方式

  1. 在VitePress配置文件中加载生成的侧边栏数据:
typescript
// config.mts
function loadSidebarData() {
  const sidebarPath = path.resolve(__dirname, '../theme/data/sidebar-data.json')

  try {
    if (fs.existsSync(sidebarPath)) {
      const sidebarData = JSON.parse(fs.readFileSync(sidebarPath, 'utf-8'))
      return sidebarData
    }
  } catch (error) {
    console.warn('Failed to load sidebar data:', error)
  }

  return {}
}

// 在配置中使用
export const config = {
  themeConfig: {
    sidebar: {
      '/zh/': loadSidebarData()['/zh/articles/'] || [],
      '/en/': loadSidebarData()['/en/articles/'] || []
    }
  }
}
  1. 在构建流程中执行生成脚本:
json
{
  "scripts": {
    "generate:sidebar": "tsx .vitepress/theme/scripts/generate-sidebar.ts",
    "dev": "npm run generate:sidebar && vitepress dev",
    "build": "npm run generate:sidebar && vitepress build"
  }
}

最终效果

生成的侧边栏数据结构如下:

json
// sidebar-data.json
{
  "/zh/articles/": [
    {
      "text": "2025年",
      "items": [
        {
          "text": "01月 (3)",
          "collapsed": false,
          "items": [
            {
              "text": "SwiftData结合CKSyncEngine实现iCloud同步",
              "link": "/zh/Implementing-iCloud-Sync-by-Combining-SwiftData-with-CKSyncEngine/"
            }
          ]
        }
      ]
    }
  ],
  "/en/articles/": [
    {
      "text": "2025",
      "items": [
        {
          "text": "January (3)",
          "collapsed": false,
          "items": [
            {
              "text": "Implementing iCloud Sync by Combining SwiftData with CKSyncEngine",
              "link": "/en/Implementing-iCloud-Sync-by-Combining-SwiftData-with-CKSyncEngine/"
            }
          ]
        }
      ]
    }
  ]
}

总结

通过这个工具,我们实现了:

  1. 自动扫描并解析文章内容
  2. 支持多语言分组展示
  3. 按时间顺序组织文章
  4. 自动生成VitePress侧边栏配置

这大大简化了博客维护工作,使我们可以专注于内容创作。

许可协议

本文章采用 CC BY-NC-SA 4.0 许可协议进行发布。您可以自由地:

  • 共享 — 在任何媒介以任何形式复制、发行本作品
  • 演绎 — 修改、转换或以本作品为基础进行创作

惟须遵守下列条件:

  • 署名 — 您必须给出适当的署名,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但是不得以任何方式暗示许可人为您或您的使用背书。
  • 非商业性使用 — 您不得将本作品用于商业目的。
  • 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,您必须基于与原先许可协议相同的许可协议分发您贡献的作品。

上次更新时间: