Skip to content

添加博客功能

将你的 VitePress 站点转换为功能完善的博客。

功能规划

  • 文章列表页
  • 分类和标签
  • 文章详情页
  • 归档页面
  • RSS 订阅

步骤 1:创建博客布局

博客列表布局

vue
<!-- .vitepress/theme/layouts/BlogLayout.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useData, useRouter } from 'vitepress'

interface Post {
  title: string
  path: string
  date: string
  excerpt: string
  tags: string[]
  readingTime?: number
}

const { frontmatter } = useData()
const router = useRouter()

const posts = ref<Post[]>(frontmatter.value.posts || [])
const selectedTag = ref<string | null>(null)
const searchQuery = ref('')

const allTags = computed(() => {
  const tags = new Set<string>()
  posts.value.forEach(p => p.tags?.forEach(t => tags.add(t)))
  return Array.from(tags)
})

const filteredPosts = computed(() => {
  let result = posts.value
  
  if (selectedTag.value) {
    result = result.filter(p => p.tags?.includes(selectedTag.value!))
  }
  
  if (searchQuery.value) {
    const query = searchQuery.value.toLowerCase()
    result = result.filter(p => 
      p.title.toLowerCase().includes(query) ||
      p.excerpt?.toLowerCase().includes(query)
    )
  }
  
  return result
})

const navigate = (path: string) => {
  router.go(path)
}
</script>

<template>
  <div class="blog-layout">
    <header class="blog-header">
      <h1>{{ frontmatter.title || '博客' }}</h1>
      <p v-if="frontmatter.subtitle">{{ frontmatter.subtitle }}</p>
    </header>
    
    <div class="filters">
      <input
        v-model="searchQuery"
        type="text"
        placeholder="搜索文章..."
        class="search-input"
      />
      
      <div class="tag-filter">
        <button
          :class="{ active: !selectedTag }"
          @click="selectedTag = null"
        >
          全部
        </button>
        <button
          v-for="tag in allTags"
          :key="tag"
          :class="{ active: selectedTag === tag }"
          @click="selectedTag = tag"
        >
          {{ tag }}
        </button>
      </div>
    </div>
    
    <div class="posts">
      <article
        v-for="post in filteredPosts"
        :key="post.path"
        class="post-card"
        @click="navigate(post.path)"
      >
        <div class="post-meta">
          <time>{{ post.date }}</time>
          <span v-if="post.readingTime">{{ post.readingTime }} 分钟</span>
        </div>
        
        <h2 class="post-title">{{ post.title }}</h2>
        
        <p class="post-excerpt">{{ post.excerpt }}</p>
        
        <div class="post-tags">
          <span v-for="tag in post.tags" :key="tag" class="tag">
            {{ tag }}
          </span>
        </div>
      </article>
    </div>
    
    <div v-if="filteredPosts.length === 0" class="empty">
      <p>没有找到匹配的文章</p>
    </div>
  </div>
</template>

<style scoped>
.blog-layout {
  max-width: 800px;
  margin: 0 auto;
  padding: 2rem 1.5rem;
}

.blog-header {
  text-align: center;
  margin-bottom: 2rem;
}

.blog-header h1 {
  font-size: 2rem;
  margin-bottom: 0.5rem;
}

.filters {
  margin-bottom: 2rem;
}

.search-input {
  width: 100%;
  padding: 0.75rem 1rem;
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  margin-bottom: 1rem;
  font-size: 1rem;
}

.tag-filter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.tag-filter button {
  padding: 0.25rem 0.75rem;
  background: var(--vp-c-bg-alt);
  border: 1px solid var(--vp-c-divider);
  border-radius: 16px;
  cursor: pointer;
  font-size: 0.875rem;
  transition: all 0.2s;
}

.tag-filter button.active {
  background: var(--vp-c-brand-1);
  color: white;
  border-color: var(--vp-c-brand-1);
}

.post-card {
  padding: 1.5rem;
  border: 1px solid var(--vp-c-divider);
  border-radius: 12px;
  margin-bottom: 1rem;
  cursor: pointer;
  transition: all 0.2s;
}

