Code前端首页关于Code前端联系我们

用Vue3开发个人博客系统,从0到上线有哪些必须提前避坑的细节和高效落地的方法?

terry 1小时前 阅读数 22 #Vue

刚开始准备启动这个项目的时候,我以为就是找个UI库、搭个页面连接口的事儿,结果踩了不下20个大大小小的坑——比如路由切换动画卡得离谱、SSR预渲染和SPA hydration冲突炸了两次本地预览、Markdown编辑器的图片上传到OSS后链接带特殊字符SEO蜘蛛爬不动……前前后后折腾了1个月才勉强上线能用的版本,后来又优化了3周,才算能拿出手,现在把这些踩过的坑和摸索出来的高效方法整理清楚,不管你是第一次用Vue3练手全栈,还是想搞个长期更文的个人站,应该都能省不少事。

项目启动前要先想清楚的3个“定海神针”

很多人一开始就急着敲代码、装UI库,结果写到一半发现风格不对、架构不合理,又推倒重来,其实写之前先花1天想明白这3件事,能少走90%的冤枉路。

技术栈要“按需选轻量”,别为了炫技堆太多

我见过太多练手Vue3博客的人,上来就直接套Vben Admin或者Nuxt.js 4的全栈模板,结果打开控制台全是自己用不上的依赖,编译速度慢到怀疑人生,遇到问题还得翻几百页的官方文档啃,其实个人博客的核心功能无非就是:内容展示(首页、列表页、详情页、标签分类页)、内容管理(后台登录、文章发布/编辑/删除、标签管理)、基础功能(搜索、评论、点赞、RSS订阅),轻量的技术栈完全够用。

我最后落地的前台技术栈是Vue3 + Vite 5 + Vue Router 4 + Pinia 2 + Tailwind CSS 4,后台是Vue3 + Vite 5 + Element Plus,SSR用的是VitePress的预渲染插件,数据库是SQLite(数据量不大完全足够,不用额外租MySQL,成本直接降了),后端是Node.js + Express(上手快,适合个人开发),图片和附件存的是七牛云OSS(免费额度够个人站用一年多),选这些的理由很简单:Vite编译快,写一行代码刷新一下就能看到效果;Tailwind写样式省时间,不用写重复的CSS类;Pinia状态管理比Vuex简单,适合小型项目;SQLite不用配置服务器数据库,本地直接跑,上线的时候上传到服务器根目录就行。 管理方案要选“能写得爽+SEO友好”的管理是个人博客的灵魂,要是写文章要写HTML标签、图片要一个个转base64,那写一次两次还行,长期更文肯定坚持不下来,还有SEO的问题,要是博客内容是纯SPA渲染的,百度、Google这些搜索引擎的蜘蛛根本爬不到,写再多文章也没人看。

我一开始试了两种方案:第一种是直接用VitePress写内容前台,用VitePress的默认后台(其实就是修改Markdown文件),但后来发现后台的功能太弱了,标签管理、文章置顶、草稿箱这些都没有,还要手动修改文件的frontmatter,太麻烦;第二种是自己写后台,用Markdown编辑器写内容,然后存到SQLite数据库里,前台用axios请求接口获取数据,但后来发现纯SPA渲染的SEO效果太差了,爬取的全是“Loading...”。

最后我用了第三种方案:前台用Vue3 + Tailwind写静态页面框架,然后用VitePress的预渲染插件(@vitejs/plugin-vue-prerender或者vite-plugin-ssr都可以,我用的是后者,配置更灵活)预渲染首页、列表页、详情页、标签分类页的静态HTML,后台用Vue3 + Element Plus自己写,用Markdown-it + highlight.js渲染Markdown,图片上传到七牛云OSS,存图片链接的时候把七牛云的CDN加速域名也带上,这样访问速度快,SEO也友好,Markdown编辑器选的是md-editor-v3,这个编辑器是专门为Vue3开发的,功能很全,支持Markdown-it的所有插件,还支持图片拖拽上传、代码高亮、表格编辑、数学公式编辑,写起来特别爽。

服务器和域名要选“便宜+稳定+符合备案要求”的

