なうびるどいんぐ

脳みそ常時-3dB

【Hugo】Table of Contentsをjsなしでコンテンツの中に移動する

      HimaJyun

ブログの更新サボってるので頑張っていきます。タイトルはSEO的理由で一つに絞っただけで、他にもいろいろ紹介。

HugoのTocは少し機能が貧弱気味なので、それを自力で解決する方法。JavaScriptはナシで。

スポンサーリンク

HugoのToCが微妙

前置きが長いのは好まれないみたいなので単刀直入に説明すると、恐らくこのページを閲覧している人は「HugoのToC微妙すぎね?」という問題に直面しているはず。

ToCとContentsの出力が別なのでコンテンツ内にToCとか出来ない……記事内のヘッダーが歯抜けの時にul-liが多い……カスタマイズが効かない……などなど。

という訳でそれらを一挙に解決。

コンテンツの中に入れる

同じ事を考える人は少なくないっぽいのですが、ちょっとググった感じ「JavaScriptで」という記事が多いみたい。

せっかくHugoで静的に作ってるのに、それを動かすためにJavaScriptを使うのって……という気がしてならないのでJSなしで頑張ろう。

まず最初に、アプローチを説明。例えば多くのサイトではタイトルにh1を使用し、前置きとかがあって、h2で記事が始まる。という構成が多いと思う。

「コンテンツの中に入れたい」といったとき、多くの場合は「最初のh[1-6]の前に入れたい」という意味のはずだ。そうじゃないなら知らん。

じゃあそのh[1-6]を探してその前に入れればいいじゃん?と、以下コード。

{{- /* 一応「tocMove」で動作を切り替えられるようにしておく */ -}}
{{- if (.Param "tocMove" | default true) -}}
    {{- /* 正規表現でh[1-6]を探す */ -}}
    {{- $header := (findRE "<h[1-6].*?>(?:.|\n)*?</h[1-6]>" .Content) -}}
    {{- /* 最初に出現するh[1-6]を取得 */ -}}
    {{- $firstH := index $header 0 -}}

    {{- if ne $firstH nil -}}
        {{- /* ヘッダーの前にToCを結合した「新しいヘッダー」を作成 */ -}}
        {{- $newH := printf `%s%s` .TableOfContents $firstH -}}
        {{- /* 古いヘッダーを新しいヘッダーに置換して出力 */ -}}
        {{- replace .Content $firstH $newH | safeHTML -}}
    {{- else -}}
        {{- /* そもそもヘッダーがない時は普通に出力 */ -}}
        {{- .Content -}}
    {{- end -}}
{{- else -}}
    {{- /* tocMove = falseの時 */ -}}
    {{- .TableOfContents -}}
    {{- .Content -}}
{{- end -}}

やってる事がどういう事かはコメントを参考に理解して。

要するに正規表現を使ってh[1-6]を探し出し、それをreplaceで直接書き換えているだけだ。

<h2>見出し</h2>のようなヘッダーを探しだし、それを(toc)<h2>見出し</h2>に置き換えている。こうする事で最初のh[1-6]の前にToCが入る。

ToCを無効化

ページによってはToCを無効化したい事もあるはず。

これは.Paramを使えば簡単に実装できる。

{{- /* tocパラメータを確認 */ -}}
{{- if (.Param "toc" | default true) -}}
    {{- /* ここはさっきと同じ */ -}}
    {{- if (.Param "tocMove" | default true) -}}
        {{- $header := (findRE "<h[1-6].*?>(?:.|\n)*?</h[1-6]>" .Content) -}}
        {{- $firstH := index $header 0 -}}

        {{- if ne $firstH nil -}}
            {{- $newH := printf `%s%s` .TableOfContents $firstH -}}
            {{- replace .Content $firstH $newH | safeHTML -}}
        {{- else -}}
            {{- .Content -}}
        {{- end -}}
    {{- else -}}
        {{- .TableOfContents -}}
        {{- .Content -}}
    {{- end -}}
{{- else -}}
    {{- /* ToC使わないとき */ -}}
    {{- .Content -}}
{{- end -}}

