Next.jsでSSGした静的サイトを(CSR後リロード時404問題を最小コストで解決しながら)S3でホスティングする


概要

Next.jsでSSGした静的サイトをS3にデプロイする場合、基本的には next export の結果をバケットに入れれば良いんですが、何も考慮せずにただそうすると、CSRしたページでリロードした際に404になります。

他のAWSサービスの導入は管理コスト増加の観点で避けたく、ここではS3とシェルスクリプトだけでなんとかする方法を共有します。
(ただし、シェルスクリプトはawsコマンドに依存しています)

S3デプロイ時に起きる問題

(実際はS3以外のホスティングサービスでも起きますが)

next export はページを .html の拡張子つきで書き出す(pages/hoge.tsxであればout/hoge.htmlのように)のですが、何も考慮しないとURLが https://hoge.com/hoge.html のように、 .html つきになってしまいます。

これ何が問題かというと、該当ページにNextのCSRをするとURLは https://hoge.com/hoge.htmlなし)になるので、この状態でリロードすると404になってしまうのです。

脱線だけど

Amplifyでデプロイするとこの問題はおきません。
Amplifyを使うことに懸念が無いのならオススメします。

Next.jsでSSGしたサイトをAmplifyでホスティングする - Qiita

対策

  1. next export で出来る .html なファイルから拡張子を取り除く。
  2. S3に配置した後に、拡張子を取り除いたHTMLファイルに content-type: "text/html" を付与する。

これらを手で行いたくないので、シェルスクリプトを書きます。

1. next export で出来るHTMLファイルから拡張子を取り除く。

そもそもCSRが.htmlをつけないのなら、ファイル名から.htmlを削れば良いですよね。
でも、index.htmlに関してはどちらにしろURLに含まれないし、削らなくていっか。ということで

next export

html_filepaths=$(find ./out -name "*.html" ! -path "*/index.html")
for filepath in $html_filepaths; do
  mv $filepath ${filepath%\.html}
done

こんな感じで、index.html以外のxxx.htmlファイルから拡張子を消します。
next exportの出力先がout/の場合)

2. S3に配置した後に、拡張子を取り除いたHTMLファイルに content-type: "text/html" を付与する。

ただし、.htmlを削ったファイルには、Content-Typeヘッダがつきません。
そうすると、ブラウザでそこにアクセスした際にそれがHTMLだと認識されず、ブラウザのダウンロードが実行されてしまいます。

なので、上で拡張子を削ったHTMLファイル(S3のオブジェクト)全てに、Content-Type: "text/html"をつけます。

# 拡張子を削ったファイルのファイル名を回して、S3上のオブジェクトに対してContent-Typeを付与する
for filepath in $html_filepaths; do
  path=${filepath#\.\/out\/}
  key=${path%\.html}

  aws s3api copy-object \
    --bucket $bucket_name \
    --copy-source $bucket_name/$key \
    --key $key \
    --metadata-directive "REPLACE" \
    --content-type "text/html"
done

ここでawsコマンドを使っているので無い場合はインストール必要です。

CSR後リロード時404問題についてはこれで解決です。

デプロイスクリプト

上記の対策込みのデプロイスクリプトはこんな感じです。
実際は環境の振り分けとかその他細かい処理がもっとあるけどその辺はここでは省いてます。

# next exportの実行
npm run export

# 趣旨と関係ないけど、これら消さずにS3に上げるとブラウザからダウンロードできちゃうので消す
find ./out -name ".DS_Store" | xargs rm
find ./out -name ".keep" | xargs rm

# .html なファイルから拡張子を取り除く
html_filepaths=$(find ./out -name "*.html" ! -path "*/index.html")
for filepath in $html_filepaths; do
  mv $filepath ${filepath%\.html}
done

# S3にsync(アップロード)
$bucket_name=hoge_bucket
aws s3 sync ./out/ s3://$bucket_name/ --delete

# Content-Type付与する
for filepath in $html_filepaths; do
  path=${filepath#\.\/out\/}
  key=${path%\.html}

  aws s3api copy-object \
    --bucket $bucket_name \
    --copy-source $bucket_name/$key \
    --key $key \
    --metadata-directive "REPLACE" \
    --content-type "text/html"
done

おしまい

ページが何階層もあるサイトで試してないので、ダメだったらいい感じに直してください🚀

追記

続きとしてこちらの末尾スラッシュ404問題への対応があります。

Next.jsのSSGサイトをS3に静的ホスティングした際に起きる末尾スラッシュ404問題を低コストで解決した - Qiita