非服务端渲染页面如何做SEO

布利丹牵驴子

前段时间对公司的社区h5网站,进行改版(整站重写)。老版本的网站是在一套古老的php框架下开发的,包含很多模板文件,大部分页面都是后端模板渲染,前端开发时要与后端沟通模板逻辑的编写,前后端耦合严重,非常不利于开发。为了实现前后端分离,减轻服务端的渲染压力,我们决定使用目前流行Vue框架,进行前端页面组件化开发,使用前端路由,后端只提供数据接口和必要的模板变量渲染。
但这样一来,网站的SEO就成为不得不考虑的重要问题之一,本文就是对我们实际开发中SEO解决方案的一个总结,介绍为什么要做SEO,客户端渲染应用的SEO解决方案,以及我们采用的方案。

为什么要做SEO

对于一般的功能性h5单页应用,因为其入口或使用场景的原因,使其对SEO并不敏感,例如微信下的滴滴打车。但对于社区类应用,通过搜索引擎搜索对应的帖子是基本的需求。因此在进行前期的技术方案调研时,我们首先考虑的是如何做网页的SEO。
对于服务端渲染的页面,由于页面的HTML结构直接由后端吐出,天然对搜索引擎支持良好,考虑更多的是如何让网站搜索排名更靠前。而对于页面由前端渲染,HTML结构是js动态生成的网站,由于搜索引擎目前并不支持js渲染内容的抓取,所以如何给搜索引擎爬虫提供收录的内容,成为要考虑的首要问题。

解决方案

客户端渲染应用的SEO

