Skip to content

VitePress Sidebar Auto-generation Implementation

Introduction

When building a blog with VitePress, a common requirement is to automatically generate sidebar navigation based on articles. This article will detail how to implement a multilingual sidebar auto-generation tool with year-month grouping.

Implementation Goals

  1. Automatically scan article directories and extract article information
  2. Support both Chinese and English languages
  3. Group articles by year and month
  4. Automatically generate sidebar configuration data

Core Implementation

1. Data Structure Design

First, define the required interface types:

typescript
// generate-sidebar.ts
// Article metadata interface
interface ArticleMeta {
  title: string
  date: string
  tags?: string[]
  excerpt?: string
  url: string
  lang: string
}

// Sidebar item interface
interface SidebarItem {
  text: string
  link?: string
  items?: SidebarItem[]
  collapsed?: boolean
}

// Sidebar configuration interface
interface SidebarConfig {
  [path: string]: SidebarItem[]
}

2. Article Scanning and Parsing

Use the fast-glob library to scan article directories and parse article frontmatter using gray-matter:

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

// Parse article information
for (const file of files) {
  const content = fs.readFileSync(fullPath, 'utf-8')
  const { data: frontmatter } = matter(content)
  
  // Skip non-article pages
  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

For special pages that shouldn't appear in the sidebar, you can add noneArticle: true in the frontmatter. For example:

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

3. Article Grouping

Group articles by language, year, and month:

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

// Group articles by date
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. Generate Sidebar Configuration

Convert the grouped article data into VitePress sidebar configuration format:

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)) // Sort years descending
    .map(([year, months]) => ({
      text: isZh ? `${year}年` : year,
      items: Object.entries(months)
        .sort(([a], [b]) => b.localeCompare(a)) // Sort months descending
        .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. Configuration File Generation

Write the generated configuration data to a JSON file:

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)
)

Usage

  1. Load the generated sidebar data in the VitePress configuration file:
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 {}
}

// Use in configuration
export const config = {
  themeConfig: {
    sidebar: {
      '/zh/': loadSidebarData()['/zh/articles/'] || [],
      '/en/': loadSidebarData()['/en/articles/'] || []
    }
  }
}
  1. Execute the generation script in the build process:
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"
  }
}

Final Result

The generated sidebar data structure looks like this:

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/"
            }
          ]
        }
      ]
    }
  ]
}

Summary

Through this tool, we have achieved:

  1. Automatic scanning and parsing of article content
  2. Support for multilingual grouping display
  3. Article organization by chronological order
  4. Automatic generation of VitePress sidebar configuration

This greatly simplifies blog maintenance work, allowing us to focus on content creation.

License

This article is licensed under CC BY-NC-SA 4.0 . You are free to:

  • Share — copy and redistribute the material in any medium or format
  • Adapt — remix, transform, and build upon the material

Under the following terms:

  • Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use.
  • NonCommercial — You may not use the material for commercial purposes.
  • ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license as the original.

Last updated at: