自分のサイトの sitemap.xml に載っているURLを数えたら189だった。実際に置いてあるHTMLは214枚あった。1枚は意図して外しているので、抜けているのは24枚。その24ページの中に、値段のついた商品の販売ページが3つとも入っていた。
Google Search Console でその1つを検査したら、こう返ってきた。「URL が Google に認識されていません」。インデックスに登録されていない、ではない。存在を知られていない。
この記事は2026年8月17日に自分の環境で測った話だ。
誰が何をやっているか
Claude Code を自律で動かしている。Claude Code は、AIがコマンドを実行しながら開発を手伝うツールだ。それを人がほぼ介在しない形で回して、記事を書いたり商品を作ったりさせている。自分はコードを書けない側の人間で、AIに指示を出して結果を見る役をやっている。
その運用で、GitHub Pages に置いた静的サイトのページが214枚まで増えた。無料の診断ツール、記事、商品の販売ページ。AIが作ったものと、自分が方針を決めて作らせたものが混ざっている。
sitemap.xml は、そのサイトにどんなページがあるかを検索エンジンへ知らせるファイルだ。これが手で管理されていた。ページを足すたびに、誰かが手で1行足す前提になっていた。
誰も足していなかった。
数え直したきっかけは、別の面が死んだこと
きっかけは商品の売り上げではない。到達だった。
自分が使っている公開の面のうち、Zenn は6月中旬から検索の表示が落ちている。同じ頃に、ページへ noindex が付いているのを見つけた。ただし付いた時期そのものは分からない。見つけた日より前に測っていないからだ。分かっているのは、いま自分の記事115本(最も古いのは2月8日)と本12冊の全部に付いていること、8月に入っても解けていないこと、そして同じ日に他の書き手の記事と本を13件測ったら、どれにも付いていなかったことだ。note も、プロフィールとメンバーシップのページに noindex, nofollow が付いていた。こちらも他の書き手の同じ種類のページには付いていなかった。
つまり自分の持ち物のうち、検索から人が来られる面が GitHub Pages しか残っていない。
だから数えた。214ページある面が、実際にどう見えているのかを一度も確かめていなかったからだ。
sitemap と実ページ数を突き合わせる
やり方は単純だ。sitemap.xml を取ってきて <loc> を数える。ディレクトリの中のHTMLを数える。比べる。
# sitemap に載っている URL の数
curl -s https://example.github.io/yoursite/sitemap.xml | grep -c '<loc>'
# 実際に置いてある HTML の数
find docs -name '*.html' | wc -l
189 と 214 だった。214 のうち1枚は意図して sitemap から外しているので、比べるべきは 189 と 213 になる。
ここで一度、数え方を間違えた。sitemap に拡張子なしのURLが混ざっていた(サイトのトップを除くと1件)。ファイル名と単純に突き合わせると「載っていない」と誤判定する。ページ側が <link rel="canonical"> で拡張子なしの形を正としていたためだ。両方の形を許して数え直したら、差は24だった。
差の中身を見て、手が止まった。
有料の監査サービスの日本語の販売ページが3枚とも入っていた。月額の商品を買う前に中身を確かめてもらうための無料の見本が6枚。日本語の自己診断ツールが8枚。残りは英語のページと、ツールの補助ページ。
売り物の入口が、まとめて抜けていた。
sitemap に無いことと、検索に出ないことは同じではない
ここは正確に書きたい。sitemap は「ここにページがありますよ」と伝える手段の1つでしかない。載っていなくても、他のページからリンクされていればクローラーは辿り着ける。だから「sitemap に無い=検索に出ない」とは言えない。
だから確かめた。Google で site: を使って、1ページずつ狙って引いた。
sitemap に無い側を2枚。どちらも「該当なし」。
そのままだと自分の検索のやり方が悪いだけかもしれないので、対照を取った。sitemap に載っている側を同じ形の検索で2枚。どちらも結果が並んだ。
同じ検索の形で、載っている側は出て、載っていない側は出ない。少なくともこの面では、sitemap の漏れと検索に出ないことが一致していた。
決定的だったのは Search Console の URL 検査
site: 検索は便利だが、Googleが公式に返す答えではない。もっと直接的な口がある。Search Console の URL 検査だ。調べたいURLを入れると、そのURLについてGoogleが今どう扱っているかを返してくれる。
有料の販売ページ3枚を1枚ずつ入れた。3枚とも同じ判定だった。
URL が Google に認識されていません
検出 — 参照元サイトマップ: 参照元サイトマップが検出されませんでした
検出 — 参照元ページ: 検出されませんでした
クロール — 前回のクロール: 該当なし
「インデックスに登録されていない」と「認識されていない」は違う。 前者は、見に来たが載せなかった、という意味になりうる。後者は、そもそも存在を知らない。
前回のクロールが「該当なし」だった。一度も見に来ていない。
自分の売り物のページを、Googleは知らなかった。3枚とも公開してから4日から8日たっていた。
「インデックス未登録」の理由は複数ある
ここは自分が読み違えかけたところだ。
Search Console が「インデックスに登録されていません」と言う時は、理由の欄を読む。クロールしたが低品質と判定した。重複と見なして別のURLを正とした。robots で拒否している。理由はいくつも並びうる。それぞれ打つ手が違う。
今回は「URL が Google に認識されていません」だった。品質の問題でも設定の問題でもない。届いていないだけだ。
打つ手は単純になる。存在を知らせればいい。
直したのは sitemap ではなく、sitemap の作り方
24行足して終わりにもできた。それだと3か月後にまた同じことが起きる。実際、同じ形の事故を2週間前にも踏んでいた。
サイト内検索用のインデックスファイルも、手で更新する作りだった。中身は140件のまま。実際のページは208枚まで増えていた。日本語のページと診断ツールが、まるごと自前の検索から漏れていた。誰も気づかなかったのは、何もエラーが出ないからだ。
生成物を手で更新する運用は、静かに古くなる。人が忘れた瞬間から、何も言わずにずれていく。
だから作り方を直した。sitemap を docs 配下のHTMLから生成するスクリプトを書いた。そのうえで、CIで「再生成して差分が出たら落とす」ようにした。
- name: Generated sitemap is current
run: |
python3 scripts/build-sitemap.py
if ! git diff --quiet -- docs/sitemap.xml; then
echo "::error::docs/sitemap.xml is out of date."
echo "Run: python3 scripts/build-sitemap.py and commit the result."
git diff --stat -- docs/sitemap.xml
exit 1
fi
echo "docs/sitemap.xml matches docs/**/*.html ($(grep -c '<loc>' docs/sitemap.xml) URLs)"
これで、ページを足して sitemap を更新し忘れたPRは通らなくなる。
生成スクリプトで気をつけた3点
書きながら詰まった点を残しておく。同じものを書く人がいたら、たぶん同じところで詰まる。
1つ目。CIで差分を見るなら、生成物は同じ入力から必ず同じ結果にならないといけない。 ファイルの更新時刻は使えない。CIはチェックアウトした瞬間の時刻になるからだ。gitのコミット日時も、浅いクローンだと取れない。自分は lastmod と priority を既存の sitemap から引き継ぐ形にした。リポジトリの中にあるものだけで決まるようにするためだ。新しく足されたページには lastmod を付けない。付けないのは仕様上ゆるされているし、いつ更新されたか知らないのに日付を書くほうが嘘になる。
2つ目。ページが <link rel="canonical"> で自分の正しいURLを宣言していたら、それに従う。片方だけ違う形にすると、ページと sitemap でどちらが本物かの答えが食い違う。
3つ目。これは自分がやらかした。最初に書いた版は glob('*.html') でディレクトリ直下だけを見ていた。1階層下に置いてあったページが1枚、静かに落ちた。
落ちたことに気づけたのは、生成した後で旧版と突き合わせて「消えたURL」を数えたからだ。増えた数だけ見ていたら気づかなかった。24件増えた、で満足して終わっていた。
生成物を差し替える時は、増えたものではなく減ったものを数えたほうがいい。
検査が0件を返した時、まず疑うべきもの
この日の作業で、0件という答えを4回受け取った。4回とも、対象が無いのではなく、検査のほうが届いていなかった。
sitemap の突き合わせで拡張子なしのURLを取り逃した。別の場面では、ページ本文を確かめようとして数えた語が、実はタイトルにも出る語だった。本文にしか出ない語で数え直したら、そもそも本文を1文字も取れていなかった。
0件には少なくとも3つの意味がある。本当に無い。探し方が届いていない。検査そのものが成立していない。症状は全部同じ「0」だ。
対処は1つで足りる。0件を根拠にする前に、必ず当たるはずのもので同じ検査を撃つ。 対照が1件でも返れば検査は生きている。対照も0なら、検査を直すのが先だ。
翌日に見に行ったら、直っていたのは半分だった
生成の仕組みを入れて sitemap を213件にした。ここで終わりにするつもりだった。
翌日、Search Console でその sitemap の状態を見た。「取得できませんでした」。検出されたページ数は0。
実物を疑う前に、実物を叩いた。本番のURLは HTTP 200 を返し、content-type は application/xml、XMLとして妥当で、<loc> は213件あった。ファイルは健全だった。
つまりあの表示は、いまのファイルの状態ではなかった。送信した時点で取りに行って失敗した、という記録がそのまま残っていただけだ。送ったのが配備より先だったか、その時間帯に向こう側が不調だったか、どちらかだろう。状態の表示は、いつかの観測の記録であって、現在の事実ではない。
そのまま、販売ページ3枚をもう一度1枚ずつ URL 検査に入れた。
2枚は「登録済み」に変わっていた。残る1枚だけが、前の日と同じ「URL が Google に認識されていません」のままだった。しかも、いちばん高い商品のページだった。
その1枚では「参照元サイトマップが検出されませんでした」も変わっていなかった。sitemap が読まれていないのだから、そこから辿り着けるはずがない。2枚が登録されたのは、たぶん他のページからのリンク経由だ。
だからその1枚は、sitemap を待たずに URL 検査から直接「インデックス登録をリクエスト」を押した。「URL を優先クロールキューに追加しました」と返ってきた。sitemap も送信し直した。
なぜ3枚のうち1枚だけ残ったのかは、こちらからは分からない。分かるのは、生成の仕組みを入れた時点で閉じていたら、いちばん高い商品のページは今日も知られていないままだった、ということだけだ。
直したことと、直ったことは別だ。
今日から確かめられること
自分のサイトを持っている人なら、10分で同じ点検ができる。
sitemap の <loc> の数と、実際のHTMLの数を比べる。ずれていたら、ずれている側に何が入っているかを見る。うちの場合はそこに売り物が固まっていた。
そのうえで、いちばん失いたくないページを1枚だけ Search Console の URL 検査に入れる。「認識されていません」と出たら、それは品質の話ではない。届いていないだけだ。
そして、生成物を手で更新している場所が他にもないかを探す。検索インデックス、OGP画像の一覧、記事の目次、リンク集。エラーを出さずに古くなるものは、たいてい複数ある。
うちは2つ見つかった。1つは2週間前に、もう1つは今日。どちらも、何も壊れていないように見えていた。
最後にもう1つ。sitemap を出したら、翌日にその sitemap の「状態」と「検出されたページ数」を見に行く。見るのは送信が完了したかではなく、向こうが取得できたかだ。送信の完了は、こちらが投げたという意味しか持たない。
この記事の調査と是正は Claude Code に実行させた。数値は 2026年8月17日に自分の環境(GitHub Pages 上の214ページ)で測ったもの。翌日談の節は 2026年8月18日に同じ環境で測り直したもので、Search Console の表示はどちらも画面で確認した文言をそのまま引いている。生成スクリプトとCIの設定は MIT ライセンスで公開している。
危ない操作を実行前に止めるフックは cc-safe-setup(MIT・無料)に置いてある。AIにリポジトリを触らせる運用をしているなら、先にこちらを入れておくといい。