【軽量&高速】Blogger用ページネーションを作成しました

2021/07/29
8
thumbnail

軽量なページネーションが欲しいなと思い、自作してみました。ページナビゲーションやページャーとも呼ばれていて紛らわしいですが、投稿のものとは別のページネーションです。こういうのですね↓

pagination1

今まではBlogger標準のページネーションをちょこっといじったものを使っていましたが、変えてみるとだいぶ印象変わりました!

作成したページネーションの特徴や導入方法について、詳しくご紹介します。

はじめに

ページネーション作成にあたり、こちらの「Step」さんの記事を参考にさせて頂きました。

Step: 【第1回】Bloggerにページナビゲーションをつけるよ!

bloggerのカスタマイズなどを情報発信しています。

ページネーションに関しては長らく面倒くさそうだなぁと思って手を付けていないかったので、こちらの記事がなかったら作ってなかったと思います。感謝です(-人-)

特徴

  • 最小限のJavaScriptで構成
  • けっこう速い
  • ラベルページ・検索ページに完全対応
  • 導入が簡単

例のごとく、最適化にこだわりました。無駄な処理が一切発生しないよう組んでおり、フィード取得時の通信量も最小限。

実際に何度もテストしましたが、速さも問題ないと思われます。リアルタイムに生成している&フィードを取得しているため一瞬表示が遅れることがありますが、許容範囲内のはずです。PageSpeed Insightsでも指摘されませんでした。

また、ページネーション(海外製)を導入している多くのブログで検索ページ等うまく機能していないようだったので、ラベルページ・検索ページでもしっかり表示されるようにしています。

注意

※アーカイブページはサポートしていません。
※フィードの取得にfetch APIを利用しているためIEでは動作しません。

ソースコード

今回作成したページネーションは「Step」さんのものをベースに、コードの修正と最適化、機能追加もろもろを経て完成しました。コードも元のものと同様コメントを残しているので、参考にしてみてください。

以下、ソースコードです。

