Hugoテーマbeautifulhugoをlegacyからcurrent版へ移行しダークモード・検索・TOCを有効化した記録

Migrating the beautifulhugo Hugo Theme from Legacy to Current: Enabling Dark Mode, Search, and TOC

* 本ページはプロモーションが含まれています

beautifulhugoのリポジトリを覗いたら、今使っているlegacy版とは別に、現行のmaster(以下current版)でダークモード・検索・目次(TOC)といった機能がまとめて入っているのに気づいた。
実は一度、“Update Beautifulhugo” というコミットでgo.modのpinだけ上げてみたことがあったが、34分後にはrevertしている。何が起きたのかコミットログには残っていないが、当時はBootstrap 3→5の破壊的変更への対応など考えずにpinだけ差し替えていたので、恐らく表示が崩れて即座に戻したのだろう。
今回はそのリベンジとして、legacy版からcurrent版への移行を段階に分けてやり直すことにした。


1回目:pinだけ上げて34分でrevertした

最初の試みはシンプルで、go.mod のバージョン文字列を書き換えただけだった。

-require github.com/halogenica/beautifulhugo v0.0.0-20260331144909-84b4ad9e12e6 // indirect
+require github.com/halogenica/beautifulhugo v0.0.0-20260706190448-b2d547f7a61c // indirect

go.sum に新バージョンのハッシュが追記されるだけの、Hugo Modulesとしてはごく普通の更新コミットになった。
ところが同日中に “Revert” コミットで元のバージョンに戻している。current版はBootstrap 3→5への移行を含む大規模刷新版で、レイアウト側(layouts/)が追従していない状態でテーマだけ差し替えたので、恐らくCSS崩れかJS未読み込みで表示が壊れたのだと思う。34分という短い間隔がその慌てぶりを物語っている。

_vendorディレクトリの罠に気づいた

pinの上げ下げをしている過程で、以前書いたHugo Modules移行の記事に1つ補足を追記した。

hugo mod vendor を実行すると _vendor ディレクトリにテーマの中身がコピーされるが、これを .gitignore に入れて管理外にしていると、ローカルとCloudflare Pagesでビルド結果がずれる原因になる。Hugoは _vendor が存在するとそちらを優先するため、go.mod でバージョンを上げてもローカルの _vendor が古いままだと更新前のテーマでビルドされ続けてしまう。一方Cloudflare Pages側は毎回リポジトリを新規cloneするため _vendor が存在せず、go.mod 通りの最新テーマでビルドされる。

このリポジトリでは元々 hugo mod vendor を使っていないので今回のrevert騒動には直接関係なかったが、「ローカルでは崩れないのにCloudflareでだけ崩れる」系の事故を将来踏まないよう、_vendor は作らない方針であることを明文化しておいた。


本番のmigration:legacy版からcurrent版へ

体制を立て直して、go.mod のpinを同じバージョンへ改めて更新した。

-require github.com/halogenica/beautifulhugo v0.0.0-20260331144909-84b4ad9e12e6 // indirect
+require github.com/halogenica/beautifulhugo v0.0.0-20260706190448-b2d547f7a61c // indirect

今回は_vendorディレクトリを作らずクリーンな状態で検証した。current版は以前一度導入を試みてrevertされたバージョンと同一だが、そのときとは違い、Bootstrap 3→5への破壊的変更に自分で対応してから取り込む方針にした。

JS読み込み場所の移動に対応する

一番効いたのがこれだった。current版ではJSの読み込み場所が footer.html から移動しており、layouts/_default/baseof.html 側でその呼び出しを追加しないと、jQuery・Bootstrap・KaTeXなどが軒並み読み込まれなくなる。

-    {{ partial "footer.html" . }}
-    {{ block "footer" . }}{{ end }}
+    {{ block "footer" . }}
+      {{ partial "footer.html" . }}
+      {{ partial "footer_custom.html" . }}
+      {{ partial "scripts.html" . }}
+    {{ end }}

pinを上げただけの1回目でおそらく表示が崩れたのは、この呼び出しが欠けていたのが主因ではないかと考えている(確認はしていない)。

Bootstrap 3→5のクラス名対応

layouts/_default/single.html のグリッドクラスとdata属性を書き換えた。

-    <div class="col-lg-8 col-lg-offset-2 col-md-10 col-md-offset-1">
+    <div class="col-lg-8 offset-lg-2 col-md-10 offset-md-1">

data-toggle="tooltip" のようなBootstrap 4未満のdata属性も data-bs-toggle に変更が必要で、この時点ではページャー部分のみ対応した(関連記事一覧・Disqusボタンのクラス名はこの後の修正で気づくことになる)。

CDNバージョンの更新

layouts/partials/head.html で読み込んでいるCDNのバージョン文字列を更新した。

