单页应用博客的SEO(搜索引擎优化)
单页应用已经开始慢慢取代传统网页。而我的博客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优化有很多专门的技术。不过比起学习这些,我更愿意先学好自己要学的东西,提高文章质量。何况自己的写作博客本身也不是为了推广,而是与周边感兴趣的人一起分享所学习的事情。
