单页应用博客的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),然后注册自己的网站。有几步可以做。

  1. 提交链接

    访问提交链接 (opens new window) 提交你的网站

  2. 提交站点地图

    访问链接提交 (opens new window) 选择自动提交 -> sitemap,输入自己的sitemap地址并提交,百度会自动验证并抓取内容

  3. 自动推送

    还是链接提交 (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);
    
  4. 主动推送

    通过百度接口主动推送你的页面。其中2/3/4项影响因素不明,理论只要完成其一即可不会叠加影响。

  5. 熊掌号

    实名认证。我还没验证,不过效果应该是最好的?

  6. HTTPS认证

    百度鼓励站长使用HTTPS,但是百度的站内搜索功能不支持HTTPS。哈哈哈哈哈

  7. 站点属性

    完善一下资料

  8. 移动适配

    也是完善一下资料,填写一下对应的正则URL规则

  9. ……

我的百度写完了那么多,经过了一天才收录了首页。额……有时间我可以尝试一下绑定熊掌号。

# Google

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优化有很多专门的技术。不过比起学习这些,我更愿意先学好自己要学的东西,提高文章质量。何况自己的写作博客本身也不是为了推广,而是与周边感兴趣的人一起分享所学习的事情。