常见的单页应用中,页面的切换是通过URL中的哈希(#)来实现的,hash值得变化并不会发起浏览器请求,通过监听hashChnage事件,来实现前端的路由切换。对于这种应用中,搜索引擎很难抓取不同页面的内容,而且页面的渲染大多也是ajax异步获取数据后进行渲染,更加不利于SEO。为此,Google提供了一套针对这种类型的网站开发者的SEO解决方案。
方案规定:

  1. 网站提交sitemap给Google;

  2. Google发现URL里有#!符号,例如example.com/#!/detail/1,于是Google开始抓取example.com/?_escaped_fragment_=/detail/1;

_escaped_fragment_这个参数是Google指定的命名,如果开发者希望把网站内容提交给Google,就必须通过这个参数生成静态页面。
这种方案本质上是为搜索引擎提供单独页面,以供爬虫收录。

目前流行的前端路由库,大多是使用了HTML5 History API,通过这种方式,使得前端hash跳转同样能够很好的记录历史,兼容浏览器的前进后退按钮,提供良好的用户体验。同时也都提供history模式,例如vue-router:

const router = new VueRouter({
  mode: 'history',
  routes: routes
});

这种模式下,加上服务端的配合,能够使前端路由更加接近后端路由,提供更加友好的url,
例如: http://domain.com/user/tom 等价于 非history模式下的http://domain.com/#/user/tom

至于如何设置服务端,可以参看vue router教程history-mode;

因为网页的的地址发生了变化,浏览器会发起请求,但由于服务端设置,其实访问的还是同一个资源。这种模式下,其实SEO就可以使用我们下面介绍的方案。

首屏渲染主要内容到noscript标签

这个方案是阮一峰的一篇文章如何让搜索引擎抓取AJAX内容?里提到的,也是我们最终采用的方案。
这个方案的主要思想是:

  1. 利用History api 实现前端路由跳转

  2. 通过服务端配置,支持不带#号的URL(这个可酌情考虑,是否有必要)

  3. 通过服务端将页面主要内容渲染近<noscript>标签,供搜索爬虫抓取

这种模式下,不仅使页面更好的被搜索引擎收录,同时使网站在禁用javascript的时候,也能够浏览基本的帖子内容。

项目实际操作

我们使用了第二种方案,来做网站的SEO。
后端提供了一套机制来将页面的主要内容渲染进模板,供搜索引擎收录。首次渲染之后,如果是用户正常访问页面,后续的翻页其实是ajax请求接口,获取数据后渲染进页面。如果是爬虫或者禁用js的情况下,页面通过noscript提供收录内容和渲染页面。
先来看我们列表页的结构:

<body>
    <div id="app"></div>

    <noscript>
      <!--板块列表-->
      <div class="item">
      <?php if (isset($data_seo['forums'])): ?>
      <?php foreach ($data_seo['forums'] as $key => $value): ?>
        <div title="<?=$value['group']?>" class="item">
          <h1 title="<?=$value['group']?>"><?=$value['group']?></h1>
          <div>
          <?php foreach ($value['list'] as $_k => $_v): ?>
            <a title="魅族社区板块<?=$_v['name']?>" href="<?=$_v['url']?>"><?=$_v['name']?></a>
          <?php endforeach ?>
          </div>
        </div>
      <?php endforeach ?>        
      <?php endif ?>
      </div>
      <!--热门推荐列表-->
      <?php if (isset($data_seo['list'])): ?>
      <div>
      <?php foreach ((array)$data_seo['list'] as $key => $value): ?>
        <a href="<?=$value['url']?>" title="<?=$value['subject']?>" target="_blank" class="item">
          <h1><?=$value['subject']?></h1>
          <div class="info">
            <div class="author">
              <span title="作者"><?=$value['author']?></span>
              <img src="<?=$value['avatar']?>" title="<?=$value['author']?>的头像" alt="<?=$value['author']?>" />
            </div>
            <div class="view">
              <span title="回复数"><?=$value['replies']?></span>
              <span title="浏览数"><?=$value['views']?></span>
            </div>
          </div>
          <!--图片搜索-->
          <div class="image">
            <img src="<?=$value['pic']?>" title="<?=$value['subject']?>" alt="<?=$value['subject']?>" />
          </div>
        </a>
      <?php endforeach ?>
      </div>        
      <?php endif ?>
      <?=isset($data_pager) ? $data_pager : ''?>
    </noscript>
    <!-- built files will be auto injected -->
  </body>

在禁用js(爬虫访问时),得到的dom结构如下图
图片描述

这样浏览器即使禁用了js,依然能够显示出网站的关键内容,而页面上的网址也是爬虫继续收录的入口。

优化

其实,上面的方案在首屏渲染的时候,已经包含了页面所需的数据,而这些数据是可以被js渲染页面时所利用的,将首屏数据渲染进js变量,就可以减少首屏渲染的http请求。

例如,我们将首屏的列表数据,渲染进全局变量,对应的地址: https://domain/forum-22-1.htm...

<script type="text/javascript">
  var data_index_list = <?=isset($data_index_list) ? $data_index_list : 0?>;
  var data_current_page = <?=isset($data_current_page) ? $data_current_page : 0?>;
</script>

然后在vuex获取列表数据时,我们就可以判断,如果当前页面前端路由的页面和后端的当前页面是同一个,就直接在data_thread_list 取数据:

[actions.FETCH_FORUM_LIST]({commit, state}, params) {

      commit(actions.FETCH_FORUM_LIST_PENDING);

      if (window.data_current_page === params.page) { // 如果当前前端路由的页面和后端的当前页面是同一个,就直接在data_thread_list 取数据
        let forumlistData = window.data_thread_list.data;
        commit(actions.FETCH_FORUM_LIST_SUCCESS, forumlistData);
        return;
      }
      
      axios.get() // ajax请求获取页面数据。
}

这样一来,当页面首次渲染时,我们就不需要发起任何ajax请求:
图片描述

参考文档

如何让搜索引擎抓取AJAX内容
url的 #号
单页应用SEO浅谈

阅读 6.4k

前沿开发团队
Make the world be a better place by coding!

if you never try, you'll never know.

670 声望
19 粉丝
0 条评论
你知道吗?

if you never try, you'll never know.

670 声望
19 粉丝
宣传栏