ランサーズ等のサービスを開発・運用する中で得た知識やノウハウを紹介しています。

thumbnail

Labels:  JavaScript, SEO 投稿者:ota

SEOフレンドリーな無限スクロールの実装方法

飲み物は常温派のota@purratto)です。自動販売機で常温のものも販売してくれればいいのになあ、と常々思っています。

ランサーズストアでSEOフレンドリーな無限スクロールをjQueryで実装しました。
サンプル向けに一部修正したコードを公開します。参考になれば幸いです。
検索エンジンとの相性を考慮した無限スクロールのベストプラクティス | Googleウェブマスター向け公式ブログに準拠した実装になっています。

デモ(PCでみてください。スマホでは無限スクロールになっていません 2017/4/21現在。サンプルコード自体はPCのみならずスマホでも無限スクロールの実装として使えるコードになっています。)
コード

/**
 * SEOフレンドリーな無限スクロール
 */
(function () {
  'use strict';
  // 事前に読み込む次ページのページ数
  const LOAD_PAGES = 1.5;
  // スクロール位置がページの下限からどのぐらい上にあったときそのページを閲覧中とするのかの設定値
  const BOTTOM_POSITION_RATE = 0.25;
  // ブラウザのウィンドウのどの高さを閲覧中の中心の高さとするのかの設定値
  const FORCUS_POSITION_RATE = 0.5; 

  var seoInfiniteScroll = {
    /**
     * スクロール位置に応じて、次のページを読み込む
     */
    init: function () {
      setPosition();
      loadPage();
    }
  };

  /**
   * 2ページ目以降にアクセスしたときは前のページも読み込まれるため、アクセスしたページにスクロール位置を調整する
   */
  function setPosition() {
    var pageArray = $(location).attr('search').match(/[?&]page=\d+/);
    if (pageArray) {
      var page = pageArray[0].replace(/[?&]page=/, '');
      if (page >= 2) {
        $(window).load(function(){
          setTimeout(function() {
            if ($(".item:last")[0]) {
              $(window).scrollTop($(".item:last").offset().top);
            }
          }, 300);
        });
      }
    }
  }

  /**
   * 前後のページを読み込み表示する
   *
   * 前のページの無限スクロールの仕組み
   * loadPrevPage()で前のページを読み込み、prevDataCacheに読み込んだデータを保持
   * showPrevPage()でprevDataCacheに保持しているデータを表示
   *
   * 読み込みと表示で処理を分け、タイミングをずらしている理由は以下。
   * 上にコンテンツが追加されるとユーザーがみている位置が下にずれてしまうためのを防ぐためにjsで位置を修正しており、
   * そのときに読み込みまで行うと処理に時間がかかってしまう、jsでの位置の調整がカクカクするため。
   * 
   * 次のページの無限スクロールの仕組み
   * loadNextPage()で次のページを読み込み表示
   */
  function loadPage() {
    var isLoading = false;
    var prevDataCache = false;
    var nextPageExist = true;

    $(window).scroll(function() {
      var infiniteScrollTopPosition = $(".item:first").offset().top;
      var itemHeight = $(".item:first").outerHeight();
      var scrollTopPosition = $(window).scrollTop();
      loadPrevPage();
      showPrevPage();
      loadNextPage();
      updateBrowserHistory();

      /**
       * 前のページを読み込んでprev_data_cacheに入れる
       */
      function loadPrevPage() {
        if (!prevDataCache && !isLoading) {
          // ユーザーに読込みの待ち時間を発生させないためにLOAD_PAGESページ分前で読み込みイベントを発火する
          // 読み込みする位置までスクロールされていたらtrue、そうでなかったらfalseを返す
          var isScrollPosionToLoad = infiniteScrollTopPosition + itemHeight * LOAD_PAGES >= scrollTopPosition;
          // 前のページのURLが存在していたらtrue、そうでなかったらfalseを返す
          var isExistPrevUrl = Boolean($(".item:first").attr('data-prev-url'));
          // 読み込み位置までスクロールされており、data-prev-urlに読み込むべきURLがあるときに読み込み処理を行う
          if (isScrollPosionToLoad && isExistPrevUrl) {
            isLoading = true;
            var loadUrl = $(".item:first").attr('data-prev-url') + '&type=part';
            $.ajax({
              type:'GET',
              url:loadUrl,
              dataType:'json',
              'success': function(data) {
                prevDataCache = data.data;
                setTimeout(function() {
                  isLoading = false;
                }, 200);
              },
              'error': function(data) {}
            });
          }
        }
      }

      /**
       * loadPrevPage()によってprev_data_cacheに入れた前のページのデータを表示する
       */
      function showPrevPage() {
        if (prevDataCache && !isLoading) {
          // ユーザーに読込みの待ち時間を発生させないために1ページ分前で表示イベントを発火する
          var isScrollPosionToShow = infiniteScrollTopPosition + itemHeight >= scrollTopPosition;
          if (isScrollPosionToShow) {
            isLoading = true;
            $(".item-container").prepend(prevDataCache);
            $(window).scrollTop($(window).scrollTop() + itemHeight);
            prevDataCache = false;
            setTimeout(function() {
              isLoading = false;
            }, 200);
          }
        }
      }

      /**
       * 次のページを読み込み表示する
       */
      function loadNextPage() {
        // 読み込み中でなく、読み込む次のページが存在しているか
        if (!isLoading && nextPageExist) {
          // ユーザーに読込みの待ち時間を発生させないために1ページ分前で読込みイベントを発火する
          var scrollPositionBottom = scrollTopPosition + $(window).height();
          var isScrollPosionToLoad = $(".item:last").offset().top - itemHeight <= scrollPositionBottom;
          if (isScrollPosionToLoad) {
            isLoading = true;
            $(".loading").show();
            var loadUrl = $(".item:last").attr('data-next-url') + '&type=part';
            $.ajax({
              type:'GET',
              url:loadUrl,
              dataType:'json',
              'success': function(data) {
                if (data.data) {
                  $(".item-container").append(data.data);
                  setTimeout(function() {
                    isLoading = false;
                    $(".loading").hide();
                  }, 200);
                } else {
                  nextPageExist = false;
                  isLoading = false;
                  $(".loading").hide();
                  $(".finished").show();
                }
              },
              'error': function(data) {}
            });
          }
        }
      }
      /**
       * 検索窓に表示されるURLを現在みているページのものにする
       */
      function updateBrowserHistory() {
        $(".item").each(function(index) {
          if (mostlyVisible(this) && $(this).attr("data-url") !== $(location).attr('pathname') + $(location).attr('search')) {
            history.pushState(null, null, $(this).attr("data-url"));
          }
        });
      }

      /**
       * 現在みているページであるかを返す
       * @return boolean
       */
      function mostlyVisible(element) {
        var scrollPosition = $(window).scrollTop();
        var windowHeight = $(window).height();
        var elementTop = $(element).offset().top;
        var elementHeight = $(element).height();
        var elementBottom = elementTop + elementHeight;
        // スクロール位置がページの下限から指定した高さ分だけ上にあるかチェック && 閲覧の中心の高さがページの上限よりも下にあることをチェック
        return ((elementBottom - elementHeight * BOTTOM_POSITION_RATE > scrollPosition) && (elementTop < (scrollPosition + windowHeight * FORCUS_POSITION_RATE)));
      }
    });
  }

  window.seoInfiniteScroll = function () {
    return Object.create(seoInfiniteScroll);
  };
})();

