WordPressからHugoへ:Hugoでブログテーマを作る (4)

今回はブログのトップページに出てくる記事の一覧と、ページネーションのしかたについて。

今回は、ブログのトップページにありがちな記事の一覧を表示するテンプレートを作ります。複数の記事を表示するには、記事の情報がたくさんはいっている配列に対して range というテンプレートを使います。


  {{ range .Pages }}
  {{ end }}

ただし、これではすべての記事が対象になってしまいますので、たとえば 100 個の記事があるならば 12 個ずつ表示するといったページネーションも同時に処理しなければいけません。そのための仕組みも hugo には組み込まれています。


  <div class="blog-list">
    {{ $paginator := .Paginate .RegularPages 12 }}
    {{ range $paginator.Pages }}
    {{ .Render "summary" }}
    {{ end }}
  </div>

重要なものが2つでてきました。一つは、.Paginate を使って $paginator というオブジェクトを定義している行です。


  {{ $paginator := .Paginate .RegularPages 12 }}

この行は、.RegularPages という条件によって、セクションページなどを除いた個別記事のみを 12 記事単位でページにして、$paginator というオブジェクトに代入しています。条件はもっと複雑にすることもできて:


  {{ $paginator := .Paginate (where .Pages "Section" "books") 5 }}

これだと、現在のそれぞれの .Page (記事)において Section というメタ情報が books に該当する場合にのみ、記事を 5 つ単位でページにします。

次に重要なのが range と .Render の行です。


  {{ range $paginator.Pages }}
    {{ .Render "summary" }}
  {{ end }}

range は配列が渡されたら、その要素を一つ一つ取り出すループを行う関数です。この場合、.Paginate が記事を 12 個単位にして paginatorpaginator.Pages でアクセスしています。

次に .Render という行がありますが、これは summary.html というテンプレートを使って、html をレンダリングするという命令です。

partial を使わないのはなぜかというと、.Render はルックアップ順に基づいて summary という名前に適合するテンプレートを探しますので、たとえば違う条件では違う summary.html を使いたいときに便利になります。

たとえばトップページでは大きな画像と抜粋入りで記事が列挙されるけれども、サイドバーのなかでは小さいサムネイルとタイトルしか表示されないといったように、同じロジックで違う見せ方をするといった使い方ができます。今回の例では、いま作っているテンプレートと同じ場所に summary.html を置いておけば十分です。

では、summary.html の中身の例をみてみましょう。


  <div class="blog-post">
    {{ if .Params.featured_image }}
      <a href="{{- .RelPermalink -}}"><img src="{{- .RelPermalink -}}{{- .Params.featured_image -}}" alt=""></a>
    {{ end }}
    <div class="blog-info">
      <h1><a href="{{ .RelPermalink }}">{{ .Title }}</a></h1>
      <div class="blog-meta">
      {{ .Date.Format "2006 年 01 月 02 日"}}
      </div>

      <div class="blog-summary">
        {{ if .Summary }}
        <p>
          {{ .Summary }}
          {{ if .Truncated }}<a href="{{- .RelPermalink -}}"> (続きを読む)</a>{{end}}
        </p>
        {{ end }}
      </div>
    </div>
  </div>

前前回の内容を理解していれば、すでに出てきた話題ばかりですね。{{ .Title }} で題名を、{{ .Summary}} で抜粋を、{{- .RelPermalink -}} で記事へのパーマリンクを、 {{ .Date.Format }} で日付を、{{- .Params.featured_image -}} で frontmatter に書かれたアイキャッチ画像のファイル名を呼び出しています。

ページネーションのロジックをつくる

出来上がったページネーションは、たとえば http://example.org/blog というテンプレートで使用したならば、http://example.org/blog/page/2/ といったような URL でアクセスすることができます。

また、テンプレートのなかでは {{ $paginator.Next }}、{{ $paginator.Prev }} などといった変数を使えますので、自分で「次のページ」「前のページ」といったリンクを作ることができます。詳しくは Pagination のドキュメントを見てください。

ただ、どうせならば次のようなページのロジックを組みたくなります。

{ . }

一番最初のページにいったり、前後のページに移動する矢印、現在のページがハイライトされ、前後数ページが表示されるといった感じにです。

これはなかなかに複雑なのですが、Glenn McCombさんの How to build custom Hugo pagination という記事に詳細な解説がありますので、ほぼそのまま踏襲して自分のページネーションを作成しました。

とても長いのですが、これを pagination.html という名前で partial ディレクトリにコピーし、テンプレートからは partial で呼び出すだけでいい感じに ul, li タグに css クラスのついた html が作られるはずです。あとは CSS で見た目を良くするだけ。注意として、「«」「<」といった矢印は Font Awesome で作っていますので、これを使いたくない場合は別の文字に置き換えてください。

