Search Console 404を329件発見し、リダイレクトルールで75%を潰した
Search Console 404を329件発見し、リダイレクトルールで75%を潰した
3月31日にリダイレクトルールを整備してから、ちょうど2週間。「2週間後に効果を見る」とカレンダーに入れていた日が今日だった。Chrome DevTools MCPでSearch Consoleを直接開き、ページインデックス作成の画面を表示した。
404の数字が目に飛び込んできた。329件。手が止まった。
CSVを落として全件分析する
Search ConsoleからCSVをエクスポートし、329件のURLをパターン別に分類した。
| パターン | 件数 | 例 | 対応 |
|---|---|---|---|
| slugベース(日付なし) | 多数 | /some-article | リダイレクト対象 |
| トレイリングスラッシュ付き | 多数 | /some-article/ | リダイレクト対象 |
/docs/ 配下 | 少数 | /docs/old-page | 放置(旧構造の残骸) |
/_nuxt/ 配下 | 少数 | /_nuxt/builds/meta/xxx | 放置(ビルドアーティファクト) |
/path/ 配下 | 少数 | /path/old-route | 放置(存在しないルート) |
slugベースURLとトレイリングスラッシュ付きURLの2つが大半を占めていた。つまり、Googlebotは /article-slug と /article-slug/ の両方でクロールしており、どちらも404を返していた。
generate-redirects.mjsを3回書き直す
問題1: newPathがslugベースのままだった
リダイレクトの向き先が /{slug} になっていたが、実際のコンテンツは frontmatter の path フィールドで定義している。slugとpathが一致しないケースで、リダイレクト先が404になるという本末転倒な状態だった。
// Before: slugをそのまま使っていた
const newPath = '/' + slug;
// After: frontmatterのpathを正規URLとして使用
const canonicalPath = extractPath(content) || '/' + slug;
問題2: トレイリングスラッシュのルールが抜けていた
Googlebotは /slug/ でクロールするが、Nuxtは /slug で配信する。トレスラ付きURLのリダイレクトルールを追加した。
// トレスラ付きも追加
if (!from.endsWith('/')) {
redirects.add(`${from}/ ${to} 301`);
}
問題3: slug === path のときスキップされていた
from === to のガード条件で、slug直叩きのトレスラ対応がスキップされていた。slugとpathが同一の記事では /slug/ → /slug のルールが生成されない。ガード条件の中でトレスラルールだけは生成するように修正した。
if (from === to) {
// 同一パスでもトレスラ付き→なしのルールは必要
if (!from.endsWith('/')) {
redirects.add(`${from}/ ${to} 301`);
}
return;
}
この3つの修正を順に入れるたびにルール数が膨らんでいった。1,653件 → 3,554件 → 最終的に4,309件。
Cloudflare Pagesの2,000ルール制限に衝突する
_redirectsファイルを生成してデプロイしたところ、ルールが2,000行を超えた分は無視される仕様に引っかかった。4,309件のうち、2,001行目以降はただのテキストとして無視される。
対策として、ルールに優先順位をつけた。
- 優先ルール(先頭に配置): 404 CSVに実際に載っていたURLとトレスラ付きルール
- 低優先ルール(後半に配置): 旧日付パス形式(
/2025-01-01/slug→/slug)
// トレスラ・slug直叩きルールを優先
const priority = allRedirects.filter(r => !r.match(/^\/\d{4}-\d{2}[\/-]/));
const rest = allRedirects.filter(r => r.match(/^\/\d{4}-\d{2}[\/-]/));
const sortedRedirects = [...priority.sort(), ...rest.sort()];
日付パス形式のURLは古いインデックスが剥がれれば404も減るので、後回しにしても実害は小さい。
結果: 329件中248件をカバー
デプロイ後、curlで何件かサンプリングして301が返ることを確認した。
- カバー済み: 248件(75%) -- slugベース + トレスラ付き
- 放置: 81件(25%) --
/docs/、/_nuxt/、/path/等のシステムパス
残り81件は実在しないシステムパスで、リダイレクトしようがない。Googlebotが自然にクロール対象から外すのを待てばいい。
次のアクション
2週間後(4月28日)にもう一度Search Consoleを確認するリマインダーをGoogleカレンダーに登録した。今回カバーした248件の404が減っていれば成功。
試行錯誤まとめ
| # | 試したこと | 結果 | 気づき |
|---|---|---|---|
| 1 | slugベースでnewPath生成 | pathと不一致の記事でリダイレクト先が404 | frontmatterのpathを正として使う |
| 2 | トレスラなしルールだけ生成 | Googlebotの /slug/ アクセスが404のまま | Googlebotはトレスラ付きでもクロールする |
| 3 | from === to で全スキップ | slug=pathの記事のトレスラ対応が漏れた | ガード条件の中でも例外処理が要る |
| 4 | 4,309ルールをそのままデプロイ | 2,001行目以降が無視された | Cloudflare Pagesは2,000ルール上限 |
| 5 | 優先ルールを先頭配置 | 404 CSVの75%をカバー | 「実際に404になっているURL」を優先するのが合理的 |
学び
- Search Consoleの404は画面をスクロールしても全体像が見えない。CSVに落として分類表を作ると、5分でパターンが浮かび上がる
- Cloudflare Pagesの
_redirectsは2,000ルールで打ち止めになる。超えた分は黙って捨てられるので、「どのルールを先頭に置くか」を考えないとデプロイ後に404が減らない from === toのガード条件は「全部スキップ」ではなく「一部だけ通す」が正しい場合がある。トレスラのように元ルールの派生で生まれるルールを見落としやすい