Skip to content

博客搭建实战

本教程将带你从零搭建一个完整的博客站点。

项目初始化

创建项目

bash
# 创建目录
mkdir my-blog
cd my-blog

# 初始化项目
npm init -y

# 安装 VitePress
npm add -D vitepress

目录结构

my-blog/
├── docs/
│   ├── .vitepress/
│   │   └── config.mts
│   ├── posts/
│   │   ├── 2024-01-15-first-post.md
│   │   └── 2024-02-20-second-post.md
│   ├── about.md
│   └── index.md
└── package.json

配置博客

基础配置

ts
// docs/.vitepress/config.mts
import { defineConfig } from 'vitepress'

export default defineConfig({
  title: '我的博客',
  description: '记录生活与技术',
  lang: 'zh-CN',
  
  themeConfig: {
    logo: '/logo.svg',
    
    nav: [
      { text: '首页', link: '/' },
      { text: '文章', link: '/posts/' },
      { text: '关于', link: '/about' }
    ],
    
    sidebar: {
      '/posts/': [
        {
          text: '文章列表',
          items: []  // 动态生成
        }
      ]
    },
    
    footer: {
      message: '基于 MIT 许可发布',
      copyright: 'Copyright © 2026'
    },
    
    search: {
      provider: 'local'
    }
  }
})

文章数据加载

创建数据加载器

ts
// docs/.vitepress/data/posts.data.ts
import { createContentLoader } from 'vitepress'

export interface Post {
  title: string
  url: string
  date: string
  excerpt: string
  tags: string[]
  author: string
  cover?: string
}

export default createContentLoader('posts/*.md', {
  excerpt: true,
  transform(raw): Post[] {
    return raw
      .map(({ url, frontmatter, excerpt }) => ({
        title: frontmatter.title,
        url,
        excerpt,
        date: frontmatter.date,
        tags: frontmatter.tags || [],
        author: frontmatter.author || '作者',
        cover: frontmatter.cover
      }))
      .sort((a, b) => +new Date(b.date) - +new Date(a.date))
  }
})

创建页面

首页

markdown
---
layout: home

hero:
  name: 我的博客
  text: 记录生活与技术
  tagline: 热爱编程,热爱生活
  actions:
    - theme: brand
      text: 开始阅读
      link: /posts/
    - theme: alt
      text: 关于我
      link: /about

features:
  - title: 技术分享
    details: 分享前端、后端、运维等技术文章
  - title: 生活随笔
    details: 记录生活中的点滴感悟
  - title: 学习笔记
    details: 整理学习过程中的知识笔记
---

文章列表页

创建 docs/posts/index.md

markdown
---
title: 文章列表
---

<script setup>
import { data as posts } from '.vitepress/data/posts.data'
import { ref, computed } from 'vue'

const selectedTag = ref(null)
const allTags = computed(() => {
  const tags = new Set()
  posts.forEach(post => post.tags.forEach(tag => tags.add(tag)))
  return Array.from(tags)
})

const filteredPosts = computed(() => {
  if (!selectedTag.value) return posts
  return posts.filter(post => post.tags.includes(selectedTag.value))
})

function formatDate(date) {
  return new Date(date).toLocaleDateString('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  })
}
</script>

# 文章列表

<div class="tags">
  <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 class="posts">
  <article v-for="post in filteredPosts" :key="post.url" class="post-card">
    <img v-if="post.cover" :src="post.cover" :alt="post.title" class="cover" />
    <div class="content">
      <h2>
        <a :href="post.url">{{ post.title }}</a>
      </h2>
      <p class="meta">
        <span>{{ formatDate(post.date) }}</span>
        <span>·</span>
        <span>{{ post.author }}</span>
      </p>
      <p class="excerpt">{{ post.excerpt }}</p>
      <div class="tags">
        <span v-for="tag in post.tags" :key="tag" class="tag">{{ tag }}</span>
      </div>
    </div>
  </article>
</div>

<style scoped>
.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 24px;
}

.tags button {
  padding: 6px 12px;
  border: 1px solid var(--vp-c-divider);
  border-radius: 16px;
  background: transparent;
  cursor: pointer;
}

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

.post-card {
  border: 1px solid var(--vp-c-divider);
  border-radius: 8px;
  margin-bottom: 24px;
  overflow: hidden;
}

