用Vue3开发个人博客系统,从0到上线有哪些必须提前避坑的细节和高效落地的方法?
刚开始准备启动这个项目的时候,我以为就是找个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前端网发表,如需转载,请注明页面地址。
code前端网

