Skip to content

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

Schematic diagram of how to implement the blog archive feature 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.

Archive Preview

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 createdAt field

  • Scan 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 -1

Output 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-dev

4.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 year

6. 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:

FunctionSimilar solutions
tag cloudScan all articles and extract the tags field.
RSS generationExtract article metadata and generate XML
Related recommendationsRecommend 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.

If you encounter any problems during implementation, you are welcome to contact me through the following methods.

Last updated on11/07/2026, 02:16 PM
@fishfinal | @fishfinals | [email protected]