Cloudflare Workers に静的アセットとSSRを同居させている開発者向けの話です。ホスト単位のリダイレクトをアプリケーションのミドルウェアに書いたところ、本番でパスによって効いたり効かなかったりしました。原因は層の順序で、Cloudflare と Astro の両方の仕様が噛んでいます。
確かめた環境は Astro 7.2、@astrojs/cloudflare 14.2、Wrangler 4.120、Cloudflare Workers の無料プランです。1つの Worker で、静的アセット(Workers Assets)とSSRを同居させています。
やりたかったこと
2つのサイトを1つの Worker にまとめました。本体サイトを example.com、記事を blog.example.com で別々に運用していたものを統合し、記事を example.com/blog/<slug> へ移しました。
旧サブドメインは301の受け口として残します。カスタムドメインは同じ Worker に付けたまま、Astro のミドルウェアでリダイレクトを書きました。
const OLD_HOST = "blog.example.com";
export const onRequest = defineMiddleware((context, next) => {
const host = (context.request.headers.get("host") ?? "").split(":")[0];
if (host !== OLD_HOST && context.url.hostname !== OLD_HOST) return next();
const target = new URL(mapPath(context.url.pathname) + context.url.search, SITE);
return Response.redirect(target.toString(), 301);
});mapPath() は / を /blog へ、/rss.xml を /blog/rss.xml へ、記事のスラッグには /blog を前置する関数です。
このリダイレクトはローカルで確かめられませんでした。wrangler dev はリクエストの Host と request.url を、Wrangler 設定の routes に並ぶ先頭のホストへ書き換えます。旧ホスト宛てのリクエストをローカルで再現する方法が見つからず、本番に出してから確かめることにしました。
半分だけ効いた
切り替え直後に旧ホストの各URLを叩いた結果です。
/<記事スラッグ>は301。example.com/blog/<スラッグ>へ送られる/rss.xmlも301/は200。統合後の本体サイトのトップがそのまま返る/smaregi/about/appsなどの静的ページも200/sitemap.xml/llms.txt/styles.cssも200
記事のURLだけが意図どおりで、残りは旧ホスト名のまま中身が見えていました。サイト全体が2つのホスト名で200を返している状態です。
301を書いた条件はホスト名だけで、パスによる分岐はありません。それなのにパスによって結果が割れました。割れ方には規則があって、301が返ったのはビルド後の出力にファイルとして存在しないパスだけです。記事はSSRなので dist/ にHTMLがなく、/rss.xml は統合で /blog/rss.xml へ移したので、こちらも存在しません。
理由は2つの層にある
Workers Assets は Worker より先に応答する
Cloudflare の静的アセットのドキュメントが、ルーティングの既定をこう書いています。
By default, if a requested URL matches a file in the static assets directory, that file will be served — without invoking Worker code. If no matching asset is found and a Worker script is present, the request will be processed by the Worker.
アセットに一致すれば、Worker のコードは呼ばれません。呼ばれないのでミドルウェアも走りません。prerender した16ページと public/ の中身はすべてアセットなので、そこへのリクエストはリダイレクトの判定に到達しないまま200で返っていました。
この既定は Cloudflare Pages と逆向きです。Pages からの移行ガイドが、その差を名指しで警告しています。
Pages would default to serving your Pages Functions ahead of static assets and_routes.jsonand Pages Functions middleware allowed you to customize this behavior. Workers, on the other hand, will default to serving static assets ahead of your Worker script, unless you have configuredassets.run_worker_first. This option is required if you are, for example, performing any authentication checks or logging requests before serving static assets.
認証チェックやログをアセット配信より前に置きたいなら設定が要る、と書かれています。ホスト単位のリダイレクトも同じ種類の処理です。
prerender のミドルウェアはビルド時に走る
もう一段あります。Astro のミドルウェアのドキュメントは、実行の時点をこう説明しています。
This rendering occurs at build time for all prerendered pages, but occurs when the route is requested for pages rendered on demand.
prerender するページでは、ミドルウェアはビルド時に実行されます。つまりこのコードは動いていなかったのではなく、訪問者の `Host` がまだ存在しない時点で動いていました。ビルド中の判定はどう転んでも「旧ホストではない」になり、next() を返して静的HTMLが出来上がります。
順序を整理するとこうなります。ビルド時にはミドルウェアが走るがホストが無く、リクエスト時にはホストがあるがミドルウェアに届きません。ホスト名で分岐する処理は、prerender するページに対しては原理的に置けない場所にありました。
直し方は層を上げること
リダイレクトをゾーンの Redirect Rule へ移しました。Cloudflare のルールは Worker より前段で動くので、アセットかどうかに関係なく全パスに効きます。Worker が起動しないぶん、いま301できているパスのぶんも安くなります。
実際に入れたのは / の1本だけです。旧ホストで検索に載っていた実体は記事のURLで、そこはミドルウェアの301が動いています。200で見えていた /smaregi などは統合で新設したページで、旧ホスト宛ての外部リンクがありません。入口を1つ塞げば、内部リンクをたどって発見される経路が消えます。
条件: http.host eq "blog.example.com" and http.request.uri.path eq "/"
動作: 静的リダイレクト https://example.com/blog (301)全パスを塞ぐなら、同じ形のルールを「同じパスで送るもの」「残り全部」の順に足します。その場合はミドルウェアを消せます。順序には注意が要って、全部拾うルールを先に置くと /_emdash/admin のような管理画面のパスまで /blog を前置され、入れなくなります。
どの層に何を置くか
選べる場所は3つあります。区別の軸は、その処理がホストの話かパスの話か、そしてアセット配信より前に必要かどうかです。
- ゾーンの Redirect Rule: Worker より前段。ホスト名で書けて、Worker が起動しない。ホスト単位のリダイレクトはここ
- `assets.run_worker_first`: Worker をアセットより先に走らせる設定。
trueで全リクエスト、または["/api/*", "!/api/docs/*"]のようにパターンの配列で選べる。Wrangler 4.20.0 以上が要る - アプリケーションのミドルウェア: アセットとして存在しないパスにだけ届く。SSRの経路で完結する処理はここ
run_worker_first は今回の用途に向きません。パターンは / か !/ で始まる必要があり、パスしか書けません。ホスト名で絞れないので、旧ホストのトップを Worker に通すには新ホストのトップも通すことになり、prerender の利点(Worker もデータベースも経由しない配信)を手放します。
ここは設定を試していないので、ドキュメントから読める範囲までにしておきます。パターン配列で / だけ指定したときに、ホストをまたいでどう振る舞うかは確かめていません。
持ち帰るものを1行にすると、ホスト単位の判断は Worker より上、パス単位の判断は Worker の中、ということになります。今回はホストの話をパスの道具で書いたので、パスによって割れました。
最後に、この種の食い違いは静かに現れます。301が効かなくても例外は出ず、ログにも残りません。canonical が正しく入っていれば検索エンジンは名寄せするので、しばらく気づかないままになります。切り替えの直後に、リダイレクトさせたいURLを1つずつ叩いて確かめる時間を取るのが、いちばん確実でした。