HugoサイトのデータをJavaScriptから利用できるように書き出す

Hugoで構築したサイトのデータをJavaScriptから利用できるように、JSON形式で書き出す。例えばサイトやページ、タグといった情報をJSON形式で読めるように書き出せたら、JavaScriptから利用できます。

こういうのはいろんな方がやっておられて、ネットの情報は参考になります。それぞれやり方書き方が少しづつ違ったりしますが、自分の目的に合った 良いとこ取りの方法を考えました。特殊な記事ファイルやレイアウトファイルを用意するのが一般的ですが、気を付けた点を書くと、

  1. 記事ファイルやセクションファイル(_index.md)などを使う場合、通常の記事と混ざってリストなどに表示されないよう工夫する。
  2. 一つの書き出しテンプレートにいろんなデータを書き出すロジックを詰め込むと、記事数が大量になった場合、書き出されるJSONデータも巨大になる。基本巨大なデータファイルではなく、個別の小さなデータファイルに分けたい。

「2」は以前 Jekyll でサイトを作っていた際、欲張って全データが出力されたJSONを作ったことがありました。記事数が少ないうちはいいのですが、記事が増えるとデータが巨大化し、ブラウザの背後で読み込むだけで時間がかかる。非同期でも負担が大きいことは変わらない。その反省から、例えば記事なら記事、タグならタグといった形で小分けしたいです。また記事データ書き出しにしても、記事の本文コンテンツは必要なければ書き出さないなどの工夫が必要です。

データ書き出しに使えそうなHugoの仕組み

データ書き出しにおいて特殊な記事ファイルを利用するなどの場合、例えば専用のセクションを作って/layouts以下に専用の(セクション名と同じ)フォルダを作ったりして、その中に配置したレイアウトファイルにJSON書き出しのロジックをテンプレートとして書いていくやり方が多いです。しかし汎用のレイアウトファイル名、例えば/layouts/セクション名/single.htmlなどを使うと、そのセクション全てのデータ書き出しファイルに使われてしまう。「2」を実現するにはこのあたりの工夫も必要でしょう。

ということで公式サイトの次のページにある、レイアウトファイルの検索パターンの規則をよく読んでみました。

gohugo.io

英語なのでわかりにくいですが、シングルページテンプレート (Single Page Templates)の名前規則には次のパターンがありますね。

/layouts/セクション名/レイアウト名.html

ここでの「セクション名」は記事ファイルの置かれたフォルダ名、「レイアウト名」は記事ファイルのフロントマターで指定したカスタムレイアウトファイル名です。これなら書き出したいデータの種類に応じて、ファイル名とレイアウト名を関連付けて命名しておくなどできて分かりやすい。なおかつファイルごとに別々のレイアウトファイルが使えますし、それらを全て同じ/layouts/セクション名/の配下にまとめられるわけです。

ではこの仕組みを利用してデータ書き出し専用のセクションから作ります。

データ書き出し専用のセクションをつくる

データ専用のセクションとして/content/jsdata/というフォルダを作ります。名前はわかりやすければ何でもいい。

このセクションの_index.mdを置きます。フロントマターにtype: "special"と書かれただけのものです。

---
type: "special"
---

このtype: "special"はサイドメニューなどで表示させないために適当に決めた値です。たとえばサイドメニューのセクション階層でこのタイプのみ排除します。

{{- /* Loop through section pages */}}
{{- define "hierarchy" }}
<ul class="section-hierarchy not-have-mark">
  {{- range .Sections.ByTitle }}
    {{ if not (or (eq .Type "special") (eq .Type "search")) }}
       ..........
    {{ end }}
  {{- end }}
</ul>
{{- end }}

詳細は省きます。{{ if not (or (eq .Type "special") (eq .Type "search")) }}.Type"special""search"なセクションのみ排除します。.Type "search"はここでの話には関係ないですが、検索画面用に設定したセクションで、メニューから外して別表示するために排除しています。

またサイドメニューから排除しても、実際はhttp://localhost:1313/jsdata/というURLでリストページにアクセスすればセクションページが表示されるので、_default/list.htmlが呼ばれないように/layouts/jsdata/list.htmlといった空のセクションレイアウトを作るなどが必要でしょう。やり方は人それぞれでしょうが、例えば/layouts/jsdata/list.htmlを次のようにして、トップページへリダイレクトさせておきました。

【 /layouts/jsdata/list.html 】

<!-- ダミー リストレイアウト -->

<script>
window.location.href = '/';
</script>

データ書き出し用のフォルダ

JSONデータの書き出し先として/static/js/data/を作っておきます。別に/static/js/に直接書き出しても問題ありませんが、セクション名をjsdataとしたのでそれに対応する名前にしただけのことです。

ためしにタグデータ専用の仕組みを作る

書き出すデータを何にするか決める必要があるので、サンプルとして記事の全タグデータを書き出してみます。

/content/jsdata/配下にダミー記事ファイルとしてtag-names.mdを作ります。実際の記事では無く、タグデータのリストアップをするテンプレートを呼び出すためのものなので、フロントマターのみのファイルです。

【 /content/jsdata/tag-names.md 】

---
url: "/js/data/tag-names.js"
layout: "tag-names"
---

url属性で書き出し先を指定することで、/js/data/の中にtag-names.jsというファイル名でタグ情報を記したJSデータを書き出します。

またlayout属性にカスタムレイアウトファイル名(拡張子は不要)を記すことで、書き出しに使うロジックが書かれたレイアウトファイルを指定します。