.post-card:hover {
  border-color: var(--vp-c-brand-1);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.post-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.85rem;
  color: var(--vp-c-text-3);
  margin-bottom: 0.5rem;
}

.post-title {
  font-size: 1.25rem;
  margin-bottom: 0.5rem;
}

.post-excerpt {
  color: var(--vp-c-text-2);
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.post-tags {
  display: flex;
  gap: 0.5rem;
}

.tag {
  font-size: 0.75rem;
  padding: 0.125rem 0.5rem;
  background: var(--vp-c-brand-soft);
  color: var(--vp-c-brand-1);
  border-radius: 4px;
}

.empty {
  text-align: center;
  padding: 4rem 0;
  color: var(--vp-c-text-2);
}
</style>

步骤 2:创建文章模板

markdown
<!-- docs/blog/my-post.md -->
---
title: 我的第一篇博客
date: 2026-04-04
tags:
  - VitePress
  - 教程
readingTime: 5
excerpt: 这是一篇关于如何在 VitePress 中添加博客功能的教程。
---

# 我的第一篇博客

文章内容...

## 简介

...

## 正文

...

步骤 3:生成文章数据

创建脚本在构建时生成文章列表:

ts
// scripts/generate-blog-data.ts
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

interface Post {
  title: string
  path: string
  date: string
  tags: string[]
  excerpt: string
  readingTime: number
}

const posts: Post[] = []

function walk(dir: string) {
  const entries = fs.readdirSync(dir, { withFileTypes: true })
  
  for (const entry of entries) {
    const fullPath = path.join(dir, entry.name)
    
    if (entry.isDirectory()) {
      walk(fullPath)
    } else if (entry.name.endsWith('.md') && entry.name !== 'index.md') {
      const content = fs.readFileSync(fullPath, 'utf-8')
      const { data, content: body } = matter(content)
      
      if (!data.date) continue
      
      posts.push({
        title: data.title || entry.name,
        path: '/' + fullPath.replace('docs/', '').replace('.md', ''),
        date: data.date,
        tags: data.tags || [],
        excerpt: data.excerpt || body.slice(0, 200),
        readingTime: data.readingTime || Math.ceil(body.length / 300)
      })
    }
  }
}

walk('docs/blog')

// 按日期排序
posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())

// 保存数据
fs.writeFileSync('docs/public/posts.json', JSON.stringify(posts, null, 2))

console.log(`✅ 生成 ${posts.length} 篇文章数据`)

步骤 4:创建博客首页

markdown
<!-- docs/blog/index.md -->
---
layout: ../../.vitepress/theme/layouts/BlogLayout.vue
title: 博客
subtitle: 分享技术心得与学习笔记
---

<script setup>
import { ref, onMounted } from 'vue'

const posts = ref([])

onMounted(async () => {
  const res = await fetch('/posts.json')
  posts.value = await res.json()
})
</script>

步骤 5:添加 RSS 支持

bash
npm install feed -D
ts
// scripts/generate-rss.ts
import fs from 'fs'
import { Feed } from 'feed'

const feed = new Feed({
  title: '我的博客',
  description: '技术博客',
  id: 'https://your-domain.com',
  link: 'https://your-domain.com',
  language: 'zh-CN',
  copyright: 'Copyright 2026'
})

// 添加文章...
const posts = JSON.parse(fs.readFileSync('docs/public/posts.json', 'utf-8'))

posts.forEach(post => {
  feed.addItem({
    title: post.title,
    id: `https://your-domain.com${post.path}`,
    link: `https://your-domain.com${post.path}`,
    date: new Date(post.date),
    description: post.excerpt
  })
})

fs.writeFileSync('docs/public/rss.xml', feed.rss2())

学习检查清单

  • [ ] 创建了博客列表布局
  • [ ] 实现了标签筛选
  • [ ] 添加了搜索功能
  • [ ] 生成了文章数据
  • [ ] 添加了 RSS 订阅

下一步

继续学习 集成评论系统,为博客添加互动功能。

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献