<!-- ページネーション -->
<b:if cond='data:blog.pageType == "index"'>
  <div class='pagination'></div>
  <script type='text/javascript'>
    const blogUrl = "<data:blog.homepageUrl.canonical/>"
    const label = "<b:eval expr='data:blog.searchLabel'/>"
    const query = "<b:eval expr='data:blog.searchQuery'/>"
    const selector = '.pagination'; // ページネーションのセレクタ
    const maxResults = 10; // 1ページあたりの表示数

    //<![CDATA[
    insertLink(selector, maxResults);

    function insertLink(selector, maxResults) {
      // フィードURLを作成
      let feedUrl = blogUrl;
      if (query == '') {
        // トップ+ラベルページ
        feedUrl += 'feeds/posts/summary';
        if (label != '') {
          feedUrl += '/-/' + encodeURIComponent(label);
        }
        feedUrl += '?alt=json&max-results=1';
      } else {
        // 検索ページ
        feedUrl += 'feeds/posts/summary?alt=json&max-results=99&q=' + encodeURIComponent(query);
      }

      // フィードを取得
      fetch(feedUrl)
        .then((response) => {
          return response.json();
        })
        .then((json) => {
          // 現在のページ番号を算出
          let currentUrl = location.href;
          let currentPage = 1;
          if (currentUrl.indexOf("#Page-") > -1) {
            let no = currentUrl.substring(currentUrl.indexOf("#Page-") + 6);
            currentPage = parseInt(no);
          }

          // ページ総数を算出
          let totalResults = parseInt(json.feed.openSearch$totalResults.$t, 10);
          let totalPage = Math.floor(totalResults / maxResults);
          if ((totalResults % maxResults) > 0) {
            totalPage += 1;
          }

          // ページネーションを作成
          let pager = '';
          for (i = 1; i <= totalPage; i++) {
            if (i == currentPage) {
              // 現在の投稿
              pager += '<span class="pagination-link current-page">' + i + '</span>';
            } else if ((Math.abs(i - currentPage) < 2) || (i == 1) || (i == totalPage)) {
              // currentから2個未満のものは表示
              pager += '<a class="pagination-link" id="Page-' + i + '" href="">' + i + '</a>';
            } else if (Math.abs(i - currentPage) == 2) {
              pager += '<span class="pagination-ellipsis">…</span>';
            }
          }

          // HTMLへ書き出す
          const pagerElem = document.querySelector(selector);
          if (pagerElem != null) {
            pagerElem.innerHTML = pager;
          }

          // ページネーションにURLを設定
          for (i = 1; i <= totalPage; i++) {
            if (i == currentPage) continue;
            if (query == '') {
              // トップ+ラベルページ
              setUrl1(i, totalResults);
            } else {
              // 検索ページ
              setUrl2(i, totalResults);
            }
          }
        })
        .catch((error) => {
          console.log(error);
        });
    }

    // 指定したページのURLを設定(トップ+ラベルページ)
    function setUrl1(targetPage, totalResults) {
      // 指定したページのインデックスを算出
      let startIndex = (targetPage - 1) * maxResults;
      if (startIndex <= 0) startIndex = 1;

      // フィードURLを作成
      let feedUrl = blogUrl + 'feeds/posts/summary';
      if (label != '') {
        feedUrl += '/-/' + encodeURIComponent(label);
      }
      feedUrl += '?alt=json&orderby=published&start-index=' + startIndex + '&max-results=1';

      // フィードを取得
      fetch(feedUrl)
        .then((response) => {
          return response.json();
        })
        .then((json) => {
          // 一番新しい記事の日付を取得
          const date = encodeDate(json.feed.entry[0].published.$t);

          // ページURLを作成
          let href = blogUrl + 'search';
          if (label != '') {
            // ラベルページの場合はlabelをつける
            href += '/label/' + encodeURIComponent(label);
          }
          href += '?max-results=' + maxResults;
          if (targetPage != 1) {
            href += '&updated-max=' + date;
          }
          href += '#Page-' + targetPage;

          // hrefにURLを設定
          const liSelector = selector + ' #Page-' + targetPage;
          const liElem = document.querySelector(liSelector);
          if (liElem != null) {
            liElem.href = href;
          }
        })
        .catch((error) => {
          console.log(error)
        });
    }

    // 指定したページのURLを設定(検索ページ)
    function setUrl2(targetPage, totalResults) {
      let startIndex = (targetPage - 1) * maxResults;
      let href = blogUrl + 'search?max-results=' + maxResults + '&start=' + startIndex + '&q=' + encodeURIComponent(query) + '#Page-' + targetPage;
      const liSelector = selector + ' #Page-' + targetPage;
      const liElem = document.querySelector(liSelector);
      if (liElem != null) {
        liElem.href = href;
      }
    }

    // 日付をRFC3339タイムスタンプ形式で取得
    function encodeDate(dateStr) {
      dateStr = dateStr.substring(0, 19) + dateStr.substring(23, 29);
      return encodeURIComponent(dateStr);
    }
    //]]>
  </script>
  <style>
    .pagination {
      display: flex;
      justify-content: center;
    }
    .pagination-link, .pagination-ellipsis {
      display: flex;
      justify-content: center;
      width: 40px;
      height: 40px;
      align-items: center;
      font-size: 1em;
      border-radius: 50%;
      margin: 0 5px;
      text-decoration: none;
    }
    .pagination-link {
      color: #444 !important;
      background: #fff;
      box-shadow: 0 2px 4px rgb(0 0 0 / 20%), 0 1px 2px 0 rgb(0 0 0 / 20%);
    }
    .pagination-link:hover {
      text-decoration: none;
    }
    .pagination-link.current-page {
      color: #fff !important;
      background: dodgerblue;
    }
    .pagination-ellipsis {
      color: #999;
      pointer-events: none;
    }
  </style>
</b:if>

★圧縮版はこちら↓

コードを確認

導入方法

基本的にはソースコードをそのままコピペして頂くだけでOKですが、分かりにくいかもしれないので手順を載せておきます。説明なくても分かるよ!という方は適当に読み飛ばしてください。

