網上有好幾種單頁應用轉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來測試。或者改一下搜尋引擎蜘蛛的的判斷條件就可以直接用瀏覽器訪問的呢。
線上demo
如果還不太明白的話直接看demo:seo-mask.lipten.link/