如果只是想练手Vue3,本地跑或者用GitHub Pages部署就行,但如果想做个长期更文的个人站,还是得买个服务器和域名,还得备案(如果用国内服务器的话),备案虽然麻烦一点,但国内访问速度快,而且百度收录的优先级也更高。

服务器我选的是腾讯云的轻量应用服务器,1核2G 5M带宽,一年只要68块钱(新人优惠),够用了,如果预算多一点,可以选2核4G的,一年大概128块钱,域名我选的是阿里云的,.com后缀的一年大概50块钱,.cn后缀的更便宜,一年大概30块钱,备案的话,阿里云和腾讯云都有免费的备案服务,大概1-2周就能下来。

开发阶段要避的7个“致命坑”

启动前想清楚了技术栈、内容管理方案、服务器和域名,接下来就是敲代码了,这个阶段我踩的坑最多,这里挑7个最致命的给大家说一下。

坑1:路由切换动画用transition-group,卡得怀疑人生

刚开始写前台的时候,我为了让页面看起来更流畅,给router-view加了transition-group动画,结果路由切换的时候卡得要命,尤其是在列表页切换到详情页,图片多的时候要等3-5秒才能加载完,后来查了很多资料才知道,transition-group适合处理列表元素的添加、删除、移动,不适合处理整个页面的路由切换,因为transition-group会把所有的子元素都包裹在一个div里,导致DOM节点变多,渲染变慢。

正确的做法是给router-view加普通的transition动画,然后用v-slot:default获取当前路由组件,用name属性绑定动画名称,用mode属性设置动画模式(out-in或者in-out,推荐用out-in,体验更好),如果想让不同的页面有不同的动画,可以在路由配置里加meta字段,然后在transition的name属性里动态绑定meta.animation。

<!-- App.vue -->
<template>
  <div class="min-h-screen bg-gray-50">
    <Header />
    <main class="container mx-auto px-4 py-8">
      <router-view v-slot="{ Component, route }">
        <transition
          :name="route.meta.animation || 'fade'"
          mode="out-in"
          enter-active-class="duration-300 ease-out"
          leave-active-class="duration-200 ease-in"
          enter-from-class="opacity-0"
          leave-to-class="opacity-0"
        >
          <component :is="Component" :key="route.path" />
        </transition>
      </router-view>
    </main>
    <Footer />
    <BackToTop />
  </div>
</template>

坑2:SSR预渲染和SPA hydration冲突,页面炸了两次

刚才说过,我用的是vite-plugin-ssr做预渲染,刚开始写的时候,直接把Vue3组件的created和mounted钩子函数里的代码都写了,结果预渲染的时候没问题,但上线后访问页面,页面会先闪一下预渲染的内容,然后变成空白,或者出现一些奇怪的错误(Hydration completed but contains mismatches”),后来查了vite-plugin-ssr的官方文档才知道,预渲染的时候是在Node.js环境下运行的,没有window和document对象,而mounted钩子函数是在客户端运行的,有window和document对象,如果在created或者beforeMount钩子函数里用了window或者document对象,就会导致预渲染和客户端hydration的内容不一致,从而出现冲突。

正确的做法是:在预渲染的时候只处理数据获取,不处理DOM操作;把所有的DOM操作都放在mounted或者onMounted钩子函数里;如果需要在预渲染的时候获取数据,可以用vite-plugin-ssr提供的usePageContext钩子函数,在getServerSideProps或者getStaticProps函数里获取数据,然后通过pageContext传递给客户端;如果有些组件不需要预渲染,可以用vite-plugin-ssr提供的ClientOnly组件包裹起来。