バックアップ

まず、もしもの場合に備えてバックアップを取っておきます。

管理画面から「テーマ」→「バックアップ」を選択し、ダウンロードしたファイルを保存しておきましょう。

backup-img

コードの設置

どこに置けばいいかよく分からないという場合は、HTML編集ページでコードを</body>の上にコピペしてください。HTML編集に詳しい方は、Blogger標準のページネーションと同じ位置に設置するとちょうど良いと思います。

補足

Blogger標準のページネーションと二重に表示されててなんか気になるよ!という場合は、以下の方法を試してみてください。QooQ(ver1.30)を例として解説します。

まず、もともとのページネーションの表示部分を探します。Ctrl+Fで「nextprev」と検索すると見つけやすいです。

HTML編集画面

見つけたら、コードを次のように書き換えてください。

<b:include name='nextprev'/>

↓↓

<!-- <b:include name='nextprev'/> -->

<div id='list'>のほうのBloggerの標準ページネーションをコメントアウトすることで、新たに設置したページネーションのみが表示されます。元に戻したくなったらコメントアウトを外してください。

注意点

注意点として、1ページあたりの投稿表示数maxResultsは設定の「投稿数の上限」に合わせてください。数が違っていると、正しく表示されない可能性があります。

post-setting

同じく表示の観点から、ラベル表示部分のコードも以下のように変更してください。対象となるのは「ラベル」と「パンくずリスト」のリンク部分です。

expr:href='data:label.url'

↓↓

expr:href='data:label.url params {max-results: "10"}'

※ここもmax-resultsは投稿数の上限に合わせてください。

これ大事です。そのままだと一番最初に表示したとき設定よりも多く表示されてしまうことがあるので、max-resultsの部分は忘れずに付け加えるようにしてください。

解説

ソースコードについての解説です。

まず、トップ&ラベルページではナビゲーションリンクを必要個数分作るために総投稿数の情報がいります。総投稿数はopenSearch$totalResults.$tに格納されており、これだけ取得できれば良いのでmax-results=1としてフィードURLを作成しfetchします。

…が、検索ページの場合は厄介なことに、ある程度max-resultsの値が大きくないと総投稿数が取得できません。そのため今回のコードでは

// 検索ページ
feedUrl += 'feeds/posts/summary?alt=json&max-results=99&q=' + encodeURIComponent(query);

としていますが、これは妥協案です。本来は全体の総投稿数を取得してから検索結果のフィードURLを作成するべきですが、2段階でfetchすることになりタイムロスが発生するため、このような形にしています。

よほど大量に投稿していない限り検索で99件以上出てくることはないと思いますが、可能性がある方は値を99よりも大きく設定しておいたほうが良いと思います。

// ページネーションを作成
let pager = '';
for (i = 1; i <= totalPage; i++) {
  if (i == currentPage) {
    // 現在の投稿
    pager += '<span class="pagination-link current-page">' + i + '</span>';
  } else if ((Math.abs(i - currentPage) < 2) || (i == 1) || (i == totalPage)) {
    // currentから2個未満のものは表示
    pager += '<a class="pagination-link" id="Page-' + i + '" href="">' + i + '</a>';
  } else if (Math.abs(i - currentPage) == 2) {
    pager += '<span class="pagination-ellipsis">…</span>';
  }
}

ページネーションの作成は、現在のページを基準に表示を分けるようになっています。現在のページから前後2個以上のリンクについては、「...」で省略表示されます。Math.abs ...部分の条件を変えることで、3個以上や4個以上に変更することも可能です。

さて、ここまででページネーション本体ができました。ただしリンクを作っただけではhrefが空っぽなので、正しくページ移動できるようhrefにURLを設定していきます。URL設定ではsetURLという関数をトップ&ラベルページと検索ページそれぞれに対して作り、処理をします。

トップ&ラベルページ

まずトップ&ラベルページでは、updated-max用のデータを取得するためにフィードを作成してfetchします。

