記事を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で最小構成を作り、条件を変えて試しました。

メニューの nameidentifierカテゴリとタグが同名_index.md結果
選び方なしありあり失敗
選び方なしありなし失敗
選び方なしなしあり成功
構築(別の名前)なしありあり成功
選び方chooseありあり成功

落ちるのは、3つが同時に揃ったときだけです。

  1. メニュー項目の name(または identifier)が
  2. 2つのタクソノミー(categoriestags)の両方に、同じ名前の項目として存在する

分かったことが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

.KeyNamechoose になるので、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 が使われる
  • その名前が categoriestags の両方に存在すると、page reference is ambiguous でビルドごと落ちる
  • 条件はメニュー名との重複。 カテゴリの _index.md の有無は無関係で、メニューに無い名前なら同名でも落ちない
  • 直し方は2つ。 タグから消す(推奨)/メニューに identifier を付ける
  • Hugoはビルド失敗時に出力先を消さない。 --cleanDestinationDir の掃除は成功後
  • 404を見たら、変更と無関係なURLも叩く。 それをやらずに「落ちた」と言わない
  • 検査スクリプトはフロントマターだけを見る。 ファイル全体を grep すると、記事内のコード例を誤検知する

エラーメッセージは page reference "選び方" is ambiguous としか言いません。どこで呼ばれた GetPage なのかは、スタックの header.html:92:38 を最後まで読まないと分かりません。 Hugoのエラーは長いですが、末尾ではなく途中の行番号に答えが書いてあります。