// pages/index/+data.ts(获取首页数据的文件,预渲染的时候会自动调用)
import { PageContextServer } from 'vite-plugin-ssr/types'
import { getArticleList, getTagList } from '../api'
export async function data(pageContext: PageContextServer) {
  const [articleList, tagList] = await Promise.all([
    getArticleList({ page: 1, pageSize: 10, isTop: true }),
    getTagList()
  ])
  return {
    articleList,
    tagList
  }
}
<!-- 客户端组件,不需要预渲染的部分可以用ClientOnly包裹 -->
<template>
  <div class="article-list">
    <div v-for="article in pageContext.data.articleList" :key="article.id" class="article-item mb-8 bg-white rounded-lg shadow-md p-6">
      <h2 class="text-2xl font-bold mb-2 text-gray-800 hover:text-blue-600 transition-colors">
        <router-link :to="`/article/${article.id}`">{{ article.title }}</router-link>
      </h2>
      <div class="flex items-center text-gray-500 text-sm mb-4">
        <span class="mr-4">📅 {{ formatDate(article.createdAt) }}</span>
        <span class="mr-4">👁️ {{ article.views }}</span>
        <span v-for="tag in article.tags" :key="tag.id" class="mr-2 bg-gray-100 rounded-full px-3 py-1 text-xs text-gray-700">
          {{ tag.name }}
        </span>
      </div>
      <p class="text-gray-600 line-clamp-3">{{ article.summary }}</p>
    </div>
    <!-- 分页组件不需要预渲染,用ClientOnly包裹 -->
    <ClientOnly>
      <Pagination
        :current-page="page"
        :total-pages="totalPages"
        @change-page="handleChangePage"
      />
    </ClientOnly>
  </div>
</template>
<script setup lang="ts">
import { usePageContext } from 'vite-plugin-ssr/vue'
import { formatDate } from '../utils'
import ClientOnly from '../components/ClientOnly.vue'
import Pagination from '../components/Pagination.vue'
const pageContext = usePageContext<{
  data: {
    articleList: Article[]
    tagList: Tag[]
  }
}>()
</script>

坑3:Markdown-it渲染的代码高亮没生效,或者样式不对

md-editor-v3编辑器支持highlight.js和Prism.js两种代码高亮方式,我一开始选的是Prism.js,结果渲染的时候样式不对,而且有些语言(比如TypeScript、Go)的高亮效果不好,后来换成了highlight.js,结果渲染的时候代码高亮没生效,查了很多资料才知道,是因为我只在md-editor-v3的配置里引入了highlight.js,没有在前台的Markdown渲染组件里引入highlight.js和对应的样式文件。

正确的做法是:在前台的项目里安装highlight.js和@highlightjs/vue-plugin(如果用Vue3的话),然后在main.ts里引入highlight.js和对应的样式文件,再注册@highlightjs/vue-plugin组件;或者在前台的Markdown渲染组件里手动调用highlight.js的highlightAll()函数,推荐用highlight.js的github-dark或者github-light样式,和大部分个人博客的风格都比较搭。

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from './stores'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'
import hljsVuePlugin from '@highlightjs/vue-plugin'
const app = createApp(App)
app.use(router)
app.use(pinia)
app.use(hljsVuePlugin)
app.mount('#app')
<!-- Markdown渲染组件 -->
<template>
  <div class="markdown-body">
    <client-only>
      <div ref="markdownRef" v-html="renderedMarkdown"></div>
    </client-only>
  </div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import MarkdownIt from 'markdown-it'
import hljs from 'highlight.js'
import 'highlight.js/styles/github-dark.css'
const props = defineProps<{
  content: string
}>()
const markdownRef = ref<HTMLElement | null>(null)
const md = new MarkdownIt({
  html: true,
  linkify: true,
  typographer: true,
  highlight: function (str, lang) {
    if (lang && hljs.getLanguage(lang)) {
      try {
        return hljs.highlight(str, { language: lang }).value
      } catch (__) {}
    }
    return ''
  }
})
const renderedMarkdown = ref('')
const renderMarkdown = () => {
  renderedMarkdown.value = md.render(props.content)
  if (markdownRef.value) {
    // 手动调用highlightAll()函数,确保代码高亮生效
    hljs.highlightAll()
  }
}
onMounted(() => {
  renderMarkdown()
})
watch(() => props.content, () => {
  renderMarkdown()
})
</script>
<style scoped>
.markdown-body {
  line-height: 1.8;
  font-size: 16px;
  color: #333;
}
.markdown-body h1 {
  font-size: 2.5rem;
  font-weight: bold;
  margin: 2rem 0 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid #e5e7eb;
}
.markdown-body h2 {
  font-size: 2rem;
  font-weight: bold;
  margin: 1.8rem 0 0.8rem;
  padding-bottom: 0.3rem;
  border-bottom: 1px solid #e5e7eb;
}
/* 其他Markdown样式可以自己写,也可以用github-markdown-css */
</style>

