【Hugo】Table of Contentsをjsなしでコンテンツの中に移動する
ブログの更新サボってるので頑張っていきます。タイトルは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なしのサイトにするのも選択肢としては十分ありだと思う。なんでもかんでも付ければいいというものではない。