ライブラリ
Bootstrap3.4.15.3.8
Font Awesome5.5.07.2.0
PhotoSwipe4.1.25.4.4
KaTeX0.16.70.16.45

CSS・JSの取り込み方針

static/css/main.css はcurrent版テーマ本体をベースにして、独自に入れていたヘッダー背景色の調整だけ再適用した(差分は1,800行超になったので個別には載せない)。static/js/main.js はcurrent版のBootstrap 5ツールチップ初期化・キーボード操作対応・prefers-reduced-motion 対応を取り込んだ一方、テーマ側のコードコピー機能(copyCodeButton)は既存のrender hook実装(codeblock-copy)と機能が重複するため取り込まなかった。

ダークモード・検索・TOCはこの時点ではあえて有効化せず、hugo.tomlcolorScheme = "light" / toc = false を明示して見た目・挙動を現状維持にした。gist/imgurの独自ショートコード、render hooks、llms.txt出力は無改修でそのまま動作することを確認済みで、ここまでの移行は見た目が変わらないことをゴールにした。


ダークモード・検索・TOCを有効化する

見た目を変えない移行が終わったところで、本来の目的だった新機能を有効化した。hugo.toml のパラメータを立てるだけでは動かず、layouts側の実装が追いついていなかったのが原因だった。

   useHLJS = false
-  socialShare = true
+  socialShare = false
   delayDisqus = true
   showRelatedPosts = true
   gcse = "partner-pub-0357592386795601:0134336397"
   googleAnalytics = "G-SWZR4GT5FQ"

+  # beautifulhugo current版で追加された新機能
+  colorScheme = "auto" # ダークモード: OS設定に追従+ナビバーのトグルで手動切替も可
+  toc = true # 目次パネルを有効化
+
+  [Params.search]
+    provider = "fuse" # ナビバーの検索オーバーレイ(既定値だが明示)

 [outputs]
-  home = ["html", "rss", "llms", "llmsfull"]
+  home = ["html", "rss", "llms", "llmsfull", "json"] # jsonは検索インデックス(index.json)生成に必要

ダークモード

layouts/_default/baseof.htmllayouts/partials/head.html に、current版の data-theme 切り替えと dark.css 読み込みを移植した。static/js/main.js にはテーマトグルのクリックハンドラが元々無かったので追加している。auto/light/darkの3状態をボタンクリックで巡回させ、localStorage に保存した状態を次回訪問時にも復元する作りにした。あわせて static/css/custom.css に、独自の見出し装飾やタグ表示用のダークモード配色を追加した。

検索

hugo.tomloutputs.homejson を追加してホームページから index.json(検索インデックス)を生成し、ナビバーのオーバーレイ検索(fuse.js)を有効化した。

TOC

layouts/_default/single.html にはそもそも toc.html の呼び出しが無かったため追加し、記事ページで目次パネルが開くようにした。


移行直後に残っていたCSSクラス不一致を修正

ここまでで一通り動いたと思っていたが、記事ページのページャー・関連記事一覧・Disqusボタンだけ無装飾で表示されていることに気づいた。原因は、Bootstrap 3→5対応をした際に layouts/_default/single.html のこれらの部分だけクラス名がlegacy版のまま取り残されていたことだった。static/css/main.css はcurrent版の新しいクラス名(post-pager / see-also-list / btn-secondary など)を前提にしているため、古いクラス名では単にスタイルが1つも当たらない状態になっていた。

-        <ul class="pager blog-pager">
+        {{ if .PrevInSection }}
+          <a href="{{ .PrevInSection.RelPermalink }}" class="nav-side-arrow nav-side-prev" ...>
+            <i class="fas fa-chevron-left"></i>
+          </a>
+        {{ end }}
+        {{ if .NextInSection }}
+          <a href="{{ .NextInSection.RelPermalink }}" class="nav-side-arrow nav-side-next" ...>
+            <i class="fas fa-chevron-right"></i>
+          </a>
+        {{ end }}
+        <ul class="post-pager blog-post-pager">
           {{ if .PrevInSection }}
-            <li class="previous">
-              <a ... data-toggle="tooltip" ...>&larr; {{ i18n "previousPost" }}</a>
+            <li class="pager-prev">
+              <a ... data-bs-toggle="tooltip" ...>&larr; {{ i18n "previousPost" }}</a>
             </li>
+          {{ else }}
+            <li class="pager-prev"></li>
           {{ end }}

修正内容は次の5点。

  • ページャーのクラス名を post-pager / pager-prev / pager-next に変更
  • current版CSSが768px以上で前提とする nav-side-arrow(サイドナビ矢印)を追加。これが無いと768px以上で前後記事ナビが消えてしまう
  • 関連記事一覧のマークアップを see-also-list / see-also-item 形式に変更
  • Disqusボタンのクラスを、Bootstrap 5に存在しない btn-default から btn-secondary に変更
  • 記事がフロントマターに持つ categories を表示するブロックを追加(テーマ本体にスタイルはあったが、表示ロジック自体が無かった)