サンプルコードの使い方

・クエリストリングにtype=partを含むURLでアクセスがあったときは読み込ませたい前後のページのみの部分をjsonで返すようにします。
・各ページごとにitemクラスを指定します。
・各ページを囲んだものにitem-containerをidに指定します。
・読み込み中に表示するものにloadingクラスを指定します。
・これ以上ページが存在しないときに表示するものにfinishedクラスを指定します。

ランサーズではサービスを成長させてくれるエンジニア、デザイナーを募集しています!
ご興味がある方は、以下URLよりご応募ください。

PHPエンジニア
Rubyエンジニア
フロントエンドエンジニア
インフラエンジニア
新卒エンジニア
その他採用情報

関連記事

thumbnail
redux-persist でブラウザストレージに一部のデータを保存しつつ、ストアに復旧するまで render を防ぐには

森です。 課題 redux-persist を使っていて、ブラウザリロード時にブラウザストレージからストアにデータを復旧する前に render されるのを防ぎたい。ストアに値があることが前提のコンポーネントでエラーになるから。公式ドキュメントとしてレシピがあっ …

thumbnail
redux アプリで定期実行処理を書くには

森です。redux アプリで redux-saga を使って定期実行処理を書いてみました。 要件 redux-saga で以下を実装する方法を検討しました。 定期的にステート(本稿では reducer で分割したストア)の値をチェックして何かする 定期的に A …

thumbnail
エンジニアでも必ずチェックしておきたい『開発に役立つSEOブログまとめ』

こんにちは! ランサーズの開発部でエンジニアをしております、神庭(かんば)です。 本日はSEOについてです! ところで、”SEOはエンジニアにはあまり関係ない技術だと思ってたりしませんか?” (SEOに興味を持つまでは、私は思ってました…) 「デー …