坑4:七牛云OSS上传的图片链接带特殊字符,SEO蜘蛛爬不动

md-editor-v3编辑器的图片上传功能支持自定义上传接口,我一开始写的上传接口是把图片上传到七牛云OSS后,直接返回七牛云返回的key(比如2024/05/20/abc123.jpg),然后前台拼接七牛云的CDN加速域名(比如https://cdn.example.com/2024/05/20/abc123.jpg),结果上线后发现百度SEO蜘蛛爬不到图片,后来查了百度站长平台的帮助文档才知道,百度SEO蜘蛛对带有中文、空格、特殊字符(、?、&)的URL不太友好,有时候会直接跳过。

正确的做法是:在上传图片到七牛云OSS之前,先给图片重命名,重命名的规则可以是:时间戳 + 随机字符串 + 原文件的后缀名(比如1716192000000_xyz789.jpg);如果原文件的后缀名是大写的,要转成小写的;七牛云OSS返回的key也可以自己指定,不需要用默认的,上传图片的时候最好加上alt属性,alt属性可以描述图片的内容,对SEO很友好。

// 后端Node.js + Express的图片上传接口
import express from 'express'
import multer from 'multer'
import qiniu from 'qiniu'
import { v4 as uuidv4 } from 'uuid'
import dayjs from 'dayjs'
const router = express.Router()
const storage = multer.memoryStorage()
const upload = multer({ storage: storage })
// 七牛云OSS配置
const accessKey = 'your-access-key'
const secretKey = 'your-secret-key'
const bucket = 'your-bucket-name'
const cdnDomain = 'https://cdn.example.com'
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
const putPolicy = new qiniu.rs.PutPolicy({
  scope: bucket
})
const uploadToken = putPolicy.uploadToken(mac)
const config = new qiniu.conf.Config()
const formUploader = new qiniu.form_up.FormUploader(config)
const putExtra = new qiniu.form_up.PutExtra()
router.post('/upload', upload.single('file'), async (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({ code: 400, message: '请选择要上传的图片' })
    }
    // 检查文件类型
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
    if (!allowedTypes.includes(req.file.mimetype)) {
      return res.status(400).json({ code: 400, message: '只支持上传JPEG、PNG、GIF、WEBP格式的图片' })
    }
    // 检查文件大小(限制在10MB以内)
    if (req.file.size > 10 * 1024 * 1024) {
      return res.status(400).json({ code: 400, message: '图片大小不能超过10MB' })
    }
    // 重命名图片
    const timestamp = dayjs().valueOf()
    const randomStr = uuidv4().split('-')[0]
    const ext = req.file.originalname.split('.').pop()?.toLowerCase() || 'jpg'
    const key = `${dayjs().format('YYYY/MM/DD')}/${timestamp}_${randomStr}.${ext}`
    // 上传图片到七牛云OSS
    formUploader.put(uploadToken, key, req.file.buffer, putExtra, (respErr, respBody, respInfo) => {
      if (respErr) {
        console.error(respErr)
        return res.status(500).json({ code: 500, message: '图片上传失败' })
      }
      if (respInfo.statusCode === 200) {
        // 返回CDN加速链接
        const url = `${cdnDomain}/${key}`
        return res.status(200).json({ code: 200, message: '图片上传成功', data: { url } })
      } else {
        console.error(respBody)
        return res.status(500).json({ code: 500, message: '图片上传失败' })
      }
    })
  } catch (error) {
    console.error(error)
    return res.status(500).json({ code: 500, message: '服务器错误' })
  }
})
export default router

坑5:Pinia状态管理里的数据刷新页面后丢失

刚开始写后台的时候,我把登录后的用户信息和token都存到了Pinia状态管理里,结果刷新页面后用户信息和token都丢失了,又要重新登录,体验非常差,后来查了很多资料才知道,Pinia状态管理里的数据是存在内存里的,刷新页面后内存会清空,所以数据会丢失。

