こんにちは。小学生向けのニュースサイト、こどもニュースをつくっています。
このサイトは静的ファイルを配信しているだけなので、サーバ側の処理がありません。リダイレクトを打つには、ホスティングの設定ファイルを使うか、別の方法が要ります。
URL の設計を変えたときに、これが問題になりました。
起きたこと
このサイトの記事は、日付と難易度で URL が決まります。
/2026/05/23/normal/
/2026/05/23/easy/
以前は難易度なしの URL がありました。
/2026/05/23/
難易度を導入したあと、この URL は使わなくなりました。ところが、既にインデックスされています。消すと 404 になり、リンクも切れます。
素直な対処は 301 リダイレクトですが、静的ファイルの配信では返せません。
meta refresh + canonical + noindex
そこで、その URL に「中身のないページ」を置くことにしました。
置いてあるのは 3 つです。
-
<meta http-equiv="refresh">で難易度付きの URL へ飛ばす -
<link rel="canonical">で正規の URL を示す -
<meta name="robots" content="noindex">でこのページ自体を除外する
本文は描画しません。E2E ではそこまで検証しています。
// 本文は出さず meta refresh + noindex + 難易度付き canonical のみ
expect(html).toContain('http-equiv="refresh"');
expect(html).toMatch(/<meta[^>]*name="robots"[^>]*noindex/);
expect(html).toMatch(new RegExp(`rel="canonical" href="[^"]*${dayNoDiff}(easy|normal)/"`));
// 本文要素は描画しない(CSS セレクタ内の文字列と区別するためタグ属性の形で判定)
expect(html).not.toMatch(/<[a-z]+[^>]*data-testid="article-body"/);
3 つを揃えているのには、それぞれ役割があります。
meta refresh は、人が来たときのためです。ブックマークや古いリンクから来ても、目的のページに着きます。
canonical は、検索エンジンに正規の URL を伝えるためです。301 ほど強い指示ではありませんが、無いよりはるかにましです。
noindex は、このスタブ自体がインデックスされないようにするためです。中身が無いページが検索結果に出ても、誰の役にも立ちません。
どの難易度へ飛ばすか
このサイトでは、同じ日の記事に難易度が 2 つあります。どちらへ飛ばすかを決める必要がありました。
既定は「普通」にしています。両方ある日は普通版へ、易しい版しか無い日は易しい版へ飛ばします。canonical も同じ判断です。
ここで気をつけたのは、この判断を sitemap と揃えることでした。sitemap が普通版を載せているのに、スタブの canonical が易しい版を指していると、検索エンジンに矛盾した情報を渡します。
正規の URL を選ぶ判断は、1 か所の述語にまとめて、すべての出力がそれを通るようにしました。
本文を描画しない理由
スタブに本文を出す実装も考えられます。リダイレクトが効かない環境でも読めるので、親切に見えます。
やめました。本文があると、検索エンジンから見て「重複した内容のページ」になります。noindex を付けていても、クロールの予算は消費します。
それに、静的書き出しでは全ページが事前に生成されるので、本文を出すぶんだけビルドの出力が増えます。日付の数だけ増えるので、無視できない量になります。
「移行のためのページ」は、移行に必要なものだけを持たせるのがよいと思います。
検証を書いておく
この手の対策は、実装した後で忘れます。数か月後にリファクタして、スタブが本文を描画するようになっても、見た目では気づきません。
E2E で 3 つの要素を検証しているのは、そのためです。とくに「本文が無いこと」の検証は、実装を変えたときに落ちてくれます。
否定の検証(無いことを確かめる)は書きにくいのですが、この種の対策では効きます。「あるべきものがある」だけを検証していると、余計なものが増えたことに気づけません。
ホスティング側の設定という選択肢
なお、多くの静的ホスティングにはリダイレクトの設定ファイルがあります。それを使えば 301 を返せます。
このサイトで使わなかったのは、設定ファイルがホスティング固有だからです。ページの生成はアプリの中で完結させておきたい、という判断でした。
ホスティングを変える予定は無いので、実際には設定ファイルでもよかったと思います。ただ、「アプリの出力だけで完結する」状態は、移行や検証のときに楽ではあります。
まとめ
- 静的配信では 301 を返せない。代わりに meta refresh + canonical + noindex のスタブを置く
- 3 つには別々の役割がある(人を飛ばす・正規 URL を伝える・スタブ自体を除外する)
- 正規 URL の判断は sitemap と同じ述語を通す。矛盾した情報を渡さない
- スタブに本文を出さない。重複した内容になり、ビルドの出力も増える
- 「余計なものが無いこと」を E2E で検証する。否定の検証がこの種の対策では効く