Hugo初心者がテーマを自作した記録 パンくずリスト

2019-07-17 |
2019-11-02

The Hugo Gopher is designed by Renée French


パンくずリスト

これまでに、ブログの基本的な機能について作成し、前回はナビゲーションメニューを作成しました。

今回は、ブログのみならずwebサイトにあると便利なパンくずリストを設置していこうと思います。

パンくずリストとは

パンくずリストは、トップページから現在見ているページまでをたどるようにして表すリストです。

私はHugoのセクション機能を使ってカテゴリーに階層をもたせて管理しています。

そのため記事はいろんなカテゴリに所属することになり、それらの親子関係を反映させたリストがあると、ユーザは記事がどういう位置にあるのかを把握しやすいです。

この記事を例に上げますと、記事タイトルの上部に、TOP/POSTS/PROGRAMMING/Hugoと書かれた横書きのメニューがありますが、それがパンくずリストです。

この記事は、すべての記事ページが属するPOSTSセクションにあって、そのなかでもPROGRAMMINGセクションに属していて、更にその下のHugoセクションに属する記事です。

いちいち文章で書くとこれくらいの長さになるものを、簡単に表現できます。

また、パンくずリストから同一カテゴリや親カテゴリへ移動してもらえる可能性もあり、設置しておくメリットは大きいです。

Hugoの機能で実装するモチベーション

この機能については、Hugoの機能を用いるべきだと思います。

記事ページを作成するたびにパンくずリストをHTMLとCSSで作成するのは手間がかかります。

そして、もしカテゴリの名前を変えたり、カテゴリの配置を変えようものなら各記事ページを修正して回ることになるので、イヤですね。

というわけで、Hugoでビルドしたときに各記事ページにパンくずリストを表示する仕組みを作っておけば、なにか変更を加えたとしても修正する範囲が限定されるため管理が楽になります。

Hugoの機能でパンくずリストを作る

Content Sections | Hugo

今回も公式にパンくずリストの作り方について載っている頁があるのでそこを参考にしながら勧めていきます。

テンプレートを用いて再帰的に処理をしているところがあり、最初は戸惑うと思いますが、展開してみるとナルホド〜となると思います。

それでは実装するコードを見ていきます。

パンくずリストを設置したいところで、次のようにコードを書いてください。

<ol  class="nav navbar-nav">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ol>
{{ define "breadcrumbnav" }}
{{ if .p1.Parent }}
{{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )  }}
{{ else if not .p1.IsHome }}
{{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )  }}
{{ end }}
<li{{ if eq .p1 .p2 }} class="active"{{ end }}>
  <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
</li>
{{ end }}

いままで見たことない部分がいくつか出てきましたが、自分なりに解説してみたいと思います。

define

まず、2行目において{{ template "breadcrumbnav" (dict "p1" . "p2" .) }}といきなりでてきますが、これはどこで定義しているんだ…

上から順に読んでいくとそういうふうに感じてしまいますが、4行目の{{ define "breadcrumbnav" }}から{{ end }}までの間で定義しています。

このdefineって一体どういう働きをするのだろう、と調べてみました。

Hugoのテンプレート構文「template」「partial」「block」「define」のわかりやすい解説 - OTTAN.XYZ

Hugo テンプレート内で define よる部分テンプレート定義を行う(関数もどき) | まくまくHugo/Goノート

こちらのサイト様を参考に、defineについて考えてみましたが、

  1. {{define "name"}}{{end}}で囲った部分は関数のようにひとつのまとまりとして扱えそう
  2. {{define "name"}}{{end}}で囲った部分は、{{block "name"}}で呼び出すことができる
  3. {{define "name"}}{{end}}で囲った部分は、{{template "name"}}で呼び出すことができる

というふうに思いました。

呼び出し方が2通りある…?

というふうにちょっと釈然としない気持ちもありますが、defineを使うことで関数として扱えるようにして、そのdefineの中でdefineで定義した関数を呼び出すことで再帰的に処理しているようです。

あとから気づきましたが、このdefineはHugoだけでなくGo言語にもこういう記法があるみたいです。
template - The Go Programming Language
つまり、blockでの呼び出し方はHugo独自のやり方で、templateでの呼び出し方はGo言語の呼び出し方のように思います。
Go言語を知らずにHugoから入ってしまったので、思わぬところでモヤモヤしてしまいました。

dict

dictはdictionary(辞書)のことです。

辞書型のデータ構造といえば、keyと対になるvalueを定義することで、keyからvalueを参照します。

サンプルの中では、(dict "p1" . "p2" .)のように使っています。

これは、”p1”のkeyに対して . がvalueとして設定され、”p2”のkeyに対して . がvalueとして設定されています。

. は、テンプレートの中で使われたときは、そのページ自身を指しますので、”p1”と”p2”にそのページ自身をvalueとして設定したことになります。

template “breadcrumbnav” (dict “p1” . “p2” .)

templateを呼び出すときに、なぜか(dict "p1" . "p2" . )もくっついてる…

というのも、この書き方はtemplateを呼び出すときに引数として(dict "p1" . "p2" .)を設定することで、呼び出したtemplateの中で”p1”と”p2”を変数として使うことができます。

今回のパンくずリストのサンプルは再帰的にテンプレートを呼び出していくため、次の処理にパラメータを渡せるような仕組みが必要になってきます。

処理の流れ

自分なりに解説してみましたが、実際の処理の流れを追ってみたいと思います。

まず、上から順に処理が始まりますので、<ol>タグが出力されます。まだ閉じタグの方は出力されていませんね。

次に、<ol>タグの中に書いてあるtemaplteが呼び出されます。