最初にシングルページレイアウト検索のパターンとして/layouts/セクション名/レイアウト名.htmlというのがあることを書きましたが、このパターンに倣って/layouts/jsdata/tag-names.htmlをレイアウトとして結びつけます。こういうネーミング規則を決めて、/content/jsdata/配下のダミー記事ファイル名と/layouts/jsdata/配下のレイアウトを1対1で対応させようというわけです。

これなら/content/jsdata/の中に、書き出したいデータを示すようなファイルをいくらでも作れて、それに対応するレイアウトファイルもその数だけ作れます。/layouts/セクション名/レイアウト名.htmlというレイアウトよりも優先順位が高いのは/layouts/タイプ名/レイアウト名.htmlというファイルなので、それさえつくらなければ/layouts/セクション名/レイアウト名.htmlというレイアウトが使われます。またどのみちフロントマターでカスタムタイプ名は設定していないので、/content/jsdata/tag-names.mdのデフォルトのタイプ名はセクション名jsdataと同じであり、どのみち/layouts/jsdata/tag-names.htmlが使われます。

これで/layouts/セクション名/single.html/layouts/_default/single.htmlが呼ばれる事はありません。

ただやってみるとわかるのですが、ここでフロントマターに余計なデータを書かないこともコツです。titleとか不要なので付けない。またうっかりtypeの値をjsdata以外にしてしまうと、対応するレイアウトが使われず_default/single.htmlなどが使われてしまい、HTMLの内容になってしまうので注意。

通常記事と区別する

ただしこのままだと通常記事と同じように、例えばトップページの全記事リストなどに表示されてしまいます。そうならないように、/layouts/index.htmlに、このデータ専用セクションをリストから外すよう条件分岐させておきます。

【 /layouts/index.html 】

{{ define "main" }}

..........
  <ul id="post-list" class="post-list">
    {{ range .Site.RegularPages.ByLastmod.Reverse }}
      {{ if ne .Type "jsdata" }}
      ......................................
      {{ end }}
    {{ end }}
  </ul>
..........

{{ end }}

{{ if ne .Type "jsdata" }}で、タイプjsdata(=セクションjsdataでもある)のみリストに出ないようにしています。

専用レイアウトファイルにデータ書き出しのロジックを書く

さてレイアウトファイル/layouts/jsdata/tag-names.htmlが、データ書き出しの本体です。ここでは全てのタグ名を JSON の配列に格納したJS変数を書き出したいので、次のようにします。

【 /layouts/jsdata/tag-names.html 】

{{ $data := .Site.Data.termname }}
{{- if lt 0 (len .Site.Taxonomies.tags) }}
var allTags = [
  {{- range $index, $term := .Site.Taxonomies.tags.Alphabetical }}
    {{- if index $data $term.Name -}}
      {{ $.Scratch.Set "termname" (index $data $term.Name) }}
    {{- else -}}
      {{ $.Scratch.Set "termname" $term.Name }}
    {{- end -}}
    {{ $termname := ($.Scratch.Get "termname") }}
    {{ $termname | jsonify }},
  {{- end }}
];

{{- end }}

/dataディレクトリにtermname.yamlという YAMLデータファイルがあり、こちらで頻繁に使うタグで、URLで日本語タグ名を見せないための設定などというややこしいことをしているので、やや複雑ですが、今の話題では関係ありません。

ちなみに少し説明すると料理関係の記事をダミーにサンプルサイトを今作っているので、/data/termname.yamltag001: "梅干し"などと書いてあり、記事のフロントマターでtags: ["tag001"]などとすると、記事のタグに"梅干し"が追加され、"梅干し"タグのタグ関連からリストアップするページのURLは、http://localhost:1313/tags/tag001/などとなります。このへんのやり方は特殊だし、ここでは関係ないので省きますが、上記のコードに{{ $.Scratch.Set "termname" (index $data $term.Name) }}で分岐させているのは、タグ名を検出するのにそういう処理をさせているからです。

書き出された全タグのデータファイル

これによって書き出された/js/data/tag-names.jsがこちらになります。旅行と料理のダミー記事を持つサンプルサイトのものです。 全てのタグ名を配列としてallTagsという変数に格納しています。

【 /js/data/tag-names.js 】

var allTags = [
    "梅干し",
    "test",
    "おむすび",
    "きのこ",
    "ちぎりパン",
    "ひじき",
    "イタリア",
    "オーストラリア",
    "カーニバル",
    "ココア",
    "チーズ",
    "トースター",
    "フランス",
    "レーズン",
    "ロールパン",
    "世界遺産",
    "伊達政宗",
    "北海道",
    "城",
    "夜景",
    "大聖堂",
    "宮城県",
    "宮殿",
    "寺",
    "小麦胚芽",
    "山形県",
    "岩手県",
    "教会",
    "日本三景",
    "松島離宮",
    "温泉",
    "炊き込みご飯",
    "牡蠣",
    "美術館",
    "蒸しパン",
    "豆乳",
    "貝",
    "遊覧船",
    "青森県",
];

HTML側でこのデータを読み込んで値を利用できます。

<script type="text/javascript" src="{{ .Site.BaseURL }}js/data/tag-names.js"></script>
<script>
console.log(allTags[6]); //=> イタリア
</script>

こういう形でデータを目的別、内容別で書き出しておけば、JavaScriptコードから利用できます。例えば、本文の raw データを含んだ全記事のプロパティを書きだしておいてサイトの全文検索に利用するなど、書き出すデータによっては活用範囲は広いかと思います。