Hugo でサイドバー用のページツリーを表示する(現在表示しているページを考慮した階層表示)

サイドバーメニューに、サイトの階層構造に応じたリンクを表示しておくと、サイト内の様々なページに簡単に移動できるようになります。

サイドバーでのページツリー表示のイメージ

サイト全体のページ一覧をツリー構造で表示する方法は、下記のページで紹介しています。

しかし、上記のページで説明している方法でツリー表示すると、すべてのページが展開された状態で表示されてしまうため、サイドバーに表示するツリーとしてはちょっと情報量が多すぎます。 ここでは、もう少しコンパクトに表示されるように、現在のページの上位ノードだけを展開したツリーを表示するようにしてみます。

イメージとしては次のような感じで、表示中のページ(ここでは page1)の上位のセクションだけを展開したツリーを表示することを考えます。

- sec1/
- sec2/
    - sec2-1/
    - sec2-2/
        - page1 (表示中のページ)
        - page2
        - sec2-2-1/
        - sec2-2-2/
    - sec2-3/
- sec3/
- sec4/

このようなツリーをサイドバーなどに表示しておくことで、ユーザが今、サイト全体でどの位置の記事を読んでいるかを簡単に把握できるようになります(パンくずリストなども同様の効果がありますが、ツリー表示の方が、より全体を把握しやすいといえます)。

展開すべきノードを知る

自分自身のページが所属するセクションだけを展開したツリーを表示するには、テンプレートコード内で、セクションの親子関係を(先祖まで含めて)把握する必要があります。 そのために、Page オブジェクトの以下のようなメソッドを利用することができます(参考: Hugo - Section Variables and Methods)。 Hugo 本家のマニュアルページでは、.InSection.IsAncestor.IsDescendant メソッドは、Section 変数 の Methods として記述されていますが、通常ページを含む Page オブジェクトのメソッドとして参照することができます。

$p1.InSection $p2
$p1$p2 が同一のセクションに所属していれば true$p1 = $p2 の場合も true)。 それ以外は false
(Whether the given page is in the current section.)
$p1.IsAncestor $p2
$p1$p2 の先祖ページかどうかを調べる。 $p1 セクションが $p2 のカレントセクションよりも上位のセクションであれば true$p1 が通常ページの場合は常に false$p1 = $p2 の場合も false
(Whether the current page is an ancestor of the given page.)
$p1.IsDescendant $p2
$p1$p2 の子孫ページかどうかを調べる。$p2.IsAncestor $1 とするのと同じ。
(Whether the current page is a descendant of the given page.)
☝️ ワンポイント

なかなか分かりにくいですね。このような場合は実際にテストしてみるのが一番です。 これらのメソッドがどういう振る舞いをするのかを明確にするため、次のようなディレクトリ構成(セクション構成)のダミーサイトでテストしてみます。

/_index.md
/page.md
    /sec1/_index.md
    /sec1/page.md
        /sec1/sec1-1/_index.md
        /sec1/sec1-1/page1.md
        /sec1/sec1-1/page2.md
            /sec1/sec1-1/sec1-1-1/_index.md
            /sec1/sec1-1/sec1-1-1/page.md
        /sec1/sec1-2/_index.md
        /sec1/sec1-2/page1.md
        /sec1/sec1-2/page2.md

次の表は、各ページの Page オブジェクト ($p1) の .InSection.IsAncestor.IsDescendant メソッドに、別ページの Page オブジェクト ($p2) を渡したときにどう判定されるかの一覧です(Hugo 0.110.0 で確認)。

→ 別ウィンドウで開く

$p1 がセクションの場合と通常ページの場合で振る舞いが変わったりするので若干ややこしいですが、これらのメソッドは、セクションページのみで使用する ようにすれば比較的わかりやすいコードを記述できると思います。 特に、サイドバーなどに表示するページツリーを生成するときは、.IsAncestor が true になるセクションだけを、さらに深く辿っていく ようにすれば、カレントページの上位セクションのみを開いたページツリーを生成することができます。

サイドバー用のページツリーを作成する

下記のパーシャルテンプレートは、現在表示中のページよりも上位のセクションを展開して、ページツリーを表示します。

/layouts/partials/nav-tree.html
<h3>メニュー</h3>
{{- template "nav-tree-internal" (dict "section" .Site.Home "current" .) }}

{{- define "nav-tree-internal" }}
  {{- $section := .section }}{{/* 今回処理するセクション */}}
  {{- $current := .current }}{{/* 現在表示中のページ */}}

  <ul>
    {{- /* セクション直下のセクションページをループ表示 */}}
    {{- range $section.Sections }}
      <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}{{ if eq . $current }}{{ end }}</a>
      {{- if or (.IsAncestor $current) (eq . $current) }}
        {{- /* 開いているページよりも上位のセクション(あるいは自分自身)であればさらに辿る */}}
        {{- template "nav-tree-internal" (dict "section" . "current" $current) }}
      {{- end }}
    {{- end }}

    {{- /* セクション直下の通常ページをループ表示 */}}
    {{- range $section.RegularPages }}
      <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}{{ if eq . $current }}{{ end }}</a>
    {{- end }}
  </ul>
{{- end }}

例えば、サイドバー用のパーシャルテンプレートなどから次のような感じで使用します。

/layouts/partials/side-bar.html
{{ partial "nav-tree" . }}

すると、次のような感じのツリーメニューが表示されます。 現在表示中のページには ★ マークが表示され、それよりも上位のセクションだけが展開されて表示されます。

メニュー

- sec1/
    - sec1-1/
        - sec1-1-1/
        - page1.html★
        - page2.html
    - sec1-2/
    - page.html
- page.html

ちなみに、ここでは最上位のホームページへのリンクは表示しないようにしています(無駄に階層が 1 つ深くなってしまうため)。 トップページのリンクを表示したいときは、下記のように個別に表示すればよいでしょう。

<a href="{{ "/" | relURL }}">Home</a>