正确的做法是:把用户信息和token存到localStorage或者sessionStorage里,然后在Pinia状态管理的初始化函数里从localStorage或者sessionStorage里读取数据;如果用户退出登录,要把localStorage或者sessionStorage里的数据清空,token最好加上过期时间,过期后要自动清除并跳转到登录页面。

// stores/user.ts(Pinia状态管理用户信息的文件)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi } from '../api'
export const useUserStore = defineStore('user', () => {
  // 从localStorage里读取用户信息和token
  const token = ref<string>(localStorage.getItem('token') || '')
  const userInfo = ref<UserInfo | null>(localStorage.getItem('userInfo') ? JSON.parse(localStorage.getItem('userInfo')!) : null)
  // 计算属性:判断用户是否登录
  const isLoggedIn = computed(() => !!token.value)
  // 登录函数
  const login = async (username: string, password: string) => {
    const res = await loginApi({ username, password })
    if (res.code === 200) {
      token.value = res.data.token
      userInfo.value = res.data.userInfo
      // 把用户信息和token存到localStorage里
      localStorage.setItem('token', res.data.token)
      localStorage.setItem('userInfo', JSON.stringify(res.data.userInfo))
      return true
    }
    return false
  }
  // 退出登录函数
  const logout = () => {
    token.value = ''
    userInfo.value = null
    // 清除localStorage里的数据
    localStorage.removeItem('token')
    localStorage.removeItem('userInfo')
  }
  return {
    token,
    userInfo,
    isLoggedIn,
    login,
    logout
  }
})
// router/index.ts(路由守卫,判断用户是否登录)
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores/user'
const routes = [
  // 前台路由
  {
    path: '/',
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  // 后台路由
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('../views/admin/Layout.vue'),
    meta: { requiresAuth: true },
    children: [
      {
        path: '',
        name: 'AdminDashboard',
        component: () => import('../views/admin/Dashboard.vue')
      },
      {
        path: 'articles',
        name: 'AdminArticles',
        component: () => import('../views/admin/Articles.vue')
      }
    ]
  }
]
const router = createRouter({
  history: createWebHistory(),
  routes
})
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    // 如果用户未登录且访问后台路由,跳转到登录页面
    next('/admin/login')
  } else {
    next()
  }
})
export default router

坑6:搜索功能用的是前端模糊搜索,数据多了慢得要命

刚开始写前台搜索功能的时候,我为了省事,直接用前端模糊搜索,把所有的文章数据都存到Pinia状态管理里,然后用户输入关键词的时候,用filter方法过滤文章数据,结果后来我存了100多篇文章,搜索的时候要等2-3秒才能出结果,体验非常差。

正确的做法是:如果数据量不大(比如500篇以内),可以用lunr.js或者flexsearch.js做前端全文搜索,速度比原生的filter方法快很多;如果数据量比较大(比如500篇以上),最好用后端全文搜索,比如SQLite的FTS5插件,或者Elasticsearch,我后来用的是flexsearch.js,因为它的配置更简单,速度更快,支持中文搜索。

// stores/search.ts(Pinia状态管理搜索功能的文件)
import { defineStore } from 'pinia'
import { ref } from 'vue'
import FlexSearch from 'flexsearch'
import { getArticleList } from '../api'
export const useSearchStore = defineStore('search', () => {
  const searchResults = ref<Article[]>([])
  const isSearching = ref(false)
  // 创建FlexSearch索引
  const index = new FlexSearch.Index({
    tokenize: 'forward',
    charset: 'latin:extra',
    lang: 'zh'
  })
  // 初始化搜索索引
  const initSearchIndex = async () => {
    isSearching.value = true
    try {
      // 获取所有的文章数据
      const articleList = await getArticleList({ page: 1, pageSize: 1000 })
      // 把文章数据添加到搜索索引里
      articleList.forEach((article: Article) => {
        index.add(article.id, `${article.title} ${article.summary} ${article.tags.map((tag: Tag) => tag.name).join(' ')}`)
      })
    } catch (error) {
      console.error(error)
    } finally {
      isSearching.value = false
    }
  }
  // 搜索函数
  const search = async (keyword: string) => {
    if (!keyword.trim()) {
      searchResults.value = []
      return
    }
    isSearching.value = true
    try {
      // 如果搜索索引还没初始化,先初始化
      if (index.length === 0) {
        await initSearchIndex()
      }
      // 搜索关键词
      const ids = index.search(keyword.trim(), 20)
      // 根据id获取文章数据
      const allArticleList = await getArticleList({ page: 1, pageSize: 1000 })
      searchResults.value = allArticleList.filter((article: Article) => ids.includes(article.id))
    } catch (error) {
      console.error(error)
      searchResults.value = []
    } finally {
      isSearching.value = false
    }
  }
  return {
    searchResults,
    isSearching,
    initSearchIndex,
    search
  }
})

