静的サイトの公開は「フォルダを上げる」だけではない:キャッシュ、圧縮、正直な 404
静的ホスティングの罠はほぼ全部レスポンスヘッダーにあります。永久にキャッシュしてよいファイル、絶対にしてはいけないファイル、そして静的サイトが SPA 式フォールバックを使ってはいけない理由。
静的サイトにはバックエンドが無いので、ある錯覚を生みます。ビルドして dist/ を上げれば終わり、という錯覚です。実際には壊れる場所はほぼ全部レスポンスヘッダーです。キャッシュ方針、圧縮、ステータスコード。npm run dev では一切見えず、Nginx を前に置いて初めて露出します。
成果物は実は 3 種類
まず dist/ を分けます。キャッシュ方針が違うからです。
| 種類 | 例 | 名前は変わるか |
|---|---|---|
| 内容アドレス型 | _astro/index.B3eZ-5oA.css |
変わる。名前に内容ハッシュが入る |
| HTML | zh/blog/design-tokens/index.html |
変わらない。URL もファイル名も固定 |
| 固定名アセット | favicon.ico、og-image.png、logo-mark-64.png |
変わらない |
以下の話はすべてこの表から導かれます。
キャッシュ:名前が変わるかで 2 段階
ファイル名に内容ハッシュが入っているものだけが
immutableを受け取れます。
_astro/ の下のハッシュ付きは永久にキャッシュできます。
location /_astro/ {
# 名前に内容ハッシュが入る。中身が変われば名前も変わる
add_header Cache-Control "public, max-age=31536000, immutable";
}
HTML は正反対です。/zh/blog/design-tokens/ という URL は決して変わりません。ここに長いキャッシュを付けると、記事を直しても再訪者は古い版を見続けます。しかも更新させる手段がありません。URL は変わらず、ファイル名も変わらず、etag も変わっていない可能性があるからです。
location / {
try_files $uri $uri/index.html $uri.html =404;
# 毎回確認する。名前が固定で中身が変わる唯一のもの
add_header Cache-Control "no-cache";
}
no-cache は「キャッシュしない」ではなく「使う前に毎回聞く」です。etag と組み合わせれば、変わっていなければ 304 でほぼ通信量ゼロ、変われば即座に反映されます。一番間違えやすく、間違えたときの被害が一番大きいのがここです。
固定名の画像やアイコンは厄介な位置にあります。ハッシュが無いので immutable を付ければ「ロゴを差し替えたのに古いものが出る」が確定します。短い TTL にするか、参照側にバージョンを付けます。私は短い TTL にしています。ロゴを替える頻度は記事を直す頻度よりずっと低いからです。
圧縮:テキストは圧縮、画像は絶対に圧縮しない
gzip on;
gzip_static on; # ビルド済みの .gz を配る。リクエストごとに CPU を使わない
gzip_types text/css application/javascript application/json
image/svg+xml application/xml;
gzip_vary on;
2 点だけ強調します。
- 画像を圧縮しない。 JPEG / PNG / WebP / AVIF / WOFF2 はすでに圧縮されたコンテナです。gzip してもほぼ変わらず、CPU だけ食います。
gzip_staticを優先。 ビルド時に.gzを作っておき、そのファイルを返すだけにします。完全に静的なら、リクエスト経路で同じ仕事を繰り返す理由がありません。
Brotli は gzip より 15〜20% 小さく、モジュールが増える代償があります。テキスト主体なら入れる価値がありますが、gzip_static がある前提では最適化であって必須ではありません。
404:SPA の習慣で唯一真似してはいけないもの
SPA 向けの Nginx テンプレートにはたいていこれが入っています。
# SPA フォールバック:どのパスも index.html へ
try_files $uri /index.html;
これを静的サイトにコピーすると事故になります。 /zh/blog/typo/ が 200 でホームページの中身を返します。
- 検索エンジンが存在しない URL を無限に有効なページとして取り込む(ソフト 404)
- 読者は正しくクリックしたと思い込み、ただ「少し変なページ」だと感じる
- 404 ページは永遠に見られない
静的サイトの実際のルートはビルド時に確定しているので、正解は正直に 404 を返すことです。
error_page 404 /404.html;
location / {
try_files $uri $uri/index.html $uri.html =404;
}
404 を返せるサイトは、自分がどのページを持っているかを知っています。これは「全部ホームページにフォールバック」に対する、静的生成の実質的な優位です。
CSP と、インラインでしか成立しないあのスクリプト
静的サイトの安全性はたいてい簡単です。サーバーもデータベースもセッションも無いので、unsafe-inline が要る場面はほとんどありません。
例外が 1 つあります。初回描画前のスクリプトはインラインでなければならないのです。テーマを決め、言語入口ページを表示するかどうかを最初の描画前に決める——これはバンドルされた type="module" に任せられません。module は defer で、実行される頃にはページは描かれています。そしてインラインスクリプトと厳格な CSP は本質的に衝突します。
出口は 2 つ。
- そのスクリプトのハッシュを
script-srcに入れる。ただしスクリプトを変えるたびにハッシュが変わり、ビルドと Nginx 設定を同期させ続ける必要があります。 script-src 'unsafe-inline'を受け入れ、CSP の重心をframe-ancestors・object-src・base-uriなどインラインが弱められない部分に置く。
私は 2 を選びました。ここにはユーザーデータもセッションも無いので、CSP が守るのは自分のスクリプトではなく第三者の注入です。
ドメインが隠れている 2 か所
静的サイトに実行時の設定ストアはありません。つまりドメインは成果物にコンパイルされます。このサイトでは 2 か所です。
astro.config.mjsのsite(sitemap・canonical・RSS の絶対 URL)src/lib/site.tsのフォールバックドメイン
ドメインを変えたら両方を直し、再ビルドが必要です。Nginx の server_name を変えるだけでは足りません。成果物の canonical は古いドメインのままです。
静的ホスティングは設定を実行時からビルド時へ移します。代償は、設定変更が再公開になることです。
公開前に一度通したいチェックリスト
-
_astro/はimmutable、HTML はno-cache - テキストは
gzip_static有効、画像は無効 - 存在しないパスは 404 を返し、ホームページにフォールバックしない
-
/404.htmlが実在し、ナビゲーションもある -
robots.txtとsitemap.xmlのドメインが新しい - ドメイン変更後に再ビルドした

コメント
…