網(wǎng)上有好幾種單頁(yè)應(yīng)用轉(zhuǎn)seo的方案英上,有服務(wù)端渲染ssr涝滴、有預(yù)渲染prerender耕蝉、google抓AJAX横堡、靜態(tài)化耕腾。卷扮。盗舰。這些方案都各有優(yōu)劣庭瑰,開(kāi)發(fā)者可以根據(jù)不同的業(yè)務(wù)場(chǎng)景和環(huán)境決定用哪一種方案枪狂。本文將介紹另一種思路比較清奇的SEO方案危喉,這個(gè)方案也是有優(yōu)有劣,就看讀者覺(jué)得適不適合了州疾。
項(xiàng)目分析
我的項(xiàng)目是用react+ts+dva技術(shù)棧搭建的單頁(yè)應(yīng)用辜限,目前在線上已經(jīng)有幾十個(gè)頁(yè)面,若干個(gè)sdk和插件在里面孝治。
- 考慮想用服務(wù)端渲染來(lái)做seo列粪,但是我的項(xiàng)目已經(jīng)開(kāi)發(fā)了這么多,打包配置谈飒、代碼分割岂座、語(yǔ)法兼容、摒棄瀏覽器對(duì)象杭措,服務(wù)端思想费什,這么多的點(diǎn)需要考慮,還不如換個(gè)框架重新開(kāi)發(fā)呢手素,所以改造成本太大??鸳址,服務(wù)端渲染不適合我這種情況。
- 預(yù)渲染雖然是開(kāi)發(fā)成本最低的泉懦,但畢竟是生成一張一張的靜態(tài)html稿黍,而我的seo需求是能夠讓蜘蛛抓取到我的社區(qū)論壇下的每一篇帖子,這樣子下來(lái)一篇帖子就是一份html,再加上分頁(yè)崩哩,那得多大的量級(jí)來(lái)存儲(chǔ)啊??巡球,而且網(wǎng)站更新就更麻煩了言沐,這個(gè)方案也不太適合。
- google.....Emmmm.........................下一個(gè)
- 靜態(tài)化也是跟預(yù)渲染差不多酣栈。险胰。。
隆重介紹
以前寫(xiě)過(guò)一種單頁(yè)應(yīng)用seo的方案矿筝,就是自己先在本地用爬蟲(chóng)做預(yù)渲染起便,生成同樣目錄結(jié)構(gòu)的靜態(tài)化的html,前端項(xiàng)目服務(wù)器判斷請(qǐng)求的UA是搜索引擎蜘蛛的話就會(huì)轉(zhuǎn)發(fā)到我事先靜態(tài)化過(guò)的html頁(yè)面
當(dāng)時(shí)的項(xiàng)目只是一個(gè)簡(jiǎn)單的只有幾個(gè)頁(yè)面的企業(yè)官網(wǎng)窖维,預(yù)渲染沒(méi)啥問(wèn)題榆综。
跟著這個(gè)思路,只要判斷搜索引擎蜘蛛讓蜘蛛看到另一個(gè)有數(shù)據(jù)的頁(yè)面不就行了铸史。
至于頁(yè)面長(zhǎng)什么樣奖年,蜘蛛??才不會(huì)管呢,就像是你找廣告商投放廣告沛贪,廣告商不會(huì)要求你要怎樣的主題什么色調(diào),只要你按照他的尺寸和要求來(lái)做震贵,然后給錢(qián)給貨就完事了??利赋。
所以可以針對(duì)SEO做另一套網(wǎng)站,沒(méi)有樣式猩系,只有符合seo規(guī)范的html標(biāo)簽和對(duì)應(yīng)的數(shù)據(jù)媚送,不需要在原有項(xiàng)目上改造,開(kāi)發(fā)成本也不會(huì)很高寇甸,體積小加載速度更快塘偎。
缺點(diǎn)也有,就是需要另外維護(hù)一套網(wǎng)站拿霉,主網(wǎng)站界面變化不會(huì)影響吟秩,如果展示數(shù)據(jù)有變化就需要同步修改seo版的網(wǎng)站。
代碼實(shí)現(xiàn)
先建個(gè)單獨(dú)的seo文件夾绽淘,不需要?jiǎng)拥皆许?xiàng)目涵防,下面是代碼結(jié)構(gòu):
代碼實(shí)現(xiàn)非常之簡(jiǎn)單,只要寫(xiě)一個(gè)中間件攔截請(qǐng)求沪铭,鑒別蜘蛛壮池,返回對(duì)應(yīng)路徑的seo頁(yè)面即可。
我的前端服務(wù)器是用express杀怠,可以寫(xiě)個(gè)express的中間件, 新建server.js:
// seo/server.js
const routes = require('./routes')
const layout_render = require('./src/layout');
module.exports = (req, res, next) => {
// 各大搜索引擎蜘蛛U(xiǎn)A
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) => {
// 返回對(duì)應(yīng)的模板結(jié)果給蜘蛛
res.set({'Content-Type': 'text/html','charset': 'utf-8mb4'}).status(200).send(layout_render(result))
})
break;
}
}
} else {
// 未匹配到蜘蛛則繼續(xù)后面的中間件
return next()
}
}
然后在前端的啟動(dòng)服務(wù)器里加入這個(gè)中間件椰憋,記得要放在其他中間件之前
// 前端啟動(dòng)服務(wù)器的server文件
var express = require('express')
var app = express()
// seo
app.use(require('seo/server'));
......
app.listen(xxxx)
接下來(lái)就是寫(xiě)模板和對(duì)應(yīng)的解析了, 新建一個(gè)home文件夾,文件夾下再建一個(gè)index.ejs和index.js
<!-- seo/src/home/index.ejs -->
<div>
<h1>官網(wǎng)首頁(yè)</h1>
<p>友情鏈接:</p>
<p><a target="_blank">百度</a></p>
<p><a target="_blank">谷歌</a></p>
</div>
index.js用于解析對(duì)應(yīng)的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');
// 這里為什么會(huì)有個(gè)async關(guān)鍵字赔退,往后面看就可以知道橙依。
module.exports = async (req) => {
const result = ejs.render(template)
return result
}
我們還可以建多個(gè)layout模板來(lái)管理head、title和導(dǎo)航欄這些公有的元素
<!-- 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="網(wǎng)站關(guān)鍵字"" name="keywords"/>
<meta content="網(wǎng)站描述" name="description"/>
<title>網(wǎng)站標(biāo)題</title>
</head>
<body>
<div id="root">
<ul>
<li><a href="/">首頁(yè)</a></li>
<li><a href="/community">社區(qū)</a></li>
</ul>
<%- children -%>
</div>
</body>
</html>
解析layout.ejs,套入內(nèi)容的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
路由表用簡(jiǎn)單的鍵值對(duì)就可以了票编,鍵名用字符串形式的正則來(lái)表示路徑的匹配規(guī)則:
// seo/routes.js
const home_route = require('./src/home/index')
module.exports = {
'^(/?)$': home_route,
}
那么數(shù)據(jù)如何做請(qǐng)求并展示到對(duì)應(yīng)的模板內(nèi)呢褪储?數(shù)據(jù)請(qǐng)求是異步的,怎樣等到請(qǐng)求完成再渲染模板呢慧域?
我們可以用async/await來(lái)實(shí)現(xiàn)鲤竹,現(xiàn)在來(lái)做一個(gè)社區(qū)的帖子列表頁(yè)面,需要先請(qǐng)求社區(qū)下帖子列表數(shù)據(jù)再把數(shù)據(jù)渲染到模板昔榴,新建一個(gè)community文件夾辛藻,同樣再建一個(gè)index.ejs作為帖子列表頁(yè)面模板:
<!-- 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>
相關(guān)的接口請(qǐng)求及數(shù)據(jù)操作寫(xiě)在同級(jí)的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
}
再加上對(duì)應(yīng)的路由配置:
// seo/routes.js
const home_route = require('./src/home/index')
const community_route = require('./src/community/index')
module.exports = {
'^(/?)$': home_route,
'^/community$': community_route,
}
這樣就實(shí)現(xiàn)了先取接口數(shù)據(jù)再做渲染,保證了蜘蛛訪問(wèn)能給到完整的數(shù)據(jù)和html結(jié)構(gòu)互订。
繼續(xù)實(shí)現(xiàn)一個(gè)帖子詳情的頁(yè)面:
<!-- 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
}
同樣加上對(duì)應(yīng)的路由配置:
// 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,
}
這樣就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的seo版網(wǎng)站吱肌,不需要任何樣式,不需要js做彈框之類(lèi)的后續(xù)交互仰禽,只要蜘蛛訪問(wèn)網(wǎng)址的第一個(gè)請(qǐng)求有它要的數(shù)據(jù)即可氮墨,是不是非常的清奇??。吐葵。规揪。
總結(jié)來(lái)說(shuō)呢,就是如果你的項(xiàng)目處在線上運(yùn)營(yíng)階段并且開(kāi)發(fā)到了一定的集成度了温峭,迫于ssr的改造成本太大猛铅,又需要讓一些數(shù)據(jù)(比如每一篇文章帖子)能夠被收錄,就可以考慮一下我的這個(gè)方法??凤藏。
但是我不保證蜘蛛的防作弊機(jī)制奸忽,會(huì)不會(huì)過(guò)濾掉我這種跟瀏覽器正常訪問(wèn)主站差異較大的seo版小網(wǎng)站??。目前這個(gè)方案還在試驗(yàn)階段揖庄。
測(cè)試
測(cè)試也很簡(jiǎn)單栗菜,寫(xiě)個(gè)模擬蜘蛛請(qǐng)求即可,curl蹄梢、爬蟲(chóng)苛萎、postman都可以模擬蜘蛛的UA來(lái)測(cè)試〖旌牛或者改一下搜索引擎蜘蛛的的判斷條件就可以直接用瀏覽器訪問(wèn)的呢腌歉。
如果有朋友用了我這個(gè)方法并且真的有用能夠被搜索引擎收錄的話,請(qǐng)記得我??齐苛,要是能打賞就更好了哈哈??翘盖。