English
How to implement blog archive functionality by extending the default VitePress theme

1. Why is an archiving feature needed?
As the number of blog posts continues to grow (there are currently 30+), it is difficult for readers to quickly find past articles. The sidebar can only display a limited list of articles, while an archive page can:
Grouped by year, clear at a glance.
Arrange in reverse order, with the newest articles first.
Display title + date + description for easy retrieval
This article will share how to implement a complete archive feature by extending the VitePress default theme.
Final effect: Visit /archive Page that displays all articles grouped by year.

2. Overall Solution Design
┌─────────────────┐
│ Article .md file │
│ + createdAt │
└────────┬────────┘
│ Build-time scan
▼
┌─────────────────┐
│ generateArchive │
│ Data.js │
└────────┬────────┘
│ Generate JSON
▼
┌─────────────────┐
│ archiveData.json│
└────────┬────────┘
│ Import
▼
┌─────────────────┐
│ Archive.vue │
│ (Extended theme component) │
└────────┬────────┘
│ Rendering
▼
┌─────────────────┐
│ Archive Page │
│ /archive.html │
└─────────────────┘Core idea:
The frontmatter of each article must include
createdAtfieldScan all articles at build time, extract metadata, and generate a JSON file.
The archive page component directly imports JSON data and renders it.
3. Step 1: Normalize article metadata
3.1 Unified Addition createdAt field
Add to the frontmatter of each article createdAt(ISO 8601 format):
yaml
---
title: 'Article Title'
description: 'Article description'
createdAt: 2026-06-07T16:30:00+08:00
---Why use ISO 8601?
International standard, natively supported by all programming languages
Include time zone information to avoid ambiguity.
String sorting is time sorting.
3.2 Get creation time from Git history (optional)
If you need to batch retrieve the creation times of historical articles:
bash
# View the first commit time of a file
git log --follow --format=%ai --reverse <file path> | head -1Output example:2026-06-07 16:30:00 +0800
Manually convert to ISO 8601:2026-06-07T16:30:00+08:00
4. Step 2: Build a script to generate archive data
4.1 Install dependencies
bash
npm install gray-matter --save-dev4.2 Create a script to automatically generate archived data scripts
This script will scan all articles at build time, extract metadata, and generate a JSON file.
js
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import matter from 'gray-matter'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const docsRoot = path.resolve(__dirname, '..')
const postsRoot = path.resolve(docsRoot, 'posts')
// Recursively get all .md files
function getAllMdFiles(dir, fileList = []) {
const files = fs.readdirSync(dir)
files.forEach(file => {
const filePath = path.join(dir, file)
const stat = fs.statSync(filePath)
if (stat.isDirectory()) {
getAllMdFiles(filePath, fileList)
} else if (file.endsWith('.md')) {
fileList.push(filePath)
}
})
return fileList
}
// Parse frontmatter
function parseFrontmatter(filePath) {
const content = fs.readFileSync(filePath, 'utf-8')
const { data } = matter(content)
return data
}
function generateArchiveData() {
const mdFiles = getAllMdFiles(postsRoot)
const articles = []
mdFiles.forEach(filePath => {
const relativePath = path.relative(postsRoot, filePath)
const fm = parseFrontmatter(filePath)
// Must have createdAt to be included in archiving
if (!fm.createdAt) {
console.warn(`⚠️ Missing createdAt: ${relativePath}`)
return
}
// Generate URL
const url = `/posts/${relativePath.replace(/\.md$/, '.html')}`
articles.push({
title: fm.title || path.basename(filePath, '.md'),
description: fm.description || '',
createdAt: fm.createdAt,
url: url
})
})
// Sort by createdAt in descending order
articles.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
// Write to the .vitepress directory
const outputPath = path.resolve(docsRoot, '.vitepress/archiveData.json')
fs.writeFileSync(outputPath, JSON.stringify(articles, null, 2))
console.log(`✅ Generated archive data: ${outputPath} (${articles.length} articles)`)
}
generateArchiveData()4.3 Integration into the Build Process
Modify package.json:
json
{
"name": "your-blog",
"type": "module",
"scripts": {
"docs:build": "node docs/scripts/generate-archive-data.js && vitepress build docs",
"docs:dev": "vitepress dev docs"
}
}5. Step 3: Create the Archive.vue component
5.1 Component Location
Create docs/.vitepress/theme/components/Archive.vue
5.2 Complete component code
vue
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useData } from 'vitepress'
import { Content } from 'vitepress'
import archiveData from '~/.vitepress/archiveData.json'
const route = useRoute()
const { theme } = useData()
const pageName = computed(() =>
route.path.replace(/[./]+/g, '_').replace(/_html$/, '')
)
const articles = archiveData
// Group by year
const postsByYear = computed(() => {
const groups: Record<string, any[]> = {}
for (const article of articles) {
if (!article.createdAt) continue
const year = new Date(article.createdAt).getFullYear().toString()
if (!groups[year]) groups[year] = []
groups[year].push(article)
}
return Object.entries(groups).sort((a, b) => Number(b[0]) - Number(a[0]))
})
// Format date
function formatDate(isoString: string): string {
const date = new Date(isoString)
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
}
</script>
<template>
<div class="VPDoc">
<div class="container">
<div class="content">
<div class="content-container">
<main class="main">
<Content class="vp-doc" />
</main>
<!-- Archive list -->
<div v-if="postsByYear.length === 0" class="archive-empty">
No articles yet
</div>
<div v-else class="archive-container">
<div v-for="[year, posts] in postsByYear" :key="year" class="archive-year">
<h2>{{ year }}</h2>
<ul>
<li v-for="post in posts" :key="post.url">
<a :href="post.url">
<span class="post-date">{{ formatDate(post.createdAt) }}</span>
<span class="post-title">{{ post.title }}</span>
</a>
<p v-if="post.description" class="post-description">
{{ post.description }}
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.archive-container {
margin-top: 32px;
}
.archive-year {
margin-bottom: 48px;
}
.archive-year h2 {
font-size: 28px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid var(--vp-c-divider);
}
.archive-year ul {
list-style: none;
padding: 0;
margin: 0;
}
.archive-year li {
margin-bottom: 24px;
}
.archive-year li a {
display: flex;
align-items: baseline;
gap: 16px;
text-decoration: none;
font-size: 18px;
font-weight: 500;
color: var(--vp-c-brand-1);
transition: color 0.2s;
}
.archive-year li a:hover {
color: var(--vp-c-brand-2);
}
.post-date {
flex-shrink: 0;
font-size: 14px;
font-weight: 400;
color: var(--vp-c-text-2);
font-family: monospace;
}
.post-title {
flex-grow: 1;
}
.post-description {
margin: 8px 0 0 0;
font-size: 14px;
color: var(--vp-c-text-2);
line-height: 1.5;
}
.archive-empty {
text-align: center;
padding: 48px 0;
color: var(--vp-c-text-2);
}
</style>5.3 Register Component
at docs/.vitepress/theme/index.ts Middle:
typescript
import DefaultTheme from 'vitepress/theme'
import Archive from './components/Archive.vue'
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.component('Archive', Archive)
}
}5.4 Create an Archive Page
Create docs/archive.md:
markdown
---
layout: archive
sidebar: false
aside: false
title: 'Archive'
description: 'Archive all technical articles on this site by year'
---
# Archive
Archive all technical articles on this site by year6. Pitfalls encountered and solutions
6.1 Path Reference Issues
Error:Could not resolve "../.vitepress/archiveData.json"
Reason: Relative path calculation error
Solution: use ~/.vitepress/archiveData.json(VitePress alias)
6.2 MutationObserver is not defined
Reason: The component attempts to access browser APIs during server-side rendering
Solution: Move browser API calls to onMounted Medium (can be ignored if it does not affect functionality)
7. Final Effect
After the build is complete, access /archive Page:
2026
├── 2026-06-07 How to implement archive functionality by extending the VitePress theme
├── 2026-06-06 Blog SEO technical refactoring: from 6 clicks per month to stable Google indexing
├── 2026-06-03 Syslog Basics: Protocol Anatomy and rsyslog Configuration
└── ...
2025
├── 2025-12-01 Complete production environment deployment plan for Golang project
├── 2025-08-15 Fix Docker Desktop Kubernetes EOF error
└── ...8. Summary and Extension
8.1 Reusable Patterns
The core idea of this approach can be extended to other scenarios:
| Function | Similar solutions |
|---|---|
| tag cloud | Scan all articles and extract the tags field. |
| RSS generation | Extract article metadata and generate XML |
| Related recommendations | Recommend similar articles based on tags/keywords |
8.2 Future Directions for Expansion
Tag Archive: Display articles grouped by tags
Search function: Implement local search based on metadata
Article statistics: Display the total number of articles, total word count, etc.
8.3 Related Resources
gray-matter Frontmatter parsing library
If you encounter any problems during implementation, you are welcome to contact me through the following methods.