背景
观察基于 create-react-doc 搭建的文档站点, 发现网页代码光秃秃的一片(见下图)。这显然是单页应用 (SPA) 站点的通病 —— 不利于文档被搜索引擎搜索 (SEO)。
难道 SPA 站点就无法进行 SEO 了么, 那么 Gatsby、nuxt 等框架又为何能作为不少博主搭建博客的首选方案呢, 此类框架赋能 SEO 的技术原理是什么呢? 在好奇心的驱动下, 笔者尝试对 creat-react-doc 进行赋能 SEO 之旅。
搜索引擎优化
在实践之前, 先从理论上分析为何单页应用不能被搜索引擎搜索到。核心在于 爬虫蜘蛛在执行爬取的过程中, 不会去执行网页中的 JS 逻辑
, 所以隐藏在 JS 中的跳转逻辑也不会被执行
。
查看当前 SPA 站点打包后的代码, 除了一个根目录 index.html 外, 其它都是注入的 JS 逻辑, 因此浏览器自然不会对其进行 SEO。
此外, 搜索引擎详优化是一门较复杂的学问。如果你对 SEO 优化比较陌生, 建议阅读搜索引擎优化 (SEO) 新手指南 一文, Google 搜索中心给出了全面的 17 个最佳做法, 以及 33 个应避免的做法, 这也是笔者近期在实践的部分。
SEO 在 SPA 站点中的实践案例
在轻文档站点的背景前提下, 我们暂不考虑 SSR 方案。
对市面上文档站点的 SEO 方案调研后, 笔者总结为如下四类:
- 静态模板渲染方案
- 404 重定向方案
- SSG 方案
- 预渲染方案
静态模板渲染方案
静态模板渲染方案以 hexo 最为典型, 此类框架需要指定特定的模板语言(比如 pug)来开发主题, 从而达到网页内容直出的目的。
404 重定向方案
404 重定向方案的原理主要是利用 GitHub Pages 的 404 机制进行重定向。比较典型的案例有 spa-github-pages、sghpa。
但是遗憾的是 2019 年 Google 调整了爬虫算法, 因此此类重定向方案在当下是无利于 SEO 的。spa-github-pages 作者也表示如果需要 SEO 的话, 使用 SSG 方案或者付费方案 Netlify。
SSG 方案
SSG 方案全称为 static site generator, 中文可译为路由静态化方案
。社区上 nuxt、Gatsby 等框架赋能 SEO 的技术无一例外可以归类此类 SSG 方案。
以 nuxt 框架为例, 在约定式路由
的基础上, 其通过执行 nuxt generate
命令将 vue 文件转化为静态网页。
例子:
-| pages/
---| about.vue/
---| index.vue/
静态化后变成:
-| dist/
---| about/
-----| index.html
---| index.html
经过路由静态化后, 此时的文档目录结构可以托管于任何一个静态站点服务商。
预渲染方案
经过上文对 SSG 方案的分析, 此时 SPA 站点的优化关键已经跃然纸上 —— 静态化路由
。相较于 nuxt、Gatsby 等框架存在约定式路由的限制, create-react-doc 在目录结构上的组织灵活自由。它的建站理念是文件即站点
, 同时它对存量 markdown 文档的迁移也十分便捷。
以 blog 项目结构为例, 其文档结构如下:
-| BasicSkill/
---| basic/
-----| DOM.md
-----| HTML5.md
静态化后应该变成:
-| BasicSkill/
---| basic/
-----| DOM
-------| index.html
-----| HTML5
-------| index.html
经过调研, 该构思与 prerender-spa-plugin 预渲染方案一拍即合。预渲染方案的原理可以见如下图:
至此技术选型定下为使用预渲染方案实现 SSG。
预渲染方案实践
create-react-doc 在预渲染方案实践的步骤简单概况如下(完整改动可见 mr):
- 改造 hash 路由为 history 路由。因为 history 路由结构与文档静态化目录结构天然匹配。
export default function RouterRoot() {
return (
- <HashRouter>
+ <BrowserRouter>
<RoutersContainer />
- </HashRouter>
+ </BrowserRouter>
)
}
- 在开发环境、生成环境的基础上新增
预渲染环境
, 同时对路由进行环境匹配。其主要解决了资源文件
与主域名下的子路径
的对应关系。过程比较曲折, 感兴趣的同学可以见 issue。
const ifProd = env === 'prod'
+ const ifPrerender = window.__PRERENDER_INJECTED && window.__PRERENDER_INJECTED.prerender
+ const ifAddPrefix = ifProd && !ifPrerender
<Route
key={item.path}
exact
- path={item.path}
+ path={ifAddPrefix ? `/${repo}${item.path}` : item.path}
render={() => { ... }}
/>
- 兼容 prerender-spa-plugin 在 webpack 5 的使用。
官方版本当前未支持 webpack 5, 详见 issue, 同时笔者存在对预渲染后执行回调的需求。因此当前 fork 了一份版本 出来, 解决了以上问题。
经过上述步骤的实践, 终于在 SPA 站点中实现了静态化路由。
SEO 优化附加 buff, 站点秒开?
SEO 优化至此, 来看下站点优化前后 FP、FCP、LCP 等指标数据的变化。
以 blog 站点为例, 优化前后的指标数据如下(数据指标统计来自未使用梯子访问 gh-pages):
优化前: 接入预渲染方案前, 首次绘制(FP、FCP) 的时间节点在 8s 左右, LCP 在 17s 左右。
优化后: 接入预渲染方案后, 首次绘制时间节点在 1s
之内开始, LCP 在 1.5s 之内。
对比优化前后: 首屏绘制速度提升了 8
倍, 最大内容绘制速度提升 11
倍。本想优化 SEO, 结果站点性能优化的方式又 get 了一个。
生成站点地图 Sitemap
在完成预渲染实现站点路由静态化后, 距离 SEO 的目标又近了一步。暂且抛开 SEO 优化细节, 单刀直入 SEO 核心腹地 站点地图。
站点地图 Sitemap 格式与各字段含义简单说明如下:
<?xml version="1.0" encoding="utf-8"?>
<urlset>
<!-- 必填标签, 这是具体某一个链接的定义入口,每一条数据都要用 <url> 和 </url> 包含在里面, 这是必须的 -->
<url>
<!-- 必填, URL 链接地址,长度不得超过 256 字节 -->
<loc>http://www.yoursite.com/yoursite.html</loc>
<!-- 可以不提交该标签, 用来指定该链接的最后更新时间 -->
<lastmod>2021-03-06</lastmod>
<!-- 可以不提交该标签, 用这个标签告诉此链接可能会出现的更新频率 -->
<changefreq>daily</changefreq>
<!-- 可以不提交该标签, 用来指定此链接相对于其他链接的优先权比值,此值定于 0.0-1.0 之间 -->
<priority>0.8</priority>
</url>
</urlset>
上述 sitemap 中, lastmod、changefreq、priority 字段对 SEO 没那么重要, 可以见 how-to-create-a-sitemap
根据上述结构, 笔者开发了 create-react-doc 的站点地图生成包 crd-generator-sitemap, 其逻辑就是将预渲染的路由路径拼接成上述格式。
使用方只需在站点根目录的 config.yml
添加如下参数便可以在自动化发版过程中自动生成 sitemap。
seo:
google: true
将生成的站点地图往 Google Search Console 中提交试试吧,
最后验证下 Google 搜索站点优化前后效果。
优化前: 只搜索到一条数据。
优化后: 搜索到站点地图中声明的位置数据。
至此使用 SSG 优化 SPA 站点实现 SEO 的完整流程完整实现了一遍。后续便剩下参照 搜索引擎优化 (SEO) 新手指南 做一些 SEO 细节方面的优化以及支持更多搜索引擎了。
小结
本文从 SPA 站点实现 SEO 作为切入点, 先后介绍了 SEO 的基本原理, SEO 在 SPA 站点中的 4 种实践案例, 并结合 create-react-doc SPA 框架进行完整的 SEO 实践。
相关链接
- create-react-doc
- why-is-my-website-not-showing-up-on-google/
- A Technical Guide to SEO With Gatsby.js
- 优化向:单页应用多路由预渲染指南
- 除了 SSR,就没有别的办法了吗?
- 基于 SSR/SSG 的前端 SEO 优化