坑7:评论功能用的是自己写的,没人评论还容易被刷垃圾评论

刚开始写评论功能的时候,我为了省事,直接用自己写的,结果上线后一个月都没人评论,还被刷了几十条垃圾评论(加微信xxx”“炒股群xxx”),删除都删不过来,后来查了很多资料才知道,个人博客的评论功能最好用第三方的,比如Valine、Waline、Giscus,这些第三方评论系统都有防垃圾评论的功能,而且用户可以用GitHub、QQ、微信等账号登录,评论的门槛更低。

我后来用的是Waline,因为它的配置更简单,支持Markdown评论,支持邮件通知,支持表情,支持匿名评论,而且部署也很方便,可以直接部署到Vercel或者腾讯云开发上,Giscus也不错,它是基于GitHub Discussions的,评论数据会存到GitHub Discussions里,不用担心数据丢失,但需要用户有GitHub账号,门槛稍微高一点。

上线优化阶段要做的5个“加分项”

开发阶段避完坑,上线后还要做一些优化,才能让博客的访问速度更快,SEO效果更好,用户体验更好。

优化1:图片懒加载+压缩+CDN加速

图片是博客加载速度最慢的资源之一,所以一定要对图片做优化,优化的方法有三个:第一个是图片懒加载,就是只加载用户视口内的图片,视口外的图片等用户滚动到的时候再加载;第二个是图片压缩,把图片的大小压缩到最小,同时不影响图片的质量;第三个是CDN加速,把图片存到CDN上,让用户从最近的节点加载图片。

图片懒加载可以用Vue3的v-lazy指令,或者用Intersection Observer API自己写,我后来用的是v-lazy指令,因为它的配置更简单,图片压缩可以用TinyPNG的在线压缩工具,或者用Node.js的sharp库在上传的时候自动压缩,CDN加速我用的是七牛云的CDN加速,前面已经说过了。

<!-- 图片懒加载的示例 -->
<template>
  <div class="article-list">
    <div v-for="article in articleList" :key="article.id" class="article-item">
      <img v-lazy="article.cover" :alt="article.title" class="article-cover" />
      <h2>{{ article.title }}</h2>
    </div>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useLazyLoad } from 'vue3-lazyload'
// 注册v-lazy指令
useLazyLoad({
  loading: 'https://cdn.example.com/loading.gif',
  error: 'https://cdn.example.com/error.jpg'
})
const articleList = ref<Article[]>([
  {
    id: 1, 'Vue3开发个人博客系统的避坑指南',
    cover: 'https://cdn.example.com/2024/05/20/1716192000000_xyz789.jpg'
  }
])
</script>

优化2:SEO优化,让百度、Google更快收录

SEO优化是个人博客最重要的优化之一,要是博客内容没人看,写再多文章也没用,SEO优化的方法有很多,这里挑几个最重要的给大家说一下:第一个是title、meta description、meta keywords标签的优化,每个页面的title、meta description、meta keywords都要不一样,title要包含核心关键词,meta description要简洁明了,概括页面的内容;第二个是URL的优化,URL要短,要包含核心关键词,要用小写字母,要用连字符(-)代替下划线(_);第三个是sitemap.xml的生成,sitemap.xml可以告诉搜索引擎网站的结构,让搜索引擎更快收录网站的页面;第四个是robots.txt的生成,robots.txt可以告诉搜索引擎哪些页面可以爬,哪些页面不能爬;第五个是添加百度统计和Google Analytics,分析网站的流量数据,优化网站的内容。

