个人博客
使用 VitePress 构建现代化的个人博客网站,展示个人风采,分享知识与见解。
项目概述
个人博客是展示个人品牌、分享知识和建立影响力的重要平台。VitePress 凭借其简洁的设计理念、优秀的性能表现和灵活的定制能力,是构建个人博客的理想选择。
核心特性
- ✍️ Markdown 写作 - 专注内容创作的写作体验
- 🎨 个性化设计 - 完全可定制的主题和样式
- 📱 响应式布局 - 完美适配各种设备
- 🔍 SEO 优化 - 搜索引擎友好的结构
- 📊 数据分析 - 访问统计和用户行为分析
- 💬 评论系统 - 与读者互动交流
- 🏷️ 标签分类 - 灵活的内容组织方式
- 📧 订阅功能 - RSS 和邮件订阅
- 🚀 快速加载 - 优化的性能和用户体验
- 🌙 深色模式 - 护眼的阅读体验
技术架构
核心技术栈
json
{
"generator": "VitePress",
"framework": "Vue 3",
"styling": "CSS3 + PostCSS",
"comments": "Giscus / Disqus",
"analytics": "Google Analytics",
"search": "Algolia DocSearch",
"deployment": [
"Vercel",
"Netlify",
"GitHub Pages"
]
}
项目结构
personal-blog/
├── docs/
│ ├── .vitepress/
│ │ ├── config.ts
│ │ ├── theme/
│ │ │ ├── index.ts
│ │ │ ├── Layout.vue
│ │ │ └── components/
│ │ │ ├── BlogPost.vue
│ │ │ ├── PostList.vue
│ │ │ ├── TagCloud.vue
│ │ │ └── CommentSection.vue
│ │ └── public/
│ ├── posts/
│ │ ├── 2024/
│ │ ├── 2023/
│ │ └── index.md
│ ├── about/
│ ├── projects/
│ ├── tags/
│ └── index.md
├── scripts/
├── package.json
└── README.md
实现步骤
1. 项目初始化
bash
# 创建项目
npm create vitepress@latest personal-blog
cd personal-blog
# 安装依赖
npm install
npm install -D @giscus/vue gray-matter
# 启动开发服务器
npm run docs:dev
2. 基础配置
typescript
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
import { generateSidebar } from './utils/generateSidebar'
export default defineConfig({
title: '我的个人博客',
description: '分享技术、记录生活、展示作品',
lang: 'zh-CN',
base: '/',
cleanUrls: true,
head: [
['link', { rel: 'icon', href: '/favicon.ico' }],
['meta', { name: 'theme-color', content: '#3b82f6' }],
['meta', { name: 'author', content: '你的名字' }],
['meta', { name: 'keywords', content: '个人博客,技术分享,前端开发,Vue.js' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:locale', content: 'zh-CN' }],
['meta', { property: 'og:site_name', content: '我的个人博客' }],
['meta', { property: 'og:image', content: '/og-image.jpg' }],
// RSS 订阅
['link', { rel: 'alternate', type: 'application/rss+xml', href: '/feed.xml', title: 'RSS Feed' }],
// 分析工具
['script', { async: '', src: 'https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID' }],
['script', {}, `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_MEASUREMENT_ID');
`]
],
themeConfig: {
logo: '/avatar.jpg',
siteTitle: '我的博客',
nav: [
{ text: '首页', link: '/' },
{ text: '文章', link: '/posts/' },
{ text: '项目', link: '/projects/' },
{ text: '关于', link: '/about/' },
{ text: '标签', link: '/tags/' },
{
text: '更多',
items: [
{ text: '归档', link: '/archive/' },
{ text: '友链', link: '/friends/' },
{ text: 'RSS', link: '/feed.xml' }
]
}
],
sidebar: {
'/posts/': generateSidebar('docs/posts')
},
socialLinks: [
{ icon: 'github', link: 'https://github.com/your-username' },
{ icon: 'twitter', link: 'https://twitter.com/your-handle' },
{ icon: 'linkedin', link: 'https://linkedin.com/in/your-profile' }
],
footer: {
message: '用心记录,用爱分享',
copyright: 'Copyright © 2024 你的名字. All rights reserved.'
},
editLink: {
pattern: 'https://github.com/your-username/personal-blog/edit/main/docs/:path',
text: '在 GitHub 上编辑此页'
},
lastUpdated: {
text: '最后更新于',
formatOptions: {
dateStyle: 'short',
timeStyle: 'medium'
}
},
search: {
provider: 'local',
options: {
locales: {
zh: {
translations: {
button: {
buttonText: '搜索文章',
buttonAriaLabel: '搜索文章'
},
modal: {
noResultsText: '无法找到相关文章',
resetButtonTitle: '清除查询条件',
footer: {
selectText: '选择',
navigateText: '切换'
}
}
}
}
}
}
}
},
markdown: {
theme: {
light: 'github-light',
dark: 'github-dark'
},
lineNumbers: true,
config: (md) => {
// 自定义容器
md.use(require('markdown-it-container'), 'tip')
md.use(require('markdown-it-container'), 'warning')
md.use(require('markdown-it-container'), 'danger')
md.use(require('markdown-it-container'), 'info')
}
},
// 生成站点地图
sitemap: {
hostname: 'https://your-blog.com'
},
// 构建钩子
buildEnd: async (siteConfig) => {
// 生成 RSS feed
await generateRSSFeed(siteConfig)
}
})
async function generateRSSFeed(siteConfig) {
// RSS 生成逻辑
console.log('Generating RSS feed...')
}
3. 首页设计
vue
<!-- docs/index.md -->
---
layout: home
title: 我的个人博客
titleTemplate: 分享技术,记录生活
hero:
name: 你的名字
text: 前端开发者 & 技术博主
tagline: 用代码改变世界,用文字记录成长
image:
src: /hero-avatar.jpg
alt: 个人头像
actions:
- theme: brand
text: 阅读文章
link: /posts/
- theme: alt
text: 了解我
link: /about/
features:
- icon: ✍️
title: 技术分享
details: 分享前端开发经验、最佳实践和新技术探索
link: /posts/tech/
- icon: 💡
title: 项目展示
details: 展示个人项目和开源贡献,记录技术成长历程
link: /projects/
- icon: 📚
title: 学习笔记
details: 记录学习过程中的心得体会和知识总结
link: /posts/notes/
- icon: 🌱
title: 生活感悟
details: 分享生活中的思考、感悟和个人成长经历
link: /posts/life/
---
<script setup>
import { ref, onMounted } from 'vue'
const recentPosts = ref([])
const stats = ref({
totalPosts: 0,
totalViews: 0,
totalTags: 0
})
onMounted(async () => {
// 获取最新文章
try {
const response = await fetch('/api/recent-posts')
recentPosts.value = await response.json()
} catch (error) {
console.error('Failed to load recent posts:', error)
}
// 获取博客统计
try {
const response = await fetch('/api/blog-stats')
stats.value = await response.json()
} catch (error) {
console.error('Failed to load stats:', error)
}
})
</script>
<div class="blog-stats">
<div class="stats-container">
<div class="stat-item">
<div class="stat-number">{{ stats.totalPosts }}</div>
<div class="stat-label">篇文章</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.totalViews }}</div>
<div class="stat-label">次阅读</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ stats.totalTags }}</div>
<div class="stat-label">个标签</div>
</div>
</div>
</div>
<div class="recent-posts">
<h2>最新文章</h2>
<div class="posts-grid">
<article
v-for="post in recentPosts"
:key="post.id"
class="post-card"
>
<div class="post-image" v-if="post.cover">
<img :src="post.cover" :alt="post.title">
</div>
<div class="post-content">
<div class="post-meta">
<time :datetime="post.date">{{ formatDate(post.date) }}</time>
<span class="post-category">{{ post.category }}</span>
</div>
<h3 class="post-title">
<a :href="post.url">{{ post.title }}</a>
</h3>
<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>
</div>
</article>
</div>
<div class="view-all">
<a href="/posts/" class="view-all-btn">查看全部文章 →</a>
</div>
</div>
<div class="about-preview">
<div class="about-content">
<div class="about-text">
<h2>关于我</h2>
<p>
我是一名热爱技术的前端开发者,专注于 Vue.js、React 和现代前端技术栈。
喜欢通过博客分享技术心得,记录学习成长的点点滴滴。
</p>
<p>
除了编程,我还喜欢阅读、摄影和旅行。相信技术能够改变世界,
也相信每一次分享都能帮助到更多的人。
</p>
<a href="/about/" class="about-link">了解更多 →</a>
</div>
<div class="about-image">
<img src="/about-me.jpg" alt="关于我">
</div>
</div>
</div>
<style>
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--vp-home-hero-image-background-image: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--vp-home-hero-image-filter: blur(44px);
}
.blog-stats {
margin: 48px 0;
padding: 32px;
background: var(--vp-c-bg-soft);
border-radius: 12px;
border: 1px solid var(--vp-c-border);
}
.stats-container {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 32px;
max-width: 600px;
margin: 0 auto;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: 700;
color: var(--vp-c-brand);
margin-bottom: 8px;
}
.stat-label {
font-size: 14px;
color: var(--vp-c-text-2);
}
.recent-posts {
margin: 64px 0;
}
.recent-posts h2 {
text-align: center;
margin-bottom: 32px;
font-size: 32px;
color: var(--vp-c-text-1);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.post-card {
background: var(--vp-c-bg);
border: 1px solid var(--vp-c-border);
border-radius: 12px;
overflow: hidden;
transition: all 0.2s ease;
cursor: pointer;
}
.post-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(59, 130, 246, 0.15);
border-color: var(--vp-c-brand);
}
.post-image {
aspect-ratio: 16/9;
overflow: hidden;
}
.post-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.2s ease;
}
.post-card:hover .post-image img {
transform: scale(1.05);
}
.post-content {
padding: 20px;
}
.post-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 12px;
}
.post-meta time {
color: var(--vp-c-text-3);
}
.post-category {
background: var(--vp-c-brand-soft);
color: var(--vp-c-brand);
padding: 2px 8px;
border-radius: 12px;
font-weight: 500;
}
.post-title {
margin: 0 0 12px 0;
font-size: 18px;
line-height: 1.4;
}
.post-title a {
color: var(--vp-c-text-1);
text-decoration: none;
transition: color 0.2s;
}
.post-title a:hover {
color: var(--vp-c-brand);
}
.post-excerpt {
color: var(--vp-c-text-2);
font-size: 14px;
line-height: 1.6;
margin-bottom: 16px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
background: var(--vp-c-bg-mute);
color: var(--vp-c-text-2);
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
border: 1px solid var(--vp-c-border);
}
.view-all {
text-align: center;
}
.view-all-btn {
display: inline-block;
padding: 12px 24px;
background: var(--vp-c-brand);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
transition: all 0.2s;
}
.view-all-btn:hover {
background: var(--vp-c-brand-dark);
transform: translateY(-2px);
}
.about-preview {
margin: 64px 0;
padding: 48px 0;
background: var(--vp-c-bg-soft);
border-radius: 16px;
}
.about-content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 48px;
align-items: center;
max-width: 1000px;
margin: 0 auto;
padding: 0 32px;
}
.about-text h2 {
margin-bottom: 20px;
font-size: 28px;
color: var(--vp-c-text-1);
}
.about-text p {
color: var(--vp-c-text-2);
line-height: 1.7;
margin-bottom: 16px;
}
.about-link {
color: var(--vp-c-brand);
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.about-link:hover {
color: var(--vp-c-brand-dark);
}
.about-image {
text-align: center;
}
.about-image img {
width: 200px;
height: 200px;
border-radius: 50%;
object-fit: cover;
border: 4px solid var(--vp-c-brand);
}
.VPFeature {
transition: all 0.2s ease;
}
.VPFeature:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
}
@media (max-width: 768px) {
.stats-container {
grid-template-columns: 1fr;
gap: 20px;
}
.posts-grid {
grid-template-columns: 1fr;
}
.about-content {
grid-template-columns: 1fr;
text-align: center;
gap: 32px;
}
.about-image img {
width: 150px;
height: 150px;
}
}
</style>
4. 博客文章组件
vue
<!-- .vitepress/theme/components/BlogPost.vue -->
<template>
<article class="blog-post">
<header class="post-header">
<div class="post-cover" v-if="frontmatter.cover">
<img :src="frontmatter.cover" :alt="frontmatter.title">
</div>
<div class="post-meta">
<h1 class="post-title">{{ frontmatter.title }}</h1>
<p v-if="frontmatter.description" class="post-description">
{{ frontmatter.description }}
</p>
<div class="post-info">
<div class="author-info">
<img :src="frontmatter.author?.avatar || '/default-avatar.jpg'"
:alt="frontmatter.author?.name || '作者'"
class="author-avatar">
<div class="author-details">
<span class="author-name">{{ frontmatter.author?.name || '博主' }}</span>
<time :datetime="frontmatter.date" class="post-date">
{{ formatDate(frontmatter.date) }}
</time>
</div>
</div>
<div class="post-stats">
<span class="reading-time">
<ClockIcon />
{{ readingTime }} 分钟阅读
</span>
<span class="view-count">
<EyeIcon />
{{ viewCount }} 次阅读
</span>
</div>
</div>
<div class="post-tags" v-if="frontmatter.tags?.length">
<span
v-for="tag in frontmatter.tags"
:key="tag"
class="tag"
@click="navigateToTag(tag)"
>
{{ tag }}
</span>
</div>
</div>
</header>
<div class="post-content">
<div class="table-of-contents" v-if="showToc">
<h3>目录</h3>
<nav class="toc-nav">
<!-- 目录内容会由 VitePress 自动生成 -->
</nav>
</div>
<div class="content-wrapper">
<Content />
</div>
</div>
<footer class="post-footer">
<div class="post-actions">
<button @click="toggleLike" :class="['like-btn', { liked: isLiked }]">
<HeartIcon />
{{ likeCount }}
</button>
<button @click="sharePost" class="share-btn">
<ShareIcon />
分享
</button>
<button @click="toggleBookmark" :class="['bookmark-btn', { bookmarked: isBookmarked }]">
<BookmarkIcon />
{{ isBookmarked ? '已收藏' : '收藏' }}
</button>
</div>
<div class="post-navigation" v-if="prevPost || nextPost">
<div class="nav-item prev" v-if="prevPost">
<span class="nav-label">上一篇</span>
<a :href="prevPost.url" class="nav-title">{{ prevPost.title }}</a>
</div>
<div class="nav-item next" v-if="nextPost">
<span class="nav-label">下一篇</span>
<a :href="nextPost.url" class="nav-title">{{ nextPost.title }}</a>
</div>
</div>
<div class="related-posts" v-if="relatedPosts.length">
<h3>相关文章</h3>
<div class="related-list">
<article
v-for="post in relatedPosts"
:key="post.url"
class="related-item"
>
<div class="related-image" v-if="post.cover">
<img :src="post.cover" :alt="post.title">
</div>
<div class="related-content">
<h4><a :href="post.url">{{ post.title }}</a></h4>
<p>{{ post.excerpt }}</p>
<time :datetime="post.date">{{ formatDate(post.date) }}</time>
</div>
</article>
</div>
</div>
</footer>
</article>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useData, useRouter } from 'vitepress'
import ClockIcon from './ClockIcon.vue'
import EyeIcon from './EyeIcon.vue'
import HeartIcon from './HeartIcon.vue'
import ShareIcon from './ShareIcon.vue'
import BookmarkIcon from './BookmarkIcon.vue'
const { page, frontmatter } = useData()
const router = useRouter()
const viewCount = ref(0)
const likeCount = ref(0)
const isLiked = ref(false)
const isBookmarked = ref(false)
const relatedPosts = ref([])
const prevPost = ref(null)
const nextPost = ref(null)
const showToc = computed(() => {
return frontmatter.value.toc !== false
})
const readingTime = computed(() => {
// 简单的阅读时间计算
const content = page.value.content || ''
const wordsPerMinute = 200
const wordCount = content.split(/\s+/).length
return Math.ceil(wordCount / wordsPerMinute)
})
onMounted(async () => {
await loadPostData()
await loadRelatedPosts()
await loadNavigation()
incrementViewCount()
})
async function loadPostData() {
try {
const response = await fetch(`/api/posts/${page.value.relativePath}/data`)
const data = await response.json()
viewCount.value = data.viewCount || 0
likeCount.value = data.likeCount || 0
isLiked.value = data.isLiked || false
isBookmarked.value = data.isBookmarked || false
} catch (error) {
console.error('Failed to load post data:', error)
}
}
async function loadRelatedPosts() {
try {
const response = await fetch(`/api/posts/${page.value.relativePath}/related`)
relatedPosts.value = await response.json()
} catch (error) {
console.error('Failed to load related posts:', error)
}
}
async function loadNavigation() {
try {
const response = await fetch(`/api/posts/${page.value.relativePath}/navigation`)
const data = await response.json()
prevPost.value = data.prev
nextPost.value = data.next
} catch (error) {
console.error('Failed to load navigation:', error)
}
}
async function incrementViewCount() {
try {
await fetch(`/api/posts/${page.value.relativePath}/view`, {
method: 'POST'
})
viewCount.value++
} catch (error) {
console.error('Failed to increment view count:', error)
}
}
async function toggleLike() {
try {
const response = await fetch(`/api/posts/${page.value.relativePath}/like`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: isLiked.value ? 'unlike' : 'like'
})
})
const data = await response.json()
likeCount.value = data.likeCount
isLiked.value = !isLiked.value
// 记录分析事件
if (typeof gtag !== 'undefined') {
gtag('event', isLiked.value ? 'like_post' : 'unlike_post', {
event_category: 'engagement',
event_label: page.value.relativePath
})
}
} catch (error) {
console.error('Failed to toggle like:', error)
}
}
async function toggleBookmark() {
try {
const response = await fetch(`/api/posts/${page.value.relativePath}/bookmark`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
action: isBookmarked.value ? 'unbookmark' : 'bookmark'
})
})
isBookmarked.value = !isBookmarked.value
// 显示提示
const message = isBookmarked.value ? '已添加到收藏' : '已从收藏中移除'
showToast(message)
} catch (error) {
console.error('
# Personal Blog
本文档正在建设中,敬请期待。
## 概述
这里将提供关于 Personal Blog 的详细信息和指导。
## 主要内容
- 基础概念介绍
- 使用方法说明
- 最佳实践建议
- 常见问题解答
## 相关资源
- [VitePress 官方文档](https://vitepress.dev/)
- [Vue.js 官方文档](https://vuejs.org/)
- [更多教程](../tutorials/index)
---
*本文档将持续更新,如有问题请通过 [GitHub Issues](https://github.com/shingle666) 反馈。*