网上有好几种单页应用转 seo 的方案, 有服务端渲染 ssr, 有预渲染 prerender,google 抓 Ajax, 静态化... 这些方案都各有优劣, 开发者可以根据不同的业务场景和环境决定用哪一种方案. 本文将介绍另一种思路比较清奇的 SEO 方案, 这个方案也是有优有劣, 就看读者觉得适不适合了.
项目分析
我的项目是用 react+ts+dva 技术栈搭建的单页应用, 目前在线上已经有几十个页面, 若干个 sdk 和插件在里面.
考虑想用服务端渲染来做 seo, 但是我的项目已经开发了这么多, 打包配置, 代码分割, 语法兼容, 摒弃浏览器对象, 服务端思想, 这么多的点需要考虑, 还不如换个框架重新开发呢, 所以改造成本太大, 服务端渲染不适合我这种情况.
预渲染虽然是开发成本最低的, 但毕竟是生成一张一张的静态 html, 而我的 seo 需求是能够让蜘蛛抓取到我的社区论坛下的每一篇帖子, 这样子下来一篇帖子就是一份 HTML, 再加上分页, 那得多大的量级来存储啊, 而且网站更新就更麻烦了, 这个方案也不太适合.
google.....Emmmm......................... 下一个
静态化也是跟预渲染差不多...
隆重介绍
以前写过一种单页应用 seo 的方案, 就是自己先在本地用爬虫做预渲染, 生成同样目录结构的静态化的 HTML, 前端项目服务器判断请求的 UA 是搜索引擎蜘蛛的话就会转发到我事先静态化过的 HTML 页面
当时的项目只是一个简单的只有几个页面的企业官网, 预渲染没啥问题.
跟着这个思路, 只要判断搜索引擎蜘蛛让蜘蛛看到另一个有数据的页面不就行了.
至于页面长什么样, 蜘蛛才不会管呢, 就像是你找广告商投放广告, 广告商不会要求你要怎样的主题什么色调, 只要你按照他的尺寸和要求来做, 然后给钱给货就完事了.
所以可以针对 SEO 做另一套网站, 没有样式, 只有符合 seo 规范的 HTML 标签和对应的数据, 不需要在原有项目上改造, 开发成本也不会很高, 体积小加载速度更快.
缺点也有, 就是需要另外维护一套网站, 主网站界面变化不会影响, 如果展示数据有变化就需要同步修改 seo 版的网站.
代码实现
先建个单独的 seo 文件夹, 不需要动到原有项目, 下面是代码结构:
代码实现非常之简单, 只要写一个中间件拦截请求, 鉴别蜘蛛, 返回对应路径的 seo 页面即可.
我的前端服务器是用 express, 可以写个 express 的中间件, 新建 server.JS:
- // seo/server.JS
- const routes = require('./routes')
- const layout_render = require('./src/layout');
- module.exports = (req, res, next) => {
- // 各大搜索引擎蜘蛛 UA
- const spiderUA = /Baiduspider|bingbot|Googlebot|360spider|Sogou|Yahoo! Slurp/
- var isSpider = spiderUA.test(req.get('user-agent'))
- // 获取路由表的路径
- var seoPath = Object.keys(routes)
- if (isSpider) {
- for (let i=0,route; route = seoPath[i]; i++) {
- if (new RegExp(route).test(req.path)) {
- routes[route](req).then((result) => {
- // 返回对应的模板结果给蜘蛛
- res.set({'Content-Type': 'text/html','charset': 'utf-8mb4'}).status(200).send(layout_render(result))
- })
- break;
- }
- }
- } else {
- // 未匹配到蜘蛛则继续后面的中间件
- return next()
- }
- }
然后在前端的启动服务器里加入这个中间件, 记得要放在其他中间件之前
- // 前端启动服务器的 server 文件
- var express = require('express')
- var App = express()
- // seo
- App.use(require('seo/server'));
- ......
- App.listen(xxxx)
接下来就是写模板和对应的解析了, 新建一个 home 文件夹, 文件夹下再建一个 index.ejs 和 index.JS
- <!-- seo/src/home/index.ejs -->
- <div>
- <h1 > 官网首页</h1>
- <p > 友情链接:</p>
- <p><a href="https://www.baidu.com/" target="_blank">百度</a></p>
- <p><a href="https://www.gogole.com/" target="_blank">谷歌</a></p>
- </div>
index.JS 用于解析对应的 ejs 模板
- // seo/src/home/index.JS
- const ejs = require('ejs')
- const fs = require('fs')
- const path = require('path')
- const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8');
- // 这里为什么会有个 async 关键字, 往后面看就可以知道.
- module.exports = async (req) => {
- const result = ejs.render(template)
- return result
- }
我们还可以建多个 layout 模板来管理 head,title 和导航栏这些公有的元素
<!-- seo/layout.ejs -->
- <!DOCTYPE HTML>
- <HTML>
- <head>
- <meta charset="utf-8">
- <meta http-equiv="content-type" content="text/html;charset=utf-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="renderer" content="webkit">
- <meta content="网站关键字" " name="keywords "/>
- <meta content="网站描述 " name="description "/>
- <title > 网站标题</title>
- </head>
- <body>
- <div id="root ">
- <ul>
- <li><a href="/ ">首页</a></li>
- <li><a href="/community
- ">社区</a></li>
- </ul>
- <%- children -%>
- </div>
- </body>
- </HTML>
解析 layout.ejs, 套入内容的 layout_render:
- // seo/layout.JS
- const ejs = require('ejs')
- const fs = require('fs')
- const path = require('path')
- const template = fs.readFileSync(path.resolve(__dirname, './layout.ejs'), 'utf8');
- const layout_render = (children) => {
- return ejs.render(template, {children: children})
- }
- module.exports = layout_render
路由表用简单的键值对就可以了, 键名用字符串形式的正则来表示路径的匹配规则:
- // seo/routes.JS
- const home_route = require('./src/home/index')
- module.exports = {
- '^(/?)$': home_route,
- }
那么数据如何做请求并展示到对应的模板内呢? 数据请求是异步的, 怎样等到请求完成再渲染模板呢?
我们可以用 async/await 来实现, 现在来做一个社区的帖子列表页面, 需要先请求社区下帖子列表数据再把数据渲染到模板, 新建一个 community 文件夹, 同样再建一个 index.ejs 作为帖子列表页面模板:
- <!-- seo/src/community/index.ejs -->
- <div>
- <h1 > 帖子列表</h1>
- <ul>
- <% forum_list.map((item) => { %>
- <li><a href="/community/<%= item.id%>" target="_blank"><%= item.title-%></a></li>
- <% })%>
- </ul>
- </div>
相关的接口请求及数据操作写在同级的 index.JS:
- // seo/src/community/index.JS
- const ejs = require('ejs')
- const fs = require('fs')
- const path = require('path')
- const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8');
- const axios = require('axios');
- module.exports = async (req) => {
- const res = await axios.get('http://xxx.xx/api/community/list')
- const result = ejs.render(template, {forum_list: res.data.list})
- return result
- }
再加上对应的路由配置:
- // seo/routes.JS
- const home_route = require('./src/home/index')
- const community_route = require('./src/community/index')
- module.exports = {
- '^(/?)$': home_route,
- '^/community$': community_route,
- }
这样就实现了先取接口数据再做渲染, 保证了蜘蛛访问能给到完整的数据和 HTML 结构.
继续实现一个帖子详情的页面:
- <!-- seo/src/community_detail/index.ejs -->
- const community_route = require('./src/community/index')
- <div>
- <h1><%= forum_data.title%></h1>
- <p><%= forum_data.content%></p>
- <p > 作者:<%= forum_data.user.nickname%></p>
- </div>
- // seo/src/community_detail/index.JS
- const ejs = require('ejs')
- const fs = require('fs')
- const path = require('path')
- const template = fs.readFileSync(path.resolve(__dirname, './index.ejs'), 'utf8');
- const axios = require('axios');
- module.exports = async (req) => {
- // 获取路径里的 id /community/:id
- const forum_id = req.path.split('/')[2]
- const res = await axios.get(`http://xxx.xx/api/community/${forum_id}/details?offset=1&limit=10`)
- const result = ejs.render(template, {forum_data: res.data})
- return result
- }
同样加上对应的路由配置:
- // seo/routes.JS
- const home_route = require('./src/home/index')
- const community_route = require('./src/community/index')
- const community_detail_route = require('./src/community_detail/index')
- module.exports = {
- '^(/?)$': home_route,
- '^/community$': community_route,
- '^/community/\\d+$': community_detail_route,
- }
这样就实现了一个简单的 seo 版网站, 不需要任何样式, 不需要 JS 做弹框之类的后续交互, 只要蜘蛛访问网址的第一个请求有它要的数据即可, 是不是非常的清奇...
总结来说呢, 就是如果你的项目处在线上运营阶段并且开发到了一定的集成度了, 迫于 ssr 的改造成本太大, 又需要让一些数据 (比如每一篇文章帖子) 能够被收录, 就可以考虑一下我的这个方法.
但是我不保证蜘蛛的防作弊机制, 会不会过滤掉我这种跟浏览器正常访问主站差异较大的 seo 版小网站. 目前这个方案还在试验阶段.
测试
测试也很简单, 写个模拟蜘蛛请求即可, curl, 爬虫, postman 都可以模拟蜘蛛的 UA 来测试. 或者改一下搜索引擎蜘蛛的的判断条件就可以直接用浏览器访问的呢.
来源: https://juejin.im/post/5c42fdd36fb9a049b07da6dc