sitemap.xml和robots.txt可以用vite-plugin-sitemap和vite-plugin-robots自动生成,配置也很简单。

// vite.config.ts(配置vite-plugin-sitemap和vite-plugin-robots)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { createSitemap } from 'vite-plugin-sitemap'
import { createRobots } from 'vite-plugin-robots'
export default defineConfig({
  plugins: [
    vue(),
    createSitemap({
      hostname: 'https://example.com',
      routes: [
        '/',
        '/articles',
        '/tags',
        '/about'
      ],
      dynamicRoutes: async () => {
        // 动态获取文章详情页和标签分类页的路由
        const articleList = await getArticleList({ page: 1, pageSize: 1000 })
        const tagList = await getTagList()
        const articleRoutes = articleList.map((article: Article) => `/article/${article.id}`)
        const tagRoutes = tagList.map((tag: Tag) => `/tag/${tag.id}`)
        return [...articleRoutes, ...tagRoutes]
      }
    }),
    createRobots({
      hostname: 'https://example.com',
      policy: [
        {
          userAgent: '*',
          allow: '/',
          disallow: ['/admin', '/api']
        }
      ],
      sitemap: 'https://example.com/sitemap.xml'
    })
  ]
})

优化3:添加RSS订阅,方便用户订阅博客

RSS订阅是个人博客的标配,虽然现在用RSS订阅的人不多,但还是有一部分忠实用户在用,添加RSS订阅可以让用户用RSS阅读器(比如Feedly、Inoreader)订阅博客,不用每天打开博客就能看到最新的文章。

RSS订阅可以用rss或者feedgen库自动生成,我后来用的是rss库,因为它的配置更简单。

// 后端Node.js + Express的RSS订阅接口
import express from 'express'
import RSS from 'rss'
import { getArticleList } from './api'
const router = express.Router()
router.get('/rss.xml', async (req, res) => {
  try {
    // 获取最新的10篇文章
    const articleList = await getArticleList({ page: 1, pageSize: 10, sortBy: 'createdAt', sortOrder: 'desc' })
    // 创建RSS feed
    const feed = new RSS({
      title: '张三的个人博客',
      description: '分享Vue3、前端开发、全栈开发的经验和技巧',
      feed_url: 'https://example.com/rss.xml',
      site_url: 'https://example.com',
      image_url: 'https://example.com/favicon.png',
      managingEditor: 'zhangsan@example.com (张三)',
      webMaster: 'zhangsan@example.com (张三)',
      copyright: '2024 张三的个人博客',
      language: 'zh-CN',
      pubDate: new Date(),
      ttl: 60
    })
    // 添加文章到RSS feed
    articleList.forEach((article: Article) => {
      feed.item({
        title: article.title,
        description: article.summary,
        url: `https://example.com/article/${article.id}`,
        guid: `https://example.com/article/${article.id}`,
        categories: article.tags.map((tag: Tag) => tag.name),
        author: 'zhangsan@example.com (张三)',
        date: article.createdAt
      })
    })
    // 返回RSS feed
    res.set('Content-Type', 'application/rss+xml')
    res.send(feed.xml({ indent: true }))
  } catch (error) {
    console.error(error)
    res.status(500).send('服务器错误')
  }
})
export default router

优化4:添加评论通知,作者和评论者都能收到邮件

如果用第三方评论系统(比如Waline),可以直接在后台配置邮件通知,作者和评论者都能收到邮件,如果用自己写的评论系统,可以用nodemailer库发送邮件通知。

优化5:添加访问统计,了解网站的流量数据

访问统计可以用百度统计、Google Analytics、CNZZ等第三方工具,这些工具都有免费的版本,而且功能很全,可以分析网站的流量来源、用户画像、页面访问量等数据,我后来用的是百度统计,因为它的中文界面更友好,适合国内用户。

用Vue3开发个人博客系统,从0到上线确实不容易,要避很多坑,还要做很多优化,但只要你按照我上面说的方法去做,应该能省不少事,而且开发出来的博客系统访问速度快,SEO效果好,用户体验好,希望大家都能开发出自己满意的个人博客系统,坚持更文,分享自己的经验和技巧。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门