非服務端渲染頁面如何作SEO

前段時間對公司的社區h5網站,進行改版(整站重寫)。老版本的網站是在一套古老的php框架下開發的,包含不少模板文件,大部分頁面都是後端模板渲染,前端開發時要與後端溝通模板邏輯的編寫,先後端耦合嚴重,很是不利於開發。爲了實現先後端分離,減輕服務端的渲染壓力,咱們決定使用目前流行Vue框架,進行前端頁面組件化開發,使用前端路由,後端只提供數據接口和必要的模板變量渲染。
但這樣一來,網站的SEO就成爲不得不考慮的重要問題之一,本文就是對咱們實際開發中SEO解決方案的一個總結,介紹爲何要作SEO,客戶端渲染應用的SEO解決方案,以及咱們採用的方案。javascript

爲何要作SEO

對於通常的功能性h5單頁應用,由於其入口或使用場景的緣由,使其對SEO並不敏感,例如微信下的滴滴打車。但對於社區類應用,經過搜索引擎搜索對應的帖子是基本的需求。所以在進行前期的技術方案調研時,咱們首先考慮的是如何作網頁的SEO。
對於服務端渲染的頁面,因爲頁面的HTML結構直接由後端吐出,自然對搜索引擎支持良好,考慮更多的是如何讓網站搜索排名更靠前。而對於頁面由前端渲染,HTML結構是js動態生成的網站,因爲搜索引擎目前並不支持js渲染內容的抓取,因此如何給搜索引擎爬蟲提供收錄的內容,成爲要考慮的首要問題。php

解決方案

客戶端渲染應用的SEO

常見的單頁應用中,頁面的切換是經過URL中的哈希(#)來實現的,hash值得變化並不會發起瀏覽器請求,經過監聽hashChnage事件,來實現前端的路由切換。對於這種應用中,搜索引擎很難抓取不一樣頁面的內容,並且頁面的渲染大多也是ajax異步獲取數據後進行渲染,更加不利於SEO。爲此,Google提供了一套針對這種類型的網站開發者的SEO解決方案。
方案規定:html

  1. 網站提交sitemap給Google;前端

  2. Google發現URL裏有#!符號,例如example.com/#!/detail/1,因而Google開始抓取example.com/?_escaped_fragment_=/detail/1;vue

_escaped_fragment_這個參數是Google指定的命名,若是開發者但願把網站內容提交給Google,就必須經過這個參數生成靜態頁面。
這種方案本質上是爲搜索引擎提供單獨頁面,以供爬蟲收錄。java

目前流行的前端路由庫,大可能是使用了HTML5 History API,經過這種方式,使得前端hash跳轉一樣可以很好的記錄歷史,兼容瀏覽器的前進後退按鈕,提供良好的用戶體驗。同時也都提供history模式,例如vue-router:ios

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

這種模式下,加上服務端的配合,可以使前端路由更加接近後端路由,提供更加友好的url,
例如: http://domain.com/user/tom 等價於 非history模式下的http://domain.com/#/user/tomajax

至於如何設置服務端,能夠參看vue router教程history-mode;vue-router

由於網頁的的地址發生了變化,瀏覽器會發起請求,但因爲服務端設置,其實訪問的仍是同一個資源。這種模式下,其實SEO就可使用咱們下面介紹的方案。vuex

首屏渲染主要內容到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淺談

相關文章
相關標籤/搜索