updated-maxというのは、「この日付以前の投稿を表示するよ」という基準となる日付です。トップ&ラベルページでは開始インデックスを指定できないので、ページごとに思い通りに表示させるためにはこのパラメータを利用する必要があります。紛らわしいですが、ここでのupdatedは更新日ではなく投稿日です。

update-dmaxの解説に関して、「Step」さんの例が分かりやすかったので拝借させて頂きます。

No | 投稿日時
--|---------------------
1 | 2021-08-01 12:00:00
2 | 2021-07-01 12:00:00
3 | 2021-06-01 12:00:00
4 | 2021-05-01 12:00:00
5 | 2021-04-01 12:00:00
6 | 2021-03-01 12:00:00
7 | 2021-02-01 12:00:00
8 | 2021-01-01 12:00:00
9 | 2020-12-01 12:00:00
10 | 2020-11-01 12:00:00

上のような10個の記事があったとします。これを1ページに5記事ずつ表示させたいとき、1ページ目に表示する記事はNo.1~5、2ページ目に表示する記事はNo.6~10です。

このときどうするかというと、1ページ目はmax-results=5を指定するのみ、2ページ目はmax-results=5に加えてupdated-max="No.5の投稿日時"を指定します。

なぜ2ページ目に指定するupdated-maxがNo.6のものではないのか?ここで注意が必要なのが、指定した投稿日時の記事は「含まれない」という点です。なので、No.6から表示させるためにはNo.5の投稿日時をupdated-maxとして指定する必要があります。

1ページ目に関してはmax-results=5を指定するだけでNo.1~No.5まで表示されるので、updated-maxを付ける必要はありません。「Step」さんのコードでは1ページ目でNo.1の投稿日時がupdated-maxに指定されるようになっており、表示にずれが生じてしまうため修正しました。

各種パラメータを追加した後は、URLリンクを各要素に設定して完了です。

検索ページ

検索ページでは、updated-maxを使う必要がないのでフィードは作成しません。検索ページの場合は非常に便利なパラメータとしてstartという開始インデックスが使用できるので、それを利用します。

start=0max-results=10なら検索結果の1~10件目、start=10max-results=20なら11~20件目…という具合ですね。後はそれにエンコードした検索キーワードを追加すれば、URLリンクのできあがりです。

さいごに

今回フル活用したフィードに関してはこちらの記事で詳しく解説しているので、興味がある方はぜひご覧ください↓

【徹底解説】Bloggerフィードの各種パラメータと使い方 | IB-Note

Bloggerのフィードに関する知識をまとめてみました。フィードの基本について、例を交えながら徹底解説していきます。

8件のコメント
リンクにパラメータを簡単に追加できます。
expr:href='data:label.url params {max-results: "10"}'
data:label.url に ?max-results=10 を追加するコードです。
>BINUBALLさん
そんな書き方があったんですね!情報ありがとうございます。
早速そちらの形式に変更しようと思います。
ページネーションがうまく表示されません。 コンソールにエラーが発生します。
SyntaxError: Unexpected token < in JSON at position 0
>BINUBALLさん
ご報告ありがとうございます。
うまく表示されていないページのURLを教えていただけますか?
<data:blog.homepageUrl/> このモバイルでは、 ?m=1 が付いたまま出力されます。 つまり、https://itblogger-note.blogspot.com/?m=1feeds/posts/summary?alt=json&max-results=99&callback=loadpagerのような状況が発生する可能性があります。
Fumaさんのブログスクリプトにはblogurlが抜けています。
>BINUBALLさん
スクリプトを限界まで減らそうとblogurlを省略していたのですが、確かにモバイルでは不具合が生じかねない危ういコードになっていました。早速修正しておこうと思います。ご指摘ありがとうございますm(_ _)m
data:blog.homepageUrl を data:blog.homepageUrl.canonical に置き換えると、 ?m=1 が付いていないままモバイルでもうまく出力されます。
>BINUBALLさん
いつも情報ありがとうございます。
早速そちらに変更しておきます。