Tech BlogAWSツール & 技術ブログ

Search Console「検出 - インデックス未登録」が減らない原因:noindexタグページがsitemap.xmlに残っていた話

はじめに

このブログは Next.js 16 を output: 'export' で静的書き出しし、S3 + CloudFront に置く構成です。AdSense 審査に1度落ちたあと、コンテンツ品質改善と並行して Google Search Console(以下 GSC)のインデックス状況も整えてきました。

ところが、修正をいくら重ねても GSC の「ページがインデックスに登録されなかった理由」の件数が想像ほど減りません。「サイトマップが古いのでは?」「noindex の付け方が間違っている?」と仮説を出しては潰す日々で、最終的に sitemap.xml と noindex メタタグが矛盾していた ことが主因と判明しました。

同じような構成(Next.js 静的書き出し+ S3 + CloudFront)でブログを運用していて、GSC の数字が改善しない人の参考になればと思い、調査と修正の流れを記録に残します。


環境

  • フレームワーク: Next.js 16(App Router、output: 'export'trailingSlash: true
  • ホスティング: S3(tech-challenge-blog)+ CloudFront
  • ドメイン: https://cloud-and-code.com
  • 記事ソース: posts/*.md(gray-matter + remark)
  • サイトマップ: scripts/generate-sitemap.mjspostbuild で実行

GSC からエクスポートした「インデックス未登録」レポートはカテゴリごとに以下の件数でした。

カテゴリ 件数
アクセス禁止(403) 8 draft 化した記事の旧 URL、/bin/sh
見つかりませんでした(404) 1 draft 化した記事
noindex タグで除外 1 /tools/aws-free-cost-estimator/
クロール済み - インデックス未登録 2 1記事しかないタグページ
重複(別の正規ページが選ばれた) 1 /posts/ai_and_engineering/
代替ページ(適切な canonical タグあり) 15 末尾スラッシュなしの URL 群
検出 - インデックス未登録 56 多数のタグ・ツール・記事

まず疑った仮説と切り分け

仮説1: robots.txt の設定漏れ

draft 記事の URL が 403 として残っているのは、過去に公開していた記事を draft: true に変更したことで起きた現象です。public/robots.txt で該当 URL を Disallow: に追加済みで、かつ実 URL は 404 を返す状態。GSC の表示はクロール時点の記録なので、再クロール待ちです。

仮説2: noindex メタタグが効いていない

/tools/aws-free-cost-estimator/ が「noindex で除外」になっていたので、本番 HTML を直接 curl で確認しました。

curl -s "https://cloud-and-code.com/tools/aws-free-cost-estimator/" \
  | grep -oE 'name="robots" content="[^"]*"'
# => name="robots" content="index, follow"

すでに index, follow で配信されていました。GSC の表示が古いだけ。

仮説3: canonical タグの不整合

/posts/ai_and_engineering/ が「重複・別の正規ページ」になっていた件。canonical 値を確認:

curl -s "https://cloud-and-code.com/posts/ai_and_engineering/" \
  | grep -oE 'rel="canonical"[^>]*'
# => rel="canonical" href="https://cloud-and-code.com/posts/ai_and_engineering/"

trailingSlash: true を Next.js Metadata API が考慮し、自動でスラッシュを補完していたので問題なし。

ここまでで「GSC 表示は過去のクロール時点のスナップショットで、すでに修正済みのものが多い」とわかりました。残るは「検出 - インデックス未登録」の56件。


真の原因:sitemap × noindex の矛盾

「検出 - インデックス未登録」のリストを目で追っていくと、/tags/cve//tags/setup//tags/hooks//tags/cloudflare/ … と タグページばかり が並んでいます。

このブログのタグページは app/tags/[tag]/page.js で生成しており、記事数が2件未満のタグには noindex を返すようになっていました。

const postCount = posts.filter(
  p => Array.isArray(p.tags) && p.tags.includes(tagName)
).length;
const shouldNoIndex = postCount < 2;

return {
  // ...
  ...(shouldNoIndex
    ? { robots: { index: false, follow: true } }
    : {}),
};

実際に curl すると noindex, follow が出ています。ここまでは想定通り。

問題は sitemap でした。scripts/generate-sitemap.mjs を見ると、getAllTags() で取得した 全タグを sitemap に登録 しています。

const tags = getAllTags();
// ...
...tags.map((t) => ({
  loc: `/tags/${t.slug}/`,
  // ...
})),

つまり Google から見ると次の二重メッセージが届く状態でした。

  • sitemap.xml: 「/tags/cve/ を読みに来てね」
  • HTML 側: 「読みに来たけど、これは noindex なのでインデックスしないで」

これがまさに「検出 - インデックス未登録」の典型パターンです。Google は sitemap で発見したものの、noindex を見て登録を見送る。リストには残り続ける。減るわけがありません。

公開記事数で集計してみたところ、全57タグのうち 43タグが1記事のみ(noindex 対象)で、14タグが2記事以上(インデックス対象)でした。43件分が無駄に sitemap に乗っていたわけです。


修正コード

scripts/generate-sitemap.mjscount >= 2 のタグだけを取り込むようにフィルタしました。

// app/tags/[tag]/page.js は記事数 2 件未満のタグを noindex にしているため、
// sitemap でも同じ条件で除外する(noindex を sitemap に含めると Search Console で
// 「検出 - インデックス未登録」として滞留するため)。
const tags = getAllTags().filter((t) => t.count >= 2);

ついでに、postbuild で sitemap を public/sitemap.xml だけに書き出していたのも直しました。Next.js の next buildpublic/out/コピーしてから postbuild を実行するため、最新の sitemap が out/ に反映されず、デプロイされる sitemap が常に1ビルド遅れになります。

fs.writeFileSync('public/sitemap.xml', sitemap);
if (fs.existsSync('out')) {
  fs.writeFileSync('out/sitemap.xml', sitemap);
}

out/ が存在するときは両方に書き出すよう修正。

ビルド後、URL 数は 109 → 66 に減少。

Sitemap generated: 66 URLs

「代替ページ(適切な canonical タグあり)」15件はどう扱うか

GSC レポートにあったもう1つのカテゴリ「代替ページ(適切な canonical タグあり)」は、末尾スラッシュなし URL 群です。

https://cloud-and-code.com/posts/aws_secrets_management
https://cloud-and-code.com/about
https://cloud-and-code.com/tags/dev-tools
...

trailingSlash: true で生成されているのは末尾スラッシュ付きの URL ですが、CloudFront は末尾スラッシュなしのリクエストにも同じ HTML を返しています。canonical タグは末尾スラッシュ付きを指しているので、Google は両者を見て「代替ページ・正規版に統合」と正しく判定してくれている状態。

これは「実害がない正常動作」です。CloudFront Functions で 308 リダイレクトを足せばより綺麗にはなりますが、CloudFront の編集権限を別途必要とするので、現状のまま放置で問題ありません。


AdSense 再審査との関係

そもそもこの一連の対応は、4月にAdSense 審査で「有用性の低いコンテンツ」として落ちたことがきっかけでした。コンテンツ品質改善(実体験ベースのリライト・低品質記事の draft 化)と並行して、GSC のインデックス状況も整えています。

AdSense は Google Search のクロール結果を共有しているので、GSC で「検出 - インデックス未登録」が多い状態だと、AdSense 側からは「ほとんど評価対象がないサイト」に見えてしまいます。再審査前に GSC の数字を整えることは、コンテンツ改善と同じくらい重要です。

判断基準としてはこの辺りが目安になりそうです。

  • 公開記事のうち 7割以上が GSC でインデックス済み
  • 「検出 - インデックス未登録」が 大幅に減少傾向(横ばいや増加なら原因が残っている)
  • sitemap と robots.txt と HTML メタタグの 指示が矛盾していない

私は再審査を申請する前に、もう1〜2週間ほど GSC の動きを見て、インデックス済みカウントが2桁後半〜3桁前半に上がってきたタイミングで申請する予定です。


チェックリスト:sitemap と noindex の整合性

同じ罠にハマらないように、Next.js 静的書き出しサイトでよくある不整合パターンをまとめておきます。

  • sitemap に登録するページは すべて index 指定 になっているか?
  • noindex を返すページが sitemap に 入っていないか
  • robots.txtDisallow: したページが sitemap に 入っていないか
  • canonical の値が 実際に配信される URL と一致 しているか?
  • trailingSlash の設定と sitemap・canonical・内部リンクが 同じ流儀 になっているか?
  • postbuild の sitemap 生成が public/ だけでなく out/ にも反映 されているか?
  • 削除した記事や draft 化した記事の URL は sitemap から消えている か?

特に最初の2つ(sitemap と noindex の整合性)は、自動生成スクリプトを書いていると見落としがちです。タグやカテゴリのように「件数が変動するページ」を扱うときは、生成側で同じ条件でフィルタする癖を付けておくと安全。


まとめ

GSC のインデックス問題で件数が減らないとき、まず疑うべきは「サイト側の指示が矛盾していないか」です。今回のように、sitemap が「読んで」と言いながら HTML が「読まないで」と言っていると、Google は混乱したまま件数だけが残り続けます。

修正後は GSC で 「修正を検証」 ボタンを押すこと、そして主要ページに対して個別に 「インデックス登録をリクエスト」 を実行すること。この2つで Google の再評価サイクルを回せます。

これでもう少し GSC が落ち着いたら、AdSense 再審査に進む予定です。続報があればまた書きます。