一、背景:一个被搜索引擎"拒之门外"的 SPA
我们的项目是一个基于 Vue 3 构建的内容型单页应用,上线初期为了开发便利,路由直接采用了默认的 hash 模式——URL 形如:
https://example.com/#/article/123
https://example.com/#/category/seo
开发阶段一切顺畅,直到上线三个月后复盘数据时发现一个残酷的事实:Google 收录量几乎为零,百度更是完全没有索引。Search Console 里提交的 sitemap 显示"已发现但未编入索引",抓取到的页面内容永远是首页那一份空壳 HTML。
问题的根源很明确:hash 路由的 # 之后的内容不会被发送到服务器,搜索引擎爬虫拿到的永远是同一个 URL 和同一份 HTML。对于依赖内容分发的站点来说,这等于自断流量命脉。
于是我们启动了这次 SEO 改造,核心目标只有一个:把 hash 路由迁移到 History API 模式,让每个页面拥有真实可被抓取的 URL。
二、为什么 hash 路由对 SEO 不友好
在动手之前,有必要把原理说清楚,避免团队里有人觉得"改个路由模式而已"。
- URL 片段不参与服务端请求:
#后面的内容由浏览器本地解析,服务器永远只收到https://example.com/,无法针对不同页面返回不同内容。 - 爬虫难以执行完整 JS:虽然 Google 宣称能渲染 JS,但渲染预算有限、排队时间长,对中小站点极不友好;百度、必应对 SPA 的渲染支持更弱。
- 无法做服务端差异化:没有独立 URL,就无法配置独立的 title、meta description、canonical、结构化数据。
- 分享与统计受限:社交平台抓取、第三方统计工具对 hash 的识别都存在偏差。
结论:hash 模式适合后台管理系统,绝不适合面向搜索引擎的内容站点。
三、迁移方案总览
整个改造分为四层,缺一不可:
| 层级 | 改造内容 | 关键点 |
|---|---|---|
| 路由层 | hash → history 模式 | createWebHistory |
| 服务端 | 配置 fallback 到 index.html | Nginx / CDN 规则 |
| 渲染层 | 预渲染或 SSR,输出真实 HTML | 解决空壳问题 |
| 元信息层 | 动态 title / meta / canonical | 配合路由变化 |
下面逐层展开。
四、路由层:从 hash 到 history
Vue Router 的迁移本身很简单:
// 改造前
import { createRouter, createWebHashHistory } from 'vue-router'
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 改造后
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes
})
同时要处理两件事:
- 旧链接兼容:在路由守卫里检测到带
#/的旧 URL,用router.replace重定向到新路径,避免老用户和已收录链接 404。 - base 路径:如果站点部署在子目录,
createWebHistory('/sub/')要写对,否则所有资源路径都会错。
五、服务端:History 模式的"生命线"
history 模式下,用户直接访问 https://example.com/article/123 时,服务器必须返回 index.html,否则就是 404。这是最容易翻车的一步。
Nginx 配置:
location / {
try_files $uri $uri/ /index.html;
}
如果用了 CDN 或对象存储,需要在控制台配置"404 回源到 index.html"或"错误文档重定向"。注意:这一步只解决"能打开",不解决"有内容",真正的 SEO 问题还在下一层。
六、渲染层:让爬虫看到真实内容
history 模式解决了 URL 问题,但爬虫拿到的仍然是空壳 HTML。我们对比了三种方案:
- 纯 CSR + 动态渲染(Dynamic Rendering):用 Puppeteer 判断 UA,爬虫来临时渲染后返回。实现快,但维护成本高,且 Google 已不推荐。
- 预渲染(Prerender):构建时用
vite-plugin-prerender或prerender-spa-plugin为每个路由生成静态 HTML。适合内容更新不频繁的站点,我们最终选择了这条路线。 - SSR / SSG:用 Nuxt 或 Vite SSR 彻底改造。效果最好,但改造成本最高,周期不允许。
最终采用预渲染 + 增量更新:文章发布时触发构建,生成对应静态页;对时效性要求高的列表页保留动态渲染兜底。
七、元信息层:每个页面都要"独一无二"
URL 和内容解决后,还要保证每个页面的 head 信息独立。我们用 @vueuse/head 或 vue-meta 在路由切换时动态注入:
useHead({
title: article.title + ' - 站点名',
meta: [
{ name: 'description', content: article.summary },
{ property: 'og:title', content: article.title }
],
link: [
{ rel: 'canonical', href: `https://example.com/article/${article.id}` }
]
})
canonical 标签尤其重要:预渲染页面和 SPA 路由可能产生重复内容,canonical 能明确告诉搜索引擎哪个是主版本。
八、收尾:301 重定向与 sitemap 重建
迁移完成后还有两件必做之事:
- 旧 URL 301 重定向:把
/#/article/123永久重定向到/article/123,把历史权重传递过来。 - 重建 sitemap:用构建脚本扫描所有路由,生成包含真实 URL 的
sitemap.xml,并重新提交到 Search Console 和百度站长平台。
九、效果与复盘
改造上线后第 6 周开始出现明显变化:
- Google 收录量从个位数增长到 1200+;
- 百度收录从 0 增长到 400+;
- 自然搜索流量占全站比例从不足 2% 提升到 31%;
- 核心关键词排名进入前 3 页。
复盘下来,最关键的不是路由模式本身,而是**"URL 可达 + 内容可抓 + 元信息独立"这三件事必须同时做到**。只改路由不改渲染,等于白改;只做预渲染不改路由,爬虫依然进不来。
十、给同类项目的建议
如果你正准备做同样的改造,按这个顺序推进可以少走弯路:
- 先确认服务端 fallback 配置无误,再动前端路由;
- 优先选择预渲染而非动态渲染,长期维护成本更低;
- 路由切换时同步更新 title、description、canonical,别漏;
- 迁移后立即做 301 和 sitemap 重建,别让历史权重流失;
- 用 Search Console 的"网址检查"逐个验证抓取效果,别只看收录数字。
SPA 不是 SEO 的天敌,hash 路由才是。把 URL 还给搜索引擎,内容站点才能真正被看见。
未经允许不得转载:任鹏个人博客 » SPA 站点 SEO 改造实录:从 hash 路由迁移到 History API 的完整方案


朋友圈点赞图在线生成源码