Skip to content

个人博客

使用 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) 反馈。*

vitepress开发指南