博客搭建实战
本教程将带你从零搭建一个完整的博客站点。
项目初始化
创建项目
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
- 自定义打印样式
- 添加导出按钮