拡張子なし URL の静的サイトを S3 + CloudFront で配信する
何が問題か
Astro を build.format: 'file' でビルドすると、/about は about.html として出力される。ところが S3 のオリジンはパスをそのままキーとして引くため、ブラウザから /about を叩いても about(拡張子なし)というオブジェクトを探しに行って 404 になる。
S3 のウェブサイトホスティング機能を使えばインデックスドキュメントの補完は効くが、その場合はバケットをパブリックにしなければならず、OAC で CloudFront 経由に限定する構成と両立しない。
viewer-request で書き換える
CloudFront Function を viewer-request に割り当て、リクエストの URI をオリジンに届く前に変える。やることは 3 つだけ。
- 末尾スラッシュは 301 で落とす(URL を 1 つに寄せる)
/は/index.htmlへ- それ以外で拡張子を持たない URI に
.htmlを付ける
ランタイムは cloudfront-js-2.0 を前提にすると const / let / テンプレートリテラルが使える。ただし Node の標準ライブラリが丸ごとあるわけではないので、正規表現と基本的な文字列操作だけで書くのが安全。
踏んだ落とし穴
静的ファイルを通す判定を拡張子の正規表現でやると、コンテンツ側の命名が URL に入るときに壊れる。 例えばタグページを /tech/tags/{tag} で生成していると、Node.js というタグは .js を拡張子とみなされてそのままオリジンに流れ、存在しないオブジェクトとして 404 になる。
対策は 2 つあるが、CMS 側でタグを自由入力にせず選択肢に固定して URL 安全な値だけを許す方が、ビルド時に壊れないだけでなく編集者が迷わない。
キャッシュとデプロイ
ハッシュ付きのアセット(_astro/*)と HTML では寿命が全く違うので、同期を 2 段階に分ける。
- アセットを先に
max-age=31536000, immutableでアップロード - その後 HTML を
max-age=0, must-revalidateで、--delete付きで同期
順序を逆にすると、新しい HTML がまだ存在しないアセットを参照する瞬間ができる。