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
- Automatically scan article directories and extract article information
- Support both Chinese and English languages
- Group articles by year and month
- Automatically generate sidebar configuration data
Core Implementation
1. Data Structure Design
First, define the required interface types:
// 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
:
// 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:
---
layout: Articles
title: Articles
noneArticle: true
---
3. Article Grouping
Group articles by language, year, and month:
// 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:
// 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:
// 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
- Load the generated sidebar data in the VitePress configuration file:
// 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/'] || []
}
}
}
- Execute the generation script in the build process:
{
"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:
// 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:
- Automatic scanning and parsing of article content
- Support for multilingual grouping display
- Article organization by chronological order
- Automatic generation of VitePress sidebar configuration
This greatly simplifies blog maintenance work, allowing us to focus on content creation.