あわせて i18n/en.yaml を新規に追加し、関連記事見出し(seeAlso)だけ「関連項目」に日本語化した。このサイトは DefaultContentLanguage = "en" の設定のままなので、他のUI文言(前後記事ナビの文言など)は現状維持にしている。

# サイト独自のUI文字列上書き
# DefaultContentLanguage = "en" のため、テーマ本体のi18n/en.yamlが使われる。
# 個別の文言だけ日本語に差し替えたい場合はここに追記する。
- id: seeAlso
  translation: "関連項目"

Gist埋め込みの表示崩れを修正

CSS不一致の修正で一段落したと思ったら、今度はGist埋め込みを使っている記事でダークモード時に表と文字が同化して読めなくなっていることに気づいた。

原因は、記事内の通常テーブル用の縞模様スタイル(table tr / table tr:nth-child(2n))が、GistのHTML(<table class="highlight">)にも詳細度の関係で一部の行だけ意図せず適用されてしまうことだった。特にダークモードでは、サイト側が行の背景色だけを暗くする一方、Gist自身の文字色(ライトモード固定の濃色)はそのままになるため、1行おきに文字が背景色と同化してしまう。Gistは常に自身のライトテーマで描画され、サイトのダーク切り替えには追従しない。

/* GitHub Gist埋め込み対応
   Gistは常に自身のライトテーマで描画されるため、行の背景・罫線は
   サイト側で一切触らず、GitHub側のスタイルをそのまま活かす。 */
.gist table tr,
.gist table tr:nth-child(2n) {
  background-color: transparent !important;
  border-top: none !important;
}

.gist-file {
  margin-top: 24px !important;
  border-radius: 6px !important;
  overflow: hidden !important;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12) !important;
}

[data-theme="dark"] .gist-file {
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
}

!important を多用しているのは、GitHub側の外部CSS(.gist .gist-file、詳細度2)や、テーマ側の .blog-post :first-child { margin-top: 0 } という共通リセットが詳細度で勝ってしまうためで、Hugo標準のコードブロック(div.highlight)には影響しないよう .gist-file を祖先とするセレクタに限定してある。あわせて横長のコード行が出たときのスクロールバーを細くするスタイルも足した。ついでに socialSharetrue から false に変更しているが、これについてはコミットログに理由を残していない。


まとめ

ステップ内容
1回目のpin更新検証なしで差し替えて34分後にrevert
_vendor の注意点ローカル/Cloudflare Pagesのビルド差異を防ぐため作らない方針を明文化
legacy→current移行JS読み込み場所の移動・Bootstrap 3→5対応。見た目は維持したまま完了
新機能の有効化ダークモード・検索(fuse.js)・TOCをlayouts側の実装込みで移植
CSSクラス不一致の修正ページャー・関連記事・Disqusボタンの取りこぼしをあとから発見して修正
Gist埋め込みの修正ダークモードでの文字色同化を、Gist側のスタイルを尊重する形で解消

一度に全部やろうとせず、「見た目を変えない移行」と「新機能の有効化」を分けたのは結果的に良い判断だった。ただしBootstrap 3→5のクラス名対応は横断的な変更になりがちで、今回のようにページャー部分だけ取りこぼす、といったことが起きやすい。テーマの大規模刷新に追従するときは、移行直後に全ページタイプを一通り目視するくらいのつもりでいたほうがよさそうだ。

今までのbeautifulhugoテーマは本文の横幅が狭いのがずっと気になっていたが、更新後は横幅が広くなったのが一番嬉しい。実際に移行前後のコミットをそれぞれ hugo server で立ち上げ、記事ページの article.blog-post の実寸をブラウザ幅ごとに測ってみると次のようになった。

ブラウザ幅旧(Bootstrap 3)新(Bootstrap 5)
1280px750px736px
1440px以上750px(頭打ち)856px

原因はBootstrap自体の .container のブレークポイント設計の違いだった。Bootstrap 3の .container は1200px以上で幅1170pxに頭打ちになり、そこから広がらない。対してBootstrap 5の .container は1400px以上でさらに1320pxまで広がるブレークポイントが増えており、本文列(col-lg-8)の実寸も750px→856px(+106px、約14%増)まで広がった。
一方でBootstrap 5はlgブレークポイントの閾値自体がBootstrap 3より低く(992px vs 1200px)なっているため、1280px前後の幅ではむしろ14px狭くなる逆転も起きている。普段このブログを見ているのは1440px以上の外部モニタだったので、体感の「広くなった」はこの+106pxのことだったようだ。

関連項目