网上有好几种单页应用转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
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和导航栏这些公有的元素
网站标题
解析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作为帖子列表页面模板:
帖子列表
<% forum_list.map((item) => { %>
- <%= item.title-%>
<% })%>
相关的接口请求及数据操作写在同级的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结构。
继续实现一个帖子详情的页面:
const community_route = require('./src/community/index')
<%= forum_data.title%>
<%= forum_data.content%>
作者:<%= forum_data.user.nickname%>
// 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来测试。或者改一下搜索引擎蜘蛛的的判断条件就可以直接用浏览器访问的呢。
如果有朋友用了我这个方法并且真的有用能够被搜索引擎收录的话,请记得我,要是能打赏就更好了哈哈。