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.mjsをpostbuildで実行
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.mjs で count >= 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 build は public/ を 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.txtでDisallow:したページが 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 再審査に進む予定です。続報があればまた書きます。