temaplteを呼び出すときには、(dict "p1" . "p2" .)を引数に指定しています。

temaplteの内容は先程触れたように、{{define}}から{{end}}までの間に書かれている処理です。

temaplteの中身を見ていきます。

{{if .p1.Parent}}で、p1の親セクションがあるかどうかを判定します。

もし親セクションがなければトップページのように思いますが、親セクションがないけどトップページでもない場合があるようで、それ以降の分岐で書かれています。

では、親セクションがある場合についてみていきます。

親セクションがあるなら、その親セクションをパンくずリストに出力して、パンくずリストですからその親セクションのさらなる親セクションがあるかどうかを判定したいです。

そこで、{{template "breadcrumbnav "p1" .p1.Parent "p2" .}}とすることで、”p1”に親セクションのページ、”p2”に現在のページをvalueとして持たせてtemplateを呼び出します。

ここで、呼び出したtemplateの処理が終わるまでは、<li>タグを出力する処理が行われませんので、一番上の親セクションについての処理が終わると<li>タグの出力が始まります。

現在のページを起点に処理を始めているのに、出力されるリストは一番上の親から始まる…、なんだかフシギでキレイな処理に思いました。

実際に展開してみる

今回のページを例に展開するのはネストしまくって見づらくなってしまうので、トップページから一つだけ下の階層のページにおける処理を例にしてタグを展開してみようと思います。

<ol  class="nav navbar-nav">
  {{ template "breadcrumbnav" (dict "p1" . "p2" .) }}
</ol>
{{ define "breadcrumbnav" }}
{{ if .p1.Parent }}
{{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )  }}
{{ else if not .p1.IsHome }}
{{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )  }}
{{ end }}
<li{{ if eq .p1 .p2 }} class="active"{{ end }}>
  <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
</li>
{{ end }}

まずは、テンプレートを置き換えていきます。

これ以降は、defineの部分は省略して書いていきます。

<ol  class="nav navbar-nav">
  {{ if .p1.Parent }}
  {{ template "breadcrumbnav" (dict "p1" .p1.Parent "p2" .p2 )  }}
  {{ else if not .p1.IsHome }}
  {{ template "breadcrumbnav" (dict "p1" .p1.Site.Home "p2" .p2 )  }}
  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
  </li>
</ol>

さて、p1はトップページから一つだけ下の階層に所属するページですので、.p1.Parenttrueです。

分岐先の処理でテンプレートを呼び出しますので、それを反映したものは次のようになります。

<ol  class="nav navbar-nav">
  # 再帰的に呼び出した部分 始
  {{ if .p1.Parent.Parent }}
  {{ template "breadcrumbnav" (dict "p1" .p1.Parent.Parent "p2" .p2 )  }}
  {{ else if not .p1.Parent.IsHome }}
  {{ template "breadcrumbnav" (dict "p1" .p1.Parent.Site.Home "p2" .p2 )  }}
  <li{{ if eq .p1.Parent .p2}} class="active"{{end}}>
    <a href="{{ .p1.Parent.Permalink }}">{{ .p1.Parent.Title }}</a>
  </li>
  # 再帰的に呼び出した部分 終

  <li{{ if eq .p1 .p2 }} class="active"{{ end }}>
    <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
  </li>
</ol>

最後に、.p1.Parentはトップページですので、.p1.Parent.Parentfalseで、.p1.Parent.isHometrueになりますので、それらを反映させていくと次のようになります。

<ol  class="nav navbar-nav">
  <li>
    <a href="{{ .p1.Parent.Permalink }}">{{ .p1.Parent.Title }}</a>
  </li>
  <li class="active">
    <a href="{{ .p1.Permalink }}">{{ .p1.Title }}</a>
  </li>
</ol>

展開していくときの視点はトップページの一つ下の階層にあるページの視点になっています(処理を始めたページの視点)。

正確には、呼び出したテンプレート内では、p1に.p1.Parentをvalueとして設定して呼び出しているので、今回展開した例は厳密さを求めているというよりは、実際やってみたらどんな感じになるのかっていうのを見てみようという試みであることをご理解いただけたらと思います。

Tips

セクション機能についてある程度知っている人であればハマらないと思いますが、自分はそこまで知識がないときにパンくずリストを作成しようとしたためセクション関係でハマったことがあります。

というのも、サンプルのコードをコピペしてもナゼかうまくうごかない。

なんかセクションの.Titleが取れていないっぽい…?

みたいな状態になったことがあります。

というのも、各セクションに_index.mdファイルというセクションページの設定を書いたファイルを用意しておかないといけなかったのに、用意するのを忘れていました。

これを用意したところ、_index.mdのfront matterに設定したtitleをセクション名として扱えたりするので、ちゃんと用意してくださいね。

セクション機能については微妙にハマりポイントがあるので、セクション機能を解説する記事を書いてみようかなとも思ってます。

おわりに

パンくずリストはwebサイトにおいて有用なものですので、ぜひHugoで作った自分のブログにも設置したいと思っていました。

各ページに自分で書いていくには手間がかかるし管理が面倒だと思っていたので、Hugoの機能を使ってパンくずリストを作成しました。

公式ドキュメントを参考にパンくずリストを作成しましたが、そのなかで再帰的な処理などいろいろ新しい要素に触れましたので、それについて自分なりに調べて解説してみました。

参考文献

Content Sections | Hugo

Hugoのテンプレート構文「template」「partial」「block」「define」のわかりやすい解説 - OTTAN.XYZ

Hugo テンプレート内で define よる部分テンプレート定義を行う(関数もどき) | まくまくHugo/Goノート

template - The Go Programming Language

Please share, if you like this.