{{ $paginator := .Paginator }}
{{ $adjacent_links := 2 }}
{{ maxlinks:=(add(muladjacent_links 2) 1) }}
{{ lowerlimit:=(addadjacent_links 1) }}
{{ upperlimit:=(subpaginator.TotalPages $adjacent_links) }}
{{ if gt $paginator.TotalPages 1 }}
<div class="page">
<ul>
<!-- Goto First page. -->
{{ if ne $paginator.PageNumber 1 }}
<li class="pagination__item pagination__item--first">
<a class="pagination__link pagination__link--first" href="{{ $paginator.First.URL }}">
<i class="fa fa-angle-double-left"></i>
</a>
</li>
{{ end }}
<!-- Previous page. -->
{{ if $paginator.HasPrev }}
<li class="pagination__item pagination__item--previous">
<a href="{{ $paginator.Prev.URL }}" class="pagination__link pagination__link--previous">
<i class="fa fa-angle-left"></i></a>
</a>
</li>
{{ end }}
<!-- logic for calculating middle numbers -->
{{ range $paginator.Pagers }}
<!-- use Scratch to avoid scope error -->
{{ $.Scratch.Set "page_number_flag" false }}
<!-- if there is enough pages -->
{{ if gt paginator.TotalPagesmax_links }}
<!-- Lower limit pages. -->
<!-- If the user is on a page which is in the lower limit. -->
{{ if le paginator.PageNumberlower_limit }}
<!-- If the current loop page is less than max_links. -->
{{ if le .PageNumber $max_links }}
{{ $.Scratch.Set "page_number_flag" true }}
{{ end }}
<!-- Upper limit pages. -->
<!-- If the user is on a page which is in the upper limit. -->
{{ else if ge paginator.PageNumberupper_limit }}
<!-- If the current loop page is greater than total pages minus $max_links -->
{{ if gt .PageNumber (sub paginator.TotalPagesmax_links) }}
{{ $.Scratch.Set "page_number_flag" true }}
{{ end }}
<!-- Middle pages. -->
{{ else }}
{{ if and ( ge .PageNumber (sub paginator.PageNumberadjacent_links) ) ( le .PageNumber (add paginator.PageNumberadjacent_links) ) }}
{{ $.Scratch.Set "page_number_flag" true }}
{{ end }}
{{ end }}
<!-- Simple page numbers. -->
{{ else }}
{{ $.Scratch.Set "page_number_flag" true }}
{{ end }}
<!-- Output page numbers. -->
{{ if eq ($.Scratch.Get "page_number_flag") true }}
<li {{ if eq . $paginator }} class="active" {{ end }}>
<a href="{{ .URL }}">
{{ .PageNumber }}
</a>
</li>
{{ end }}
<!-- end range -->
{{ end }}
<!-- Next page. -->
{{ if $paginator.HasNext }}
<li class="pagination__item pagination__item--next">
<a href="{{ $paginator.Next.URL }}">
<i class="fa fa-angle-right"></i>
</a>
</li>
{{ end }}
<!-- Last page. -->
{{ if ne paginator.PageNumberpaginator.TotalPages }}
<li class="pagination__item pagination__item--last">
<a href="{{ $paginator.Last.URL }}">
<i class="fa fa-angle-double-right"></i>
</a>
</li>
{{ end }}
</ul>
</div>
{{ end }}
view raw pagination.html hosted with ❤ by GitHub

一応の説明をすると:

  1. 3行目 $adjacent_links という変数に、現在のページの両側に何ページ分表示させたいかを定義しています。この場合は、現在のページが 4 ならば、2-3-4-5-6 という具合に2ページ目から6ページ目までが表示されます

  2. 4-6行目: adjacentlinks21adjacent_links 足す1、上限は全ページ数からadjacentlinksadjacent_links が 2 で50ページあったときに、48 が中央になった状態でとまるようにしているわけですね

  3. 12-19行目: 最初のページに戻る部分、21-28行目は現在のページの前にページがあるならばそこに移動するリンクを作っています

  4. 32-35行目:すべてのページについてループを作って、該当する場合だけhtmlが書き出されるようにしています。35行目に Scratch という命令があるのは、ループのなかで変数のスコープが外れてしまうのを防ぐために、外側で定義しているものです

  5. 38行目: ページ数が十分にあるかどうかを判断しています。

  6. 41-48行目: 現在扱っているページが表示する下限域にあるかどうか、そして最大数に比べて少ないかをチェックしてフラグをたてています

  7. 50-57行目: 現在扱っているページが表示する上限域にあるかどうか、そして最大数 - $maxlinks にくらべて少ないかをチェックしてフラグを立てています

  8. 59-65行目: 中央のページになっているかとチェックしてフラグを立てています

  9. 67-70行目: ページ数が足りなくて、1 ページだけを表示すると言った場合のための場合分け

  10. 72-79行目:フラグに応じて li タグをクラスとともに書き出し。

  11. 85-101行目: 12-19行目と同じ要領で、右側の矢印の処理をしています

hugoにはこうしたページ番号を処理する内部のテンプレート _internal/pagination.html がありますが、この Gist のものをそれの代わりに利用することで頭のいいページネーションを作ることができます。

トップページのテンプレートをつくる

準備ができましたので、ブログのトップページ用の、ブログ記事の列挙されている部分をテンプレートに入れてみましょう。たとえばテンプレートの index.html を作成して例えば次のように入れたとします。


  {{ define "main" }}
  <section class="blog-area">
       <div class="container">
              <div class="col">
                  {{ $paginator := .Paginate .RegularPages 12 }}
                  {{ range $paginator.Pages }}
                  {{ .Render "summary" }}
                  {{ end }}

                  {{ partial "pagination.html" . }}
              </div>

              <div class="col">
                           {{ partial "sidebar.html" . }}
              </div>
        </div>
      </div>
  </section>
  {{ end }}

summary.html も上で説明したように作っておき、pagination.html を partial ディレクトリに用意しておけば、記事の数に応じてページネーションされた html が自動的に作成されているはずです。

説明は長かったと思いますが、そのほとんどは hugo がもっている柔軟性を理解するための知識で、実際に出来上がったテンプレートは驚くほど簡単になります。あとは CSS で見た目を良くすれば、ブログらしくなってくるでしょう。

今回までで、基本のテーマを作るための情報はそろいましたので、次にカテゴリやタグといった仕組みについてみてみます。

Author Image

2011年アルファブロガー・アワード受賞。ScanSnapアンバサダー。ブログLifehacking.jp管理人。著書に「ライフハック大全」「知的生活の設計」「リストの魔法」(KADOKAWA)など多数。理学博士。