記事を1本追加して hugo を叩いたら、ビルドごと落ちました。
ERROR error building site: render: failed to render pages:
render of "/categories" failed: ".../themes/PaperMod/layouts/baseof.html:24:7":
execute of template failed: template: taxonomy.html:24:7:
executing "taxonomy.html" at <partialCached "header.html" . .Page>:
error calling partialCached: ".../PaperMod/layouts/_partials/header.html:92:38":
execute of template failed: template: _partials/header.html:92:38:
executing "_partials/header.html" at <site.GetPage>:
error calling GetPage: page reference "選び方" is ambiguous
追加した記事のフロントマターは、これでした。
categories: ["選び方"]
tags: ["VPS", "さくらのVPS", "メモリ", "n8n", "Dify", "スペック", "選び方"]
選び方 をカテゴリとタグの両方に書いていました。 これだけです。
犯人はテーマのこの1行
_partials/header.html の92行目です。
{{- $is_search := eq (site.GetPage .KeyName).Layout `search` }}
メニュー項目が検索ページかどうかを判定するために、site.GetPage を呼んでいます。
.KeyName は、メニュー項目に identifier があればそれ、無ければ name です。 私の設定はこうでした。
menu:
main:
- name: 選び方
url: /categories/choose/
weight: 40
identifier がないので、site.GetPage "選び方" が呼ばれます。
そこに /categories/選び方/ と /tags/選び方/ の2つが存在する。Hugoはどちらを指しているか決められず、エラーを返します。テンプレート側でエラー処理をしていないので、サイト全体のビルドが止まります。
6パターン再現して、条件を特定した
「たぶんこれが原因」で終わらせず、同じHugo(v0.165.0+extended)とPaperModで最小構成を作り、条件を変えて試しました。
メニューの name | identifier | カテゴリとタグが同名 | _index.md | 結果 |
|---|---|---|---|---|
| 選び方 | なし | あり | あり | 失敗 |
| 選び方 | なし | あり | なし | 失敗 |
| 選び方 | なし | なし | あり | 成功 |
| 構築(別の名前) | なし | あり | あり | 成功 |
| 選び方 | choose | あり | あり | 成功 |
落ちるのは、3つが同時に揃ったときだけです。
- メニュー項目の
name(またはidentifier)が - 2つのタクソノミー(
categoriesとtags)の両方に、同じ名前の項目として存在する
分かったことが2つあります。
- カテゴリの
_index.mdがあるかどうかは関係ありません。 無くても落ちます - メニューに載っていない名前なら、カテゴリとタグが同名でも落ちません。
site.GetPageが呼ばれないからです
つまり 「カテゴリ名とタグ名を同じにしてはいけない」ではなく、「メニューに出しているカテゴリ名を、タグにも使ってはいけない」 が正確な条件です。
直し方は2つ
1. タグから消す(今回やったほう)
categories: ["選び方"]
tags: ["VPS", "さくらのVPS", "メモリ", "n8n", "Dify", "スペック"]
カテゴリが本体で、タグは重複していただけなので、消すのはタグの側です。
sed -i 's/, "選び方"\]/]/' content/posts/該当記事.md
hugo --minify
そもそもカテゴリとタグに同じ言葉を入れる意味はありません。 読者から見ても、同じ名前のページが2つある状態は分かりにくい。直すべきは設計のほうでした。
2. メニューに identifier を付ける
同名のタグをどうしても残したい場合は、こちらです。
menu:
main:
- name: 選び方
identifier: choose # ← これ
url: /categories/choose/
weight: 40
.KeyName が choose になるので、site.GetPage "choose" が呼ばれます。該当するページが無いので nil が返りますが、ビルドは通ります(テストで確認しました)。
表示名は「選び方」のまま変わりません。
再発を防ぐなら ── そして、その検査で私はもう一度つまずいた
デプロイスクリプトの先頭で検査すれば、同じ事故は防げます。ただし雑に書くと、自分の首を絞めます。
最初、私はこう書きました。
grep -h "^tags:" content/posts/*.md | grep -E "構築|実測|トラブル|選び方"
この記事自身が引っかかって、デプロイが止まりました。
当然です。上のほうに「悪い例」としてフロントマターを載せてあるので、grep はそれを拾います。解説のためのコード例を、本物の設定として誤検知したわけです。
正しくは、先頭の --- で挟まれた範囲だけを見ます。
for f in content/posts/*.md; do
line=$(awk 'NR==1 && $0=="---" {inb=1; next}
inb && $0=="---" {exit}
inb && /^tags:/ {print; exit}' "$f")
case "$line" in
*'"構築"'*|*'"実測"'*|*'"トラブル"'*|*'"選び方"'*)
echo "NG: $f: $line" >&2; exit 1 ;;
esac
done
Markdownを機械的に検査するときは、フロントマターと本文を分けてください。 本文にはコード例が入ります。ファイル全体を grep すると、書けば書くほど誤検知が増えます。
ここからは、私がやった誤診の話
技術的な原因は上のとおりですが、この場で私はもう1つ間違えています。 そちらのほうが再発しやすいので書き残します。
ビルドが落ちた直後、私はこう確認しました。
curl -sI https://example.com/新しい記事/ # 404
curl -sI https://example.com/categories/choose/ # 404
2件とも404。私は「サイトが落ちた」と判断しました。 デプロイスクリプトが --cleanDestinationDir を使っていたので、「ビルド失敗で公開ディレクトリが空になった」と考えたわけです。
間違いでした。
叩いた2つのURLは、どちらも今回のビルドで初めて生成されるはずだったページです。ビルドが失敗した以上、404になって当たり前でした。既存の記事のURLを、私は1つも確認していませんでした。
確かめました
同じHugoで、同じ手順を再現しました。成功するビルドで出力先を作り、そのあと同じ出力先へ、壊れた状態で --cleanDestinationDir 付きビルドを流します。
1回目(成功)のファイル数 : 20
2回目 終了コード : 1 ← 同じ ambiguous エラー
2回目(失敗)後のファイル数 : 20 ← 減っていない
index.html : 残っている
Hugoはビルドに失敗しても、出力先のファイルを消しません。 --cleanDestinationDir の掃除は、ビルドが成功したあとに走ります。
つまりあのとき、既存の記事は普通に配信され続けていました。 欠けていたのは新しいページだけです。
教訓
「404が出た」と「サイトが落ちた」は別のことです。
障害を判定するときは、今回の変更と無関係なURLを叩いてください。 トップページ1つで足ります。
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/
これが200なら、落ちているのはサイトではなく、新しいページだけです。対応の緊急度がまったく変わります。
私は「サイトが落ちた」と宣言して、緊急復旧の手順を組み立てました。結果的に直りましたが、判断の根拠は足りていませんでした。
まとめ
- PaperModのメニューは
site.GetPage .KeyNameを呼ぶ。identifierが無ければメニューのnameが使われる - その名前が
categoriesとtagsの両方に存在すると、page reference is ambiguousでビルドごと落ちる - 条件はメニュー名との重複。 カテゴリの
_index.mdの有無は無関係で、メニューに無い名前なら同名でも落ちない - 直し方は2つ。 タグから消す(推奨)/メニューに
identifierを付ける - Hugoはビルド失敗時に出力先を消さない。
--cleanDestinationDirの掃除は成功後 - 404を見たら、変更と無関係なURLも叩く。 それをやらずに「落ちた」と言わない
- 検査スクリプトはフロントマターだけを見る。 ファイル全体を
grepすると、記事内のコード例を誤検知する
エラーメッセージは page reference "選び方" is ambiguous としか言いません。どこで呼ばれた GetPage なのかは、スタックの header.html:92:38 を最後まで読まないと分かりません。 Hugoのエラーは長いですが、末尾ではなく途中の行番号に答えが書いてあります。