Hugoで複雑な事をしようとするとネストが深くなりがちなのが……

h[1-6]が少ない記事では無効化

コンテンツ量が極端に少ないページではToCを無効にしたいと考えることもあるだろう。

ここまでの時点で既にToCの無効化機能が実装できており、正規表現を利用してh[1-6]がすべて取得できている。

後はh[1-6]を数えて、少ない時は無効化フラグを立てる事でh[1-6]がn個以下のようなコンテンツ量が極端に少ないページでToCを無効化できる。

{{- /* 変数化する */ -}}
{{- $useToc := (.Param "toc" | default true) -}}

{{- /* h[1-6]を探して数を数える */ -}}
{{- $header := slice -}}
{{- if $useToc -}}
    {{- $header = (findRE "<h[1-6].*?>(?:.|\n)*?</h[1-6]>" .Content) -}}
    {{- /* h[1-6]の数がtocMinimumよりも少ない時はToCを無効化 */ -}}
    {{- if le (len $header) (.Param "tocMinimum" | default 2) -}}
        {{- $useToc = false -}}
    {{- end -}}
{{- end -}}

{{- /* ここはさっきと同じ */ -}}
{{- if $useToc -}}
    {{- if (.Param "tocMove" | default true) -}}
        {{- $firstH := index $header 0 -}}

        {{- if ne $firstH nil -}}
            {{- $newH := printf `%s%s` .TableOfContents $firstH -}}
            {{- replace .Content $firstH $newH | safeHTML -}}
        {{- else -}}
            {{- .Content -}}
        {{- end -}}
    {{- else -}}
        {{- .TableOfContents -}}
        {{- .Content -}}
    {{- end -}}
{{- else -}}
    {{- .Content -}}
{{- end -}}

これは無理に実装しなくても、パラメータを使用して手動でToCを無効化したのでも構わないのだが……めんどくさいし?

CSSで見た目を修正

さきほど「よくある例」として挙げたこのパターン。

このパターンでは、ほぼ大抵多くの場合で<h1>はテンプレートによって出力されており、実際に記事(markdown)で管理されているのは<h2>以下だけだと思われる。

で、Hugoが出力するTable of Contentsはulの深さ=hの深さであり、この例だと(<h1>は存在しないにも関わらず)<ul><li><ul><li>……となっており非常に見栄えが悪い。

これはHTMLを弄ろうとするより、CSSで解決する方が良いだろう……などと言っているがここはStackOverflowに投稿されてた質問のコピペである事を自白しておく。

#TableOfContents ul { 
    list-style-type: none; 
}

#TableOfContents ul ul { 
    list-style-type: disc; 
}

ulでlist-styleを打消し、ul ulで子以下のulに再度list-styleを与える=2層目以下のulにだけ適用される。という具合。

その他

ToCに「目次」のようなタイトルを付けるのは簡単すぎるので説明省略。分からない人向けにざっと説明すると、ToCを<div>で囲んで普通にタイトル用の<p>などを入れればいいだけだ。

<div class="toc">
    <p class="toc-title">目次</p>
    {{- .TableOfContents -}}
</div>

開閉機能などは(実装したことがないので)ここでは解説しない。ちなみにだが開閉機能もHTML+CSSだけで実装可能、実例としてWikipediaなどはその方法で実装されている。

余談

俺はネットの記事見るときにToCとか置いてあってもあまり利用した記憶がない。そもそも、ブログのようなサイトでToCが必要になる(同じページに何度もアクセスされる)ような記事は少ないだろう。(このブログはアクセスのほとんどが新規ユーザーだ)

その辺りを考えて、あえてToCなしのサイトにするのも選択肢としては十分ありだと思う。なんでもかんでも付ければいいというものではない。

 - Web制作