Astro の routeRules から外してもページがキャッシュされる
エッジキャッシュを入れるとき、問い合わせフォームのページだけは外したいと考えました。routeRules に列挙しなければ対象にならない、と理解していたので、そのページを書かずにデプロイしました。
本番で確認したら、HIT が返ってきました。
まず実測
routeRules にはトップと固定ページ 3 枚だけを書き、/contact は意図的に書いていません。
$ curl -sI https://example.com/contact
cf-cache-status: HIT
age: 66age: 66 は、66 秒前に保存されたレスポンスをそのまま返しているという意味です。書いていないルールが効いていないどころか、書いたページと同じようにキャッシュされていました。
「有効かどうか」は 2 つの条件のどちらかで決まる
Astro 7 のキャッシュは、リクエストごとに「このレスポンスはキャッシュ対象か」を判定します。判定しているのは astro/dist/core/cache/runtime/cache.js で、条件はこうなっています。
maxAgeが設定されている- または、タグが 1 つでも積まれている
routeRules は前者を埋める仕組みです。ルールに書かなければ maxAge は設定されません。しかし後者が残ります。
CMS からコンテンツを引くページには、たいてい次の 1 行が入っています。
const { entry: page, cacheHint } = await getEmDashEntry("pages", "contact");
if (Astro.cache?.enabled) Astro.cache.set(cacheHint);この cacheHint の中身を見ると、こうなっていました。
{ "tags": ["01KRTTRS7F1ATKX9P60SBQS7MB"], "lastModified": "2026-08-07T12:34:56.000Z" }`maxAge` は入っていません。 タグと更新日時だけです。タグはコンテンツを更新したときに該当ページのキャッシュを消すためのもので、保持時間の指定ではありません。
しかし判定は「maxAge または タグ」なので、タグだけでもキャッシュは有効になります。そして有効になった以上、アダプターはヘッダーを付けます。
Cloudflare-CDN-Cache-Control: publicmax-age が付いていません。保持時間を指定しないまま「キャッシュしてよい」とだけ宣言した状態です。
ルールに書くより悪い
ここが厄介なところです。routeRules に書いていれば、こうなります。
Cloudflare-CDN-Cache-Control: public, max-age=300, stale-while-revalidate=864005 分で期限が切れ、次のアクセスで作り直されます。
一方、タグだけで有効になった場合は max-age がありません。保持時間は CDN の既定に委ねられます。外したつもりのページが、明示的にキャッシュしたページより長く残ることになります。
「ルールに書かなければ対象外」という理解は、逆向きに間違っていたわけです。
同じ構成でも外れているページがある
紛らわしいのは、同じ仕組みの別サイトでは検索ページがきちんと外れていたことです。
$ curl -sI https://example.net/search?q=test
cf-cache-status: BYPASSBYPASS は「キャッシュの対象にしない」という状態です。こちらも routeRules には書いていません。
違いはページの中身でした。検索ページは CMS のコンテンツを 1 件ずつ引くのではなく検索 API を叩いているので、`Astro.cache.set()` をそもそも呼んでいません。呼んでいなければタグは積まれず、判定は「対象外」になり、アダプターが no-store を付けます。
つまり外れていた理由は「ルールを書かなかったから」ではなく「そのページが cacheHint を拾っていなかったから」でした。私はこの 1 例を見て「ルールを書かなければ外れる」と一般化し、間違えました。外れているように見えるページがあっても、それは別の理由かもしれません。
外すならページ側で宣言する
routeRules の型には「キャッシュしない」を表す値がありません。指定できるのは maxAge と swr とタグだけです。
確実に外すには、ページ側でこう書きます。
if (Astro.cache?.enabled) Astro.cache.set(false);set(false) はキャッシュを無効の状態にして、ヘッダーの付与を止めます。結果としてアダプターの既定処理が働き、Cloudflare-CDN-Cache-Control: no-store が付きます。
デプロイして確認しました。
$ curl -sI https://example.com/contact
cf-cache-status: BYPASS他のページのキャッシュはそのままです。
maxAge: 0 は「保存しない」ではない
外す方法として maxAge: 0 を試した記録も残しておきます。これは保存はするが毎回オリジンに再検証するという意味です。実測ではこうなります。
1回目 cf-cache-status: MISS
2回目 cf-cache-status: EXPIRED
3回目 cf-cache-status: EXPIRED古い内容が返ることはないので、鮮度の意味では目的を達します。ただし毎回サーバーまで届くので、サーバーの実行を減らす効果はありません。CPU の消費を減らしたくて外すなら、これでは足りません。
直した後にも少し残る
修正をデプロイしても、修正前に保存されたレスポンスはしばらく残ります。私の場合、6 回試して 1 回だけ HIT / age: 424 が返りました。地域ごとに保存場所が分かれているためです。
保持時間を指定しないまま保存されたものなので、CDN の既定で落ちるのを待つことになります。すぐ消したいなら、管理画面からそのページを公開し直すのが確実です。コンテンツを更新すると CMS がタグ経由で該当ページのキャッシュを消すため、その仕組みに乗せられます。
被害がどこで止まるかも書いておきます。修正後は新しく保存されることはないので、影響は「しばらくの間、一部の地域で古い問い合わせページが返り得る」ことだけです。フォームの構造自体は変えていないので、送信は問題なく動きます。
この構成で気をつけること
- `routeRules` は「効かせる」ためのもので、「外す」ためには使えない
- 外すのはページ側の `Astro.cache.set(false)`
- CMS のキャッシュヒントは保持時間を持たないことがある。タグだけでもキャッシュは有効になる
- ページごとに
cf-cache-statusを実際に確認する。ルールの書き方だけを見て判断しない
私はこの穴を、デプロイ後に 1 パスずつヘッダーを確認していて見つけました。設定ファイルを読み直すだけでは絶対に見つからない種類の食い違いです。