单页应用已经开始慢慢取代传统网页。而我的博客LO'S BLOG(A9043-BLOG) (opens new window)使用Vue + ElementUI开发,没有使用一些通常的博客框架。即前端所有内容都在 index.html 文件中通过JS结合AJAX生成。
这种网站的优点比起传统WEB网站开发有很大优势,它有更强大的交互能力和使用体验。除此以外,还有
- 良好的前后端工作分离模式
- 减轻服务器压力(传输数据更少)
- 共用一套后端程序代码
- ……等优点
随着用户体验和开发体验的上升,单页应用也带来了不少新的困难
首屏加载问题
为了实现单页应用,需要大量的JS和CSS统一加载。将消耗大量的带宽同时使得首屏的加载耗时变得非常长。
解决:可以压缩JS、CSS文件,分离JS部分页面使用懒加载,以及使用公共CDN加载JS插件等方式将首屏加载时间压缩到可以接受的程度。(国庆第一日中国最大的公共CDN库BootCDN倒下了……)
页面路由
单页应用所有页面都在一个html里面,通常的页面路径和历史记录已经失效了,取而代之的是各种hash路径(#号后面路由),除了让url变得很难看的同时还让浏览器路径与网络资源路径失去关联,令SEO变得很困难。
解决:通过新的前端路由模式(history)结合服务器配置已经可以让路径能和传统的一样了,不过SEO问题依旧无解
SEO难度较高
单页应用的模式注定搜索引擎无法按照传统方法爬取。蜘蛛无论按照哪种方式,都会访问到这个 noscript 的空页面,无法访问到基本内容和网站深处链接。
解决以上问题的究极方法只有使用SSR(服务端渲染),让单页应用变得和传统应用相似。其他方法也有通过预渲染将部分网页提前做好。
不过对于我的博客来讲,SSR需要学习新的技术,同时重构现有代码(暂时没兴趣...),预渲染又嫌麻烦。。于是我根据部分博客的启发,通过制造一个镜像网站引导爬虫访问来进行SEO。
# 站内搜索的打造
折腾了很久百度的站内搜索API,结果测试的时候把连接换成https就访问不到了,才反应过来百度这功能不支持https(8102年了)
。。。于是转到Google,Google站内搜索可以通过其自定义搜索引擎实现,开通发现它提供了现成的代码,而且UI和自己网站的还挺符合,于是连文档我都懒得看了,开始改造我的导航栏组件Navigation.vue
。
根据Google的指引,我新建一个div作为其容器
<div class="menu-item search" id="search">
</div>
然后改造其代码,改造方法之前做QQ Map插件的时候了解到可以用以下方法,我使用了弹出框样式的代码。
mounted() {
const script = document.createElement("script");
script.type = 'text/javascript';
script.text = `(function() {
var cx = '01xxxxxxxxxxxxxxxxxxxxxxxxxby';
var gcse = document.createElement('script');
gcse.type = 'text/javascript';
gcse.async = true;
gcse.src = 'https://cse.google.com/cse.js?cx=' + cx;
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(gcse, s);
})();`;
document.getElementById('search').appendChild(document.createElement('gcse:search'));
document.getElementById('search').appendChild(script)
},
加载之后发现UI没有对好且变形,比较麻烦,改了一下CSS到满意的程度,同时挪动了原本导航栏的位置。
同时,还发现一个问题是搜索结果的遮罩层设置了position: fixed !important
但是依旧跟着鼠标滚轮滑动,网上搜索到一个可能的原因是因为父元素设置了transform
属性,往父容器找果然发现了滚筒条的元素设置了transform:translateZ(0)
的属性,这是一条性能优化语句,可以开启GPU加速。
解决方法有两种,删掉该属性,把该遮罩层移动到更上层的地方。我选择删掉该属性,检验结果在PC端性能和流畅度影响不大。
然后Google的站内搜索功能添加完毕。
# 制作镜像网站
制作镜像网站,我选择继续使用后端代码,后端使用KOA2 + KOA-Router,代码可以在 github 查看a9043-blog-back-end (opens new window)。
新增一个app2.js开辟一个新的端口监听,新增一个robot.js作为接口文件。需要做的事情有几个
- 制作假主页
- 制作假博客页
- 制作站点地图
现在开始写robot.js
# 假主页
现在只有几十条博客,所以我没打算使用分页方案,所以我一次性加载所有博客到内存中
"use strict";
const sequelize = require("../config/sequelize"); // ORM
const showdown = require("showdown"); //markdown 转义 html
const escaper = require("true-html-escape"); // html 安全字符转义
let blogs = []
然后编写主页,主页包含标题,名字,和所有博客的简要介绍以及链接
async function getIndex(ctx) {
blogs = await sequelize.models.blog.findAll();
let divs = '';
blogs
.forEach(b => {
divs += `<div>
<a href="https://lohoknang.blog/blogs/${b.id}">${b.title}</a>
<p>${`${escaper.escape(b.intro)}...`}</p>
</div>`
});
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<link rel="shortcut icon" type="image/x-icon" href="static/myLogo.png">
<title>a9043-blog</title>
</head>
<body>
<h1>LO'S BLOG</h1>
<h2>A9043-BLOG</h2>
<h3>lohoknang</h3>
${divs}
</body>
</html>
`
}
# 假博客页
async function getBlog(ctx) {
const blogId = parseInt(ctx.params.blogId);
const blog = await sequelize.models.blog.findById(blogId);
const converter = new showdown.Converter()
const content = converter.makeHtml(blog.content);
let lis = '';
blogs
.forEach(b => {
lis += `<li>
<a href="https://lohoknang.blog/blogs/${b.id}">${b.title}</a>
<p>${`${escaper.escape(b.intro)}...`}</p>
</li>`
});
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<link rel="shortcut icon" type="image/x-icon" href="static/myLogo.png">
<title>${`a9043-blog-${blog.title}`}</title>
</head>
<body>
<h1>LO'S BLOG</h1>
<div>
<h2>${blog.title}</h2>
<h3>分类 ${blog.type} |
作者 ${blog.author} |
发布于 ${new Date(blog.createdAt).toLocaleString()} |
最后修改于 ${new Date(blog.updatedAt).toLocaleString()} |
${blog.viewNum} 阅读
</h3>
<p>${content}</p>
</div>
<ul>${lis}</ul>
</body>
</html>
`;
}
# 站点地图
站点地图 (opens new window)记录着你的网站的索引链接,可以提交给搜索引擎让其爬取。站点地图的规则详见Protocol (opens new window)
第一行必须为<?xml version="1.0" encoding="UTF-8"?>
然后是根节点和命名空间<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
然后包含多个URL结点
<url>
<loc>http://www.example.com/</loc>
<lastmod>2005-01-01</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
</url>
根据格式我们生成一个站点地图的接口
async function getGSiteMap(ctx) {
blogs = await sequelize.models.blog.findAll();
let urls = ''
blogs
.forEach(b => {
urls += `<url>
<loc>https://lohoknang.blog/blogs/${b.id}</loc>
<lastmod>${new Date(b.updatedAt).toISOString()}</lastmod>
<changefreq>always</changefreq>
</url>`
});
ctx.body = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>
`
}
然后将三个函数添加到路由
module.exports = () => {
let router = require('koa-router')();
router.get('/blogs/:blogId', getBlog);
// router.get('/sitemap.xml', getSiteMap);
router.get('/gsitemap.xml', getGSiteMap);
router.get('/**', getIndex);
return router.routes();
};
最后添加一个新的端口监听,该端口即为爬虫将要访问到的端口,开始写app2.js
"use strict";
const Koa = require('koa');
const app = new Koa();
const controller = require('./controller/rotbot.js');
app.use(async (ctx, next) => {
if (ctx.request.method === "OPTIONS") {
ctx.response.status = 200;
ctx.response.set("Access-Control-Allow-Origin", ctx.req.headers.origin);
ctx.response.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
ctx.response.set("Access-Control-Allow-Credentials", "true");
ctx.response.set("Access-Control-Allow-Methods", "GET, POST, OPTION, PUT, PATCH, DELETE");
return;
}
await next();
});
app.use(async (ctx, next) => {
ctx.response.set("Access-Control-Allow-Origin", ctx.req.headers.origin);
ctx.response.set("Access-Control-Allow-Credentials", "true");
ctx.response.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
ctx.response.set("Access-Control-Allow-Methods", "GET, POST, OPTION, PUT, PATCH, DELETE");
await next();
});
app.use(controller());
app.listen(3002);
console.log('robot started at port 3002...');
将改造后的代码提交到github,远端服务器 pull 下来后 restart 一次服务,后端代码改造就完成了。然后要做Nginx的端口转发。
我们可以让Nginx通过User-Agent识别出带有某些特征的典型爬虫,在配置文件中http结点新增如下代码,正则识别爬虫
map $http_user_agent $is_bot {
default 0;
~[a-z]bot[^a-z] 1;
~[sS]pider[^a-z] 1;
'Yahoo! Slurp China' 1;
'Mediapartners-Google' 1;
}
然后在要处理转发的Server中进行如下改造,识别爬虫返回418,同事418的error_page 转到 200 @bots 的 location
location / {
# start
error_page 418 =200 @bots;
if ($is_bot) {
return 418;
}
# end
root /usr/local/xxxxxx/xxxx/dist/;
try_files $uri $uri/ /index.html;
}
}
添加 location @bots ,将转发到3002端口
location @bots {
proxy_pass http://localhost:3002;
}
改造完成,执行nginx -s reload
重新加载配置以后,带有爬虫标识的访问都将会被转发到该端口的网站中。可以使用百度或者Goolgle的网站抓取工具进行测试。
# 搜索引擎登记
完成一切工作之后,就可以开始登录百度和Google的站长网站开始登记。
# 百度
登录百度资源平台 (opens new window),然后注册自己的网站。有几步可以做。
提交链接
访问提交链接 (opens new window) 提交你的网站
提交站点地图
访问链接提交 (opens new window) 选择自动提交 -> sitemap,输入自己的sitemap地址并提交,百度会自动验证并抓取内容
自动推送
还是链接提交 (opens new window)这个网站,点击自动提交,复制其提交代码并改造,在博客页的mounted周期加入以下代码,插入位置自己决定,保证每一次点击都会被提交到百度。
const script = document.createElement('script'); script.text = `(function(){var bp = document.createElement('script');var curProtocol = window.location.protocol.split(':')[0];if (curProtocol === 'https') {bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';}else {bp.src = 'http://push.zhanzhang.baidu.com/push.js';}var s = document.getElementsByTagName("script")[0];s.parentNode.insertBefore(bp, s);})();`; document.getElementById('blog-content-div').appendChild(script);
主动推送
通过百度接口主动推送你的页面。其中2/3/4项影响因素不明,理论只要完成其一即可不会叠加影响。
熊掌号
实名认证。我还没验证,不过效果应该是最好的?
HTTPS认证
百度鼓励站长使用HTTPS,但是百度的站内搜索功能不支持HTTPS。哈哈哈哈哈
站点属性
完善一下资料
移动适配
也是完善一下资料,填写一下对应的正则URL规则
……
我的百度写完了那么多,经过了一天才收录了首页。额……有时间我可以尝试一下绑定熊掌号。
Google 相比百度强大了很多,收录速度也是异常的快。
首先登录 Search Console (opens new window) ,登记并且验证你的网站所有权,和验证域名一样可以选择上传HTML和DNS,除此还有使用Google analysis等方法验证。
站点地图
点击站点地图,输入之前的站点地图网址,Google也会自动验证并抓取其中的网址。
提交网站
进入 Google网页提交 (opens new window) 页面,提交你的网站
提交索引
返回旧版Search Console(新版的没有这个功能),进入抓取,点击Google抓取工具,输入几个你的"网页",点击抓取。抓取后可以看到爬虫爬取你的假网页的结果,然后可以点击请求提交索引。点击请求提交索引后,Google十秒钟不到收录了我的网站。。。但是要注意,这个功能不要滥用,不要申请很多个!!!同时提交多个会被禁止提交一段时间
Google AJAX 爬虫
这个是针对AJAX由Google提出的一套方案。具体内容为,Google发现URL里有#!符号,例如example.com/#!/detail/1,然后Google开始抓取网页为example.com/?escaped_fragment=/detail/1。有一点类似我们今天做法的思想,搜索引擎主动跳转页面,但是我的前端路由已经是history模式了,于是没有做以上的改动。
做完上述步骤后,文章最头添加的Google站内搜索已经生效部分了。
# 。
单页应用做SEO优化确实没有那么简单,做了那么大一通功夫可能还没有一个静态网页什么都不做的效果好。但是办法还是有很多,SEO优化有很多专门的技术。不过比起学习这些,我更愿意先学好自己要学的东西,提高文章质量。何况自己的写作博客本身也不是为了推广,而是与周边感兴趣的人一起分享所学习的事情。