静的サイトの公開は「フォルダを上げる」だけではない:キャッシュ、圧縮、正直な 404

静的ホスティングの罠はほぼ全部レスポンスヘッダーにあります。永久にキャッシュしてよいファイル、絶対にしてはいけないファイル、そして静的サイトが SPA 式フォールバックを使ってはいけない理由。

静的サイトにはバックエンドが無いので、ある錯覚を生みます。ビルドして dist/ を上げれば終わり、という錯覚です。実際には壊れる場所はほぼ全部レスポンスヘッダーです。キャッシュ方針、圧縮、ステータスコード。npm run dev では一切見えず、Nginx を前に置いて初めて露出します。

成果物は実は 3 種類

まず dist/ を分けます。キャッシュ方針が違うからです。

種類 名前は変わるか
内容アドレス型 _astro/index.B3eZ-5oA.css 変わる。名前に内容ハッシュが入る
HTML zh/blog/design-tokens/index.html 変わらない。URL もファイル名も固定
固定名アセット favicon.icoog-image.pnglogo-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 つ。

  1. そのスクリプトのハッシュscript-src に入れる。ただしスクリプトを変えるたびにハッシュが変わり、ビルドと Nginx 設定を同期させ続ける必要があります。
  2. script-src 'unsafe-inline' を受け入れ、CSP の重心を frame-ancestorsobject-srcbase-uri などインラインが弱められない部分に置く。

私は 2 を選びました。ここにはユーザーデータもセッションも無いので、CSP が守るのは自分のスクリプトではなく第三者の注入です。

ドメインが隠れている 2 か所

静的サイトに実行時の設定ストアはありません。つまりドメインは成果物にコンパイルされます。このサイトでは 2 か所です。

  • astro.config.mjssite(sitemap・canonical・RSS の絶対 URL)
  • src/lib/site.ts のフォールバックドメイン

ドメインを変えたら両方を直し、再ビルドが必要です。Nginx の server_name を変えるだけでは足りません。成果物の canonical は古いドメインのままです。

静的ホスティングは設定を実行時からビルド時へ移します。代償は、設定変更が再公開になることです。

公開前に一度通したいチェックリスト

  • _astro/immutable、HTML は no-cache
  • テキストは gzip_static 有効、画像は無効
  • 存在しないパスは 404 を返し、ホームページにフォールバックしない
  • /404.html が実在し、ナビゲーションもある
  • robots.txtsitemap.xml のドメインが新しい
  • ドメイン変更後に再ビルドした

← 記事一覧に戻る

コメント