HugoにおけるData Templatesを利用したページの生成とPagination

# はじめに

HugoといえばPaginationに関する情報は多く、この点を魅力的に感じている人も多いのだと思います。

ただ、この機能はcontent/以下にあるファイル群が対象で、CSVなど1ファイル中に含まれる多くの情報を見栄えよく表示したい場合、(1ページに表示しないデータも含むためダウンロードにかかる時間は増えるものの)jQueryのようなJavaScriptを利用する必要があります。

この他にAjaxを使って動的にページ送りをするような仕組みも考えられますが、データを分割して提供するための仕組みが必要なので、動的なWeb APIを利用するようなニーズについては考慮しません。

今回は静的にコンテンツを生成する際に外部のRSSフィードを参照するケースで、そのRSSフィードの数に応じたPaginationを行う方法についてまとめています。

# Data Templates | Hugo

[Data Templates](https://gohugo.io/templates/data-templates/)を利用する目的は、content/配下のAsciidoc/Markdown形式などの記事・画像、 static/配下の静的ファイルの他に、外部データ(ベース or ファイル)からコンテンツを生成したい要求があるからです。

Hugoの外部データ連携機能は、TOML, YAML、外部からCSV, JSONの取り組みに対応しているぐらいで、あまり複雑なことはできませんが、最低限の機能と早いコンテンツ生成能力に期待して試しています。

HugoのDiscourse上では、XMLファイルを取り扱えないようにできないか、といった提案もありますが、現状では複雑な構造への対応は難しいと思われます。

# list.html, singile.html の活用

現在は、data/ディレクトリ以下に、menu/ サブディレクトリを作成し、ナビゲーション用の情報を配置しています。
またブログの内容については、content/ 以下に *.html ファイルを出力するような方法を試みていましたが、現在は、data/ 以下に1記事1ファイルのYAML形式のファイルを準備しようとしています。

## layouts/_default/ ディレクトリのファイル達

baseof.htmlの基本的な構造は、だいたい次のようなものになると思います。

```html
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}">
<head>
   {{ partial "head.html" . }}
</head>
<body>
  {{ partial "header.html" . }}

  {{ block "main" . }}{{ end }}

  {{ partial "footer.html" . }}
</body>
</html>
```

各.htmlファイルは layouts/partials/ の中に準備しますが、その他の```main```blockの定義をlist.htmlやsingle.htmlに準備していきます。

list.htmlは _index.ja.md ファイルなどに適応されて、そのセクション(ディレクトリ)で紹介する項目を列挙します。

私が初期に作成していたlist.htmlは```{{ define "main" }}{{ .Content }}{{ end }}```のようなもので、_index.ja.mdファイルなどでは、紹介する項目をulタグなどで列挙していました。

現在のlist.htmlは次のようになっていて、_index.ja.html などに書かれている内容を参照すらしていません。
むしろ、```{{ range .Sections }}```の中で、配下のページのプロパティや概要などの情報を利用しています。

```html:list.html
{{ define "main" }}
<div class="row">
  <ul class="list-unstyled">
    {{ range .Sections }}
    <li>{{ .Title }}</li>
    {{ end }}
  </ul>
</div>
{{ end }}
```

## single.htmlの役割

_index.??.md 以外の個別の記事を表示するために自動的にHugoによって適応されます。
いまのところは```{{ define "main" }}{{ .Content }}{{ end }}```のような内容を利用していて、個別に対応が必要な場合でもセクション毎に(e.g.```layouts/profile/single.html```）ファイルを準備しています。

# RSSフィードを表示する

HugoではXMLをデータとして取り込む手段がないので、URLを引数に取る簡単なフィルターを作成して、RSS2.0のXML形式を自前JSON形式に変換することで、データとして取り込んでいます。

```
#!/usr/bin/python3
import sys
import urllib.request
source_url = sys.argv[1]
req = urllib.request.Request(source_url)

from copy import copy
ret = []
with urllib.request.urlopen(req) as response:
    import xml.etree.ElementTree as ET

    root = ET.fromstring(response.read())
    for child in root[0]:
        ret_item = {}
        if child.tag == "item":
            for i in child:
                ret_item[i.tag] = i.text
            pass
        if len(ret_item) > 0: ret.append(copy(ret_item))
        pass
    pass
import json
print(json.dumps({"url": source_url,"items": ret}))
```

Makefileのタスクで、このスクリプトを起動し、出力を```data/rss/xxxxx.json```のような場所に配置をして、このファイル名をcontent/以下にあるmarkdownファイル等のfront matterで指定しています。

```html:content/newsfeed/news.ja.md
---
title: "..."
...
rss_data: "xxxxx.json"
```

この名前からDate Templatesの仕組みを使って、JSON形式の内容を表示しています。
_index.ja.mdファイルに対応するlist.htmlでは最初の4件だけを表示するようにしています。

```html:layouts/newsfeed/list.html
{{ define "main" }}
<div class="row">
  <div class="col-sm-12">{{ .Content }}</div>
  {{ range .Pages }}
  <div class="col-sm-6">
    <h4>{{ i18n "topic" . }} <a href="{{ .URL }}">{{ .Title }}</a></h4>
    {{ if isset .Params "rss_data" }}
      {{ $data := $.Site.Data.rss }} 
      {{ $feed := index $data .Params.rss_data }}
      {{ $items := index $feed "items" }}
      <ul class="row">
      {{ $.Scratch.Set "counter" 0 }}
      {{ range $item := $items }}
        {{ if lt ($.Scratch.Get "counter") 4 }}<li><a href="{{ $item.url }}">{{ $item.title }}</a><br/>({{ $item.pubDate }})</li>{{ end }}
      {{ $.Scratch.Add "counter" 1 }}
      {{ end }}
      </ul>
    {{ end }}
  </div>
  {{ end }}
</div>
{{ end }}
```

# 多数のデータを含む場合のPagination

サーバーから必要な情報を表示するためには、サーバー側が動的に(ページ、1ページ当り表示数などの引数を受取り)必要な分のデータを返すか、静的に複数のファイルに分割しておくなどの対応をとる必要があります。

単一ファイルに多数のデータを含む場合には、JavaScriptで表示されるデータ数を変更する必要があります。
JavaScriptのライブラリとして代表的なjQueryを利用してPaginationを実現している情報は、いろいろと見つかると思います。

* [長いページをサクッと分割！メモると便利なjQueryページネーションプラグイン](https://www.webprofessional.jp/jquery-pagination-plugins/)

jQueryを使う場合には、tableタグで構造化されているものと、適当なid,classでグルーピングされているものを対象にしたものとに分かれています。

[twbs-pagination](https://github.com/josecebe/twbs-pagination)を使う場合には、次のようなタグと、適当な場所にJavaScriptを埋め込んで次のようになります。

## 前提

jQueryとBootstrapのライブラリは layouts/partials/header.html で指定しているので省略しています。

## コード

記事本文を表示する場所をbootstrapを利用して指定しています。

```html
<script src="{{ "/js/jquery.twbsPagination.js" | relURL }}" type="text/javascript"></script>
<div class="row">
  <div class="col-sm-3" id="page-content-0"></div>
  <div class="col-sm-3" id="page-content-1"></div>
  <div class="col-sm-3" id="page-content-2"></div>
  <div class="col-sm-3" id="page-content-3"></div>
  <ul id="pagination-demo" class="col-sm-12 center"></ul>
</div>
```

コンテンツを表示するpaneを自動的に生成する方法もあると思いますが、常に見栄えに応じて変化する要素が多いので手を抜きました。

```javascript:layouts/newsfeed/single.htmlからの抜粋
<script type="text/javascript">
  $(function(){
    var numofarticle = 4;
    $('#pagination-demo').twbsPagination({
      totalPages: ({{ len $items }} / numofarticle),
      visiblePages: 5,
      onPageClick: function (event, page) {
        var index = (page - 1) * numofarticle;
        for(var i=0; i < numofarticle; i++) {
          var article = $('div').find("#feed-" + (index + i)).html();
          if (article) { $('div').find('#page-content-' + i).html(article); } else { $('div').find('#page-content-' + i).html(""); }
        }
      }
    });
});</script>
```
jquery.twbsPagination.jsファイルは static/js/ の中に入れています。

また、記事本文は後段のrangeの中で、style="display:none;"で表示を消して(念のためclass="invisible"も指定して)表示されないものの全データを1ページに含めています。

```html:layouts/newsfeed/single.html
{{ define "main" }}
{{ .Content }}
{{ if isset .Params "rss_data" }}
  {{ $data := $.Site.Data.rss }}
  {{ $feed := index $data .Params.rss_data }}
  {{ $items := index $feed "items" }}

  <div class="row">
    <script src="{{ "/js/jquery.twbsPagination.js" | relURL }}" type="text/javascript"></script>
    <div class="col-sm-3" id="page-content-0"> </div>
    <div class="col-sm-3" id="page-content-1"> </div>
    <div class="col-sm-3" id="page-content-2"> </div>
    <div class="col-sm-3" id="page-content-3"> </div>
    <div class="col-sm-12">
      <script type="text/javascript">
	$(function(){
        // 省略
      });</script>
      <ul id="pagination-demo" class="mx-auto"></ul>
    </div><!-- end of col-sm-12 -->
  </div><!-- end of .row -->
  {{ $.Scratch.Set "counter" 0 }}
  {{ range $items }}
    <div id="feed-{{ ($.Scratch.Get "counter") }}" class="invisible" style="display:none;">
      <p>{{ .description | safeHTML }}
        <span>{{ .pubDate }}</span>
      </p>
    </div>
    {{ $.Scratch.Add "counter" 1 }}
  {{ end }}
{{ end }}
{{ end }}
```

HTMLにJavaScriptが埋め込まれているので綺麗ではないですが、動きを掴むには良いと思います。

# まとめ

最後の部分でさらっと書いていますが、JavaScriptでdisplay:none;を制御しているだけなので、処理自体はRSSフィードを全件処理しています。その点ではpaginationの利点の内、必要な量のデータをユーザーに提供することはできますが、必要なデータだけをサーバーから入手するわけではない点に留意する必要があります。

扱うデータがRSSフィードであれば問題なくても、規模が大きくなると問題になることは認識しなければいけません。残念ながらHugoの枠組みでは良い方法はないので、開始位置、取得数が指定できるWeb APIを通してデータを取得するなどの処理方法自体の工夫が必要です。

Apache Solrを利用した検索ページでは、そのような処理が簡単にできますので、規模が大きくなった場合には、REST APIを提供すているなどの適切なデータベースを検討するべきでしょう。