チュートリアル
Cloudflare Workers のリダイレクトは _redirects 1枚で足りる
· 読了まで約9分
目次
先週、このサイトからタグページを6つ消しました。タグ語彙の整理に巻き込まれた形です。/tags/satori/、/tags/json-ld/ ほか4つ、どれも記事1本しか紐づいていないタグでした。ページを消すこと自体は簡単です。厄介なのは責任を持って消すほうで、インデックス済みの URL も外部からのリンクも、全部どこかまともな場所に着地しないといけない。つまり 301 リダイレクトが要ります。
このサイトは Workers static assets に完全な静的 Astro ビルドを載せているだけで、Worker のスクリプトは存在しません。URL を十数個マッピングするためだけに書く気もありませんでした。結果から言えば、書く必要はありませんでした。Workers static assets は Cloudflare Pages でおなじみのプレーンテキストの _redirects ファイルをそのままサポートします。記法も機能も Pages と同じで、コンテンツサイトが必要とするものはひととおり揃っています。要るのは十数行のテキストファイルだけで、頭を使うのはどこへ飛ばすかを決めるほうでした。
ファイルの置き場所
_redirects という名前のファイルを作ります。拡張子はありません。置くのは静的アセットのディレクトリ、フレームワークを使っているなら中身がそのままビルド出力にコピーされるディレクトリです。Astro なら public/ で、ファイルは dist/ の直下に出ます。
public/
├── _redirects
├── robots.txt
└── favicon.svg
Cloudflare はデプロイと一緒にこのファイルを拾い、ルールをエッジで適用します。ファイル自体が配信されることはありません。/_redirects にリクエストしてもルール一覧が漏れることはない、ということです。
Pages から来た人には、これは同じ仕組みで記法も同じ、と言えば通じます。Pages から Workers への移行が身構えたより小さく済む理由の1つがこれで、_redirects も _headers も手を入れずについてきます。
記法
1行に1ルール。書くのは元のパス、行き先、そして任意のステータスコードです。
# 旧タグページ → 引き継いだタグ
/tags/satori/ /tags/seo/ 301
/tags/json-ld/ /tags/seo/ 301
# 移動した記事
/blog/old-slug/ /blog/new-slug/ 301
# 外部への転送もできる
/discord https://discord.gg/example 302
# で始まる行はコメントです。ステータスコードを省くと 302 になり、使えるのは 301・302・303・307・308 の5つです。
恒久的に消したり改名したりしたものには 301 を明示します。302 は検索エンジンに「移動は一時的だ」と伝えるので、古い URL はインデックスに残ったまま様子見されます。301 なら古い URL の評価が新しいほうへ移り、古いほうはインデックスから落ちます。パーサーの既定値として 302 が安全なのは分かります。ですが、構成を組み替えたサイトで 302 が欲しくなる場面はまずありません。
末尾スラッシュの罠
これは危うく見落としかけました。静的ルールはパスの完全一致で照合されます。最初に書いたルールは末尾スラッシュ付きの /tags/satori/ で、ブラウザでは期待どおり動きました。sitemap がずっとその形を正規として出していたからです。ですが、外からリンクを貼る人は私の sitemap を見ていません。/tags/satori(スラッシュ無し)を貼った人がいれば、そのリクエストはルールの脇をすり抜けていました。
実際に取りこぼすかどうかはアセットルーティングの URL 正規化次第です。スラッシュ無しで叩いて確かめてもよかったのですが、その正規化の挙動に寄りかかること自体が嫌だったので、確かめずに両方書きました。並べても2行で、疑問そのものが消えます。
/tags/satori /tags/seo/ 301
/tags/satori/ /tags/seo/ 301
機械的ではあります。ただ「自分が試したリダイレクト」と「ウェブが実際に投げてくるものを受け止めるリダイレクト」を分けるのはここです。対象 URL が多いなら、splat を使えば1ルールで両方の形を拾えます。次節で扱います。
splat とプレースホルダ
URL が増えてくると、完全一致を1行ずつ並べるのが割に合わなくなります。そのために動的なマッチングが2種類用意されています。
パスの残りをまとめて拾うワイルドカードがあり、これを splat と呼びます。マッチした部分は行き先で :splat として使い回せます。
# セクションごと移動する
/docs/* /guides/:splat 301
/docs/setup/install へのリクエストは /guides/setup/install に着地します。splat は1ルールに1つまでです。
パスセグメント1つ分だけに当てたいときは、プレースホルダを使います。
/posts/:year/:slug /blog/:slug 301
日付入りの URL 構造(/posts/2024/my-article)をフラットな形に畳む、ブログ移行の定番ルールです。各プレースホルダは行き先で1回ずつ参照できます。
この動的ルールがあるので、大きな移行でも _redirects で足りると思っています。日付入りの URL が何百本もある WordPress や Jekyll からの移行でも、たいていは splat ルール数本に収まります。完全一致の行を何百も書き並べることにはなりません。
評価順と上限
ルールは上から順に評価され、最初にマッチしたものが勝ちます。だから、splat に飲み込まれては困る完全一致のルールは splat より上に置きます。
# 個別の例外を先に…
/docs/legacy-page /blog/why-we-dropped-this/ 301
# …そのあとに総取りのルール
/docs/* /guides/:splat 301
上限はコンテンツサイトには十分な広さです。1デプロイあたり静的ルール2,000本と動的(splat・プレースホルダ)ルール100本、1行1,000文字まで。この規模を超えると、リポジトリの外でアカウント単位に管理する Cloudflare の Bulk Redirects が本来の道具になります。
リダイレクトの判定はアセットの探索より前に走るので、元のパスにファイルがまだ存在していてもルールが発動します。たまに驚かされますが、移行中に欲しいのはまさにこの挙動です。ルールを消すまでは、ルールのほうが優先されます。
できないこと
この方法に踏み切る前に知っておく境界が3つあります。
- リライトができるのは
200の代理配信だけです。相対 URL なら200で代理配信できます(URL はそのままで、中身はサイトの別の場所から来る)。ですが nginx でいう一般的なリライトはここにはありません。静的サイトなら、私はその 200 の代理配信のほうも疑ってかかります。同じ内容を返す URL が2つある状態は重複コンテンツの問題で、あとから canonical で塞ぐ羽目になります。 - Worker コードが処理するルートには効きません。一部のルートをスクリプトで捌いているプロジェクトなら、
_redirectsが支配するのは静的アセット側だけです。スクリプト側のルートのリダイレクトはスクリプトに書きます。 - フラグメントは関与しません。
#sectionはサーバーに届かないので、ルールでマッチさせようがありません。リダイレクト先へフラグメントを引き継ぐ処理は、たいていブラウザが勝手にやってくれます。
このサイトではどれも問題になっていません。デプロイの中身はリダイレクトを入れる前と同じままです。dist/ フォルダと十数行の wrangler 設定、そして CSP を運ぶ _headers と、履歴を運ぶ _redirects。
効いているか確かめる
ブラウザではなく curl を信じます。ブラウザは 301 を強くキャッシュするので、古いキャッシュが昨日の壊れた挙動を平気で見せてきます。
curl -sI https://astro.p4ni.com/tags/satori/ | head -3
HTTP/2 301
location: /tags/seo/
スラッシュ付きと無しの両方、それにリダイレクトしないはずの URL も1つ叩いておきます。移行の最中なら、旧 sitemap の URL をループで回して 404 を返すものを grep します。5分の curl を惜しんで1か月ぶんのリンク評価を漏らすほうが、よほど高くつきます。
もう半分は SEO の仕事
ファイルは仕組みにすぎず、実際の仕事はリダイレクトの地図を引くほうです。引きながら意識していたことを挙げます。
- トップページに集めず、いちばん近い生き残りへ飛ばす。大量の URL を
/に集めると、Google はそれをソフト 404 として扱います。守ろうとしたリンク評価は結局蒸発します。消したタグページは、同じ記事群をカバーする生き残りのタグへそれぞれ送りました。等価物としてはこれが最も近い。 - ホップは1回。
/aが/bに移り、あとで/bが/cに移ったなら、最初のルールの行き先を/cに書き換えます。連鎖もある程度までは追跡されますが、1ホップごとにレイテンシとクロールバジェットを食います。 - 自分の内部リンクも直す。リダイレクトは自分では手の届かない URL、つまり他人のリンクや古いインデックスのためのものです。自分のページがそれに寄りかかっているのはおかしいので、コードベースを旧パスで grep して元から直します。
- ルールは置き続ける。301 は1週間で役目を終えません。外部のリンクは更新されませんし、クローラーは何か月も古い URL を訪ね直します。残すコストはゼロなので、私のルールは理由を書いたコメント付きで無期限に置いておきます。
ページを消すのが怖くて1週間先延ばしにしていたのに、実際の手当ては十数行のリダイレクトルールとデプロイ1回でした。静的サイトを Workers に載せていて、「静的ホスティングではリダイレクトできない」と思って掃除を避けているなら、できます。必要なのはテキストファイル1枚です。