.post-card .cover {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.post-card .content {
  padding: 20px;
}

.post-card h2 {
  margin: 0 0 8px;
}

.post-card h2 a {
  color: var(--vp-c-text-1);
  text-decoration: none;
}

.post-card h2 a:hover {
  color: var(--vp-c-brand-1);
}

.meta {
  color: var(--vp-c-text-2);
  font-size: 14px;
  margin-bottom: 12px;
}

.excerpt {
  color: var(--vp-c-text-2);
  margin-bottom: 12px;
}

.tag {
  display: inline-block;
  padding: 2px 8px;
  background: var(--vp-c-default-soft);
  border-radius: 4px;
  font-size: 12px;
  margin-right: 4px;
}
</style>

文章模板

创建 docs/posts/2024-01-15-first-post.md

markdown
---
title: 第一篇文章
date: 2024-01-15
author: 作者名
tags:
  - 前端
  - JavaScript
cover: /images/cover.jpg
---

# 第一篇文章

这是文章摘要,会在列表页显示。

<!-- more -->

这里是正文内容...

## 章节标题

正文内容...

## 总结

文章总结...

关于页面

markdown
---
title: 关于我
---

# 关于我

你好,我是作者名 👋

## 关于本站

这是一个使用 VitePress 搭建的个人博客,主要分享:

- 前端技术
- 后端开发
- 生活随笔

## 联系方式

- GitHub: [@username](https://github.com/username)
- Email: email@example.com

## 统计

<script setup>
import { data as posts } from '.vitepress/data/posts.data'

const totalPosts = posts.length
const totalTags = new Set(posts.flatMap(p => p.tags)).size
</script>

<div class="stats">
  <div class="stat">
    <span class="number">{{ totalPosts }}</span>
    <span class="label">篇文章</span>
  </div>
  <div class="stat">
    <span class="number">{{ totalTags }}</span>
    <span class="label">个标签</span>
  </div>
</div>

<style scoped>
.stats {
  display: flex;
  gap: 32px;
  margin-top: 24px;
}

.stat {
  text-align: center;
}

.number {
  display: block;
  font-size: 32px;
  font-weight: bold;
  color: var(--vp-c-brand-1);
}

.label {
  color: var(--vp-c-text-2);
}
</style>

添加评论功能

使用 Giscus

vue
<!-- docs/.vitepress/theme/components/Comment.vue -->
<script setup>
import Giscus from '@giscus/vue'
import { useData } from 'vitepress'

const { isDark } = useData()
</script>

<template>
  <div class="comments">
    <Giscus
      repo="user/repo"
      repo-id="YOUR_REPO_ID"
      category="Announcements"
      category-id="YOUR_CATEGORY_ID"
      mapping="pathname"
      :theme="isDark ? 'dark' : 'light"
    />
  </div>
</template>

集成到文章页

vue
<!-- docs/.vitepress/theme/Layout.vue -->
<script setup>
import DefaultTheme from 'vitepress/theme'
import Comment from './components/Comment.vue'

const { Layout } = DefaultTheme
</script>

<template>
  <Layout>
    <template #doc-after>
      <Comment />
    </template>
  </Layout>
</template>

构建与部署

添加脚本

json
{
  "scripts": {
    "dev": "vitepress dev docs",
    "build": "vitepress build docs",
    "preview": "vitepress preview docs"
  }
}

部署到 GitHub Pages

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: docs/.vitepress/dist
      - uses: actions/deploy-pages@v4

下一步

学习 组件库文档 了解如何为组件库编写文档。

🎯 进阶挑战

完成基础教程后,尝试以下挑战来提升你的技能:

挑战 1:阅读时长统计 ⭐⭐

目标:为每篇文章添加预计阅读时长。

提示

  • 在数据加载器中计算字数
  • 按平均阅读速度(200字/分钟)计算时长
  • 在文章卡片中显示阅读时长

参考代码

ts
function calculateReadTime(content: string): number {
  const wordsPerMinute = 200
  const wordCount = content.length
  return Math.ceil(wordCount / wordsPerMinute)
}

挑战 2:文章目录自动生成 ⭐⭐⭐

目标:在文章列表页自动生成分类目录。

提示

  • 在 frontmatter 中添加 category 字段
  • 创建按分类分组的数据加载器
  • 实现分类切换功能

挑战 3:文章搜索高亮 ⭐⭐⭐

目标:实现搜索结果关键词高亮显示。

提示

  • 使用本地搜索 API
  • 自定义搜索结果组件
  • 正则匹配高亮关键词

挑战 4:阅读进度条 ⭐⭐

目标:在文章页顶部添加阅读进度条。

提示

  • 监听 scroll 事件
  • 计算阅读百分比
  • 使用 CSS 渐变效果

参考代码

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

const progress = ref(0)

const updateProgress = () => {
  const scrollTop = window.scrollY
  const docHeight = document.documentElement.scrollHeight - window.innerHeight
  progress.value = (scrollTop / docHeight) * 100
}

onMounted(() => window.addEventListener('scroll', updateProgress))
onUnmounted(() => window.removeEventListener('scroll', updateProgress))
</script>

<template>
  <div class="progress-bar" :style="{ width: progress + '%' }" />
</template>

挑战 5:文章点赞功能 ⭐⭐⭐⭐

目标:为文章添加点赞功能,数据存储到 localStorage。

提示

  • 创建点赞按钮组件
  • 使用 localStorage 存储点赞状态
  • 在数据加载器中统计点赞数

挑战 6:Markdown 导出 PDF ⭐⭐⭐⭐

目标:为文章添加导出 PDF 功能。

提示

  • 使用浏览器打印 API
  • 自定义打印样式
  • 添加导出按钮

贡献者

加载中...

想要成为贡献者?

在 CNB 上参与贡献