前段
Next.*でSSGした静的サイトを(CSR後リロード時404問題を最小コストで解決しながら)S3でホスティングする - Qiitaの続きです。
この前回の問題を要約すると、
-
next export
をすると、pages/
配下のコンポーネントは、.html
拡張子がついて書き出される。-
pages/hoge.tsx
であれば/hoge.html
-
- しかし実際にCSRすると(CSR自体は成功するが)URLは
/hoge
になる。 - S3は
/hoge
と/hoge.html
を別のURLとして認識するので、CSR後にURlが/hoge
の状態でリロードすると404になる。- というか本来それらは別で、Webサーバがいい感じに同じルーティングとして扱ってくれたりするサービスもあるのだけど、S3の静的ホスティングにはそういう機能が無い。
解決策として、ビルド後の.html
ファイルから、拡張子を消してS3にアップロードしました。
本題の概要
さて、ここまでは良かったんですが、この状態で新たにURLの末尾にスラッシュをつけると404になるという問題が発生しました。
例えば/hoge
であれば/hoge/
は404になります。
これは、/hoge/
というパスでアクセスされた場合、実際はS3が表示しようとするのは/hoge/index.html
だからです。
そんなファイルは無いので404になります。
じゃあ/hoge/index.html
足せば良いんじゃないの?って気がするんですが、問題は、/hoge
(ファイル)と/hoge/
(ディレクトリ)がファイルシステム上同じディレクトリ配下に共存できないということです。
つまり、前段で/hoge
(拡張子なし)というファイルを作ってしまっているので、当然ながら同じ階層に/hoge/
というディレクトリを作ることは出来ません。
でもスラッシュついてても同じページを表示して欲しいですよね。
※実際にはNext使ってない場合も.html
のないHTMLファイルがある場合は起きる問題だと思います。
今回に関しては、ちょっとハック的な方法で解決しているのでご注意ください。
あと不明点もありつつ実現しています。
本番環境ではそういった事情を理解した上で使ってください〜
(なので、知識として一般形にせずやったことの共有という意味でタイトルを過去形にしています。)
対応
/hoge
と/hoge/
が共存できないと言いましたが、S3では共存出来ます。
S3の「フォルダ」というのは実際にはフォルダではなく、UI上そう見せかけているだけだからですね。
参考:Amazon S3における「フォルダ」という幻想をぶち壊し、その実体を明らかにする | Developers.IO
ということで方針は以下の感じ。
-
next export
の結果(.html
削除済み)をS3にアップロード後、S3上で/hoge/index.html
オブジェクトを作成する。 - その
index.html
オブジェクトに対して末尾スラッシュなしのURLへのリダイレクトを設定する。
ただし一つ注意があって、この対応策を実現できたのは、独自ドメインを使用していて、かつwww.
サブドメインつきのURLを、APEXドメイン(サブドメなしのドメイン)のURLにリダイレクトさせていたからでした。
(つまり https://www.my-domain.com/*
のアクセスはhttps://my-domain.com/*
にリダイレクトされるようにしていた)
もう少し厳密に言うと、静的ホスティングしているバケットに結果的にルーティングされるドメインが複数存在する場合に使えるはずです。
今回も手動でやるのは厳しいので、引き続きシェルスクリプトで書きます。
前回の記事のスクリプトの内容直後から始まるイメージです。
スクリプト
空のindex.htmlを用意して、これをコピーしていきます。
# コピー用の空の index.html を作成
empty_index_html_directory=./out/tmp/
empty_index_html=${empty_index_html_directory}index.html
mkdir -p $empty_index_html_directory
touch $empty_index_html
追加する index.html のパスリストを作成します。
# 追加する index.html のパスリストを作成
# `$html_files`には`.html`拡張子を削る前の対象ファイルのパスが全て含まれています(前記事参照)。
target_index_html_paths=()
for filepath_with_extension in $html_filepaths; do
filepath=${filepath_with_extension%\.html}
index_html_path=$filepath/index.html
target_index_html_paths+=( $index_html_path )
done
上の二つの素材を使って実際にS3にオブジェクトを作成
# Location ヘッダがついた空の index.html オブジェクトを作成
for index_html_path in $target_index_html_paths; do
key=${index_html_path#\.\/out\/}
# ここのドメインをバケットに当てられているドメインとは別のドメインにすることが重要。
# 同じドメインを指定すると正常にリダイレクトできない。
# この値は、例えば `https://my-domain.com/hoge` のように、末尾に`/index.html`がないURLになる。
destination=https://www.my-domain.com/${key%\/index.html}
aws s3api put-object \
--bucket $bucket_name \
--body $empty_index_html \
--key $key \
--website-redirect-location $destination
done
ここでポイントなのは--website-redirect-location
と、$destination
の部分にwww.
つきのURLを指定しているところです。
Website-Redirect-Location
S3のオブジェクトにはメタデータを付与することができますが、その中でもWebsite-Redirect-Location
はリダイレクト先を指定するものです。
リダイレクト先に、バケットに当てられているドメインとは別のドメインを指定する
Website-Redirect-Location
の値として指定している$destination
ですが、ポイントはバケットに当てられているドメインとは別のドメインにすることです。
S3に独自ドメインを当てる場合、バケット名をそのドメインの名前にする必要があります。
この$destination
に指定するドメインは、そのバケット名とは別のドメインということです。
同じドメインを当てるとリダイレクトが上手く行かない
なぜかというと、リダイレクト先をバケット名と同じドメインにすると、なぜか実際のリダイレクト先がカスタムドメインの方ではなくS3のドメインになってしまうからです
例えば今回の静的ホスティングしているドメインとバケット名がmy-domain.com
だとすると、Website-Redirect-Location: https://my-domain.com/hoge
(ドメインが同じ)を指定した場合、実際にリダイレクトされるのはhttp://my-domain.com.s3-website-ap-northeast-1.amazonaws.com/hoge
になります。
...なぜ...?
これよくわかっていないので、分かる方いらっしゃったら教えてください...
そもそもWebsite-Redirect-Location
ってHTTPのLocation
ヘッダそのものではないんですよね。
これをリダイレクト時にS3(もしくは前段に置いているCloudFront?)が中で何らかの処理をかけてLocation
ヘッダに変換しているのだと思いますが、同一バケットの場合は何らかの処理が入るのでしょうね、カスタムドメインが外れます...
ネットで調べても、今回発生している結果の理解の助けになるような記述を発見できませんでした。
とりあえず、ドメインが別であれば動きます。
結果的なリダイレクト状況
これでどういう状態になるかというと、S3のオブジェクトは
-
my-domain.com
(バケット)/hoge
/hoge/index.html
こういう感じになり、/hoge/index.html
というオブジェクトのリダイレクト先はhttps://www.my-domain.com/hoge
になります。
実際にアクセスすると、以下のようなリダイレクトを辿るようになりました。
0. `https://my-domain.com/hoge/` <- ここにユーザーがアクセス、`/hoge`と同じルーティングになって欲しい。
1. `https://www.my-domain.com/hoge` <- `Website-Redirect-Location`での指定通り`www.`つきのURLに飛ぶ。
2. `http://my-domain.com/hoge` <- `www.`つきのURLが一度`http`に向けられるが、`https`へのリダイレクトをかけている。
3. `https://my-domain.com/hoge` <- たどり着いた!
このように3回のリダイレクトを通して目的のURLにたどり着いています。
最終的なスクリプト
# S3にアップロードする処理がある(内容は前段の記事参照)
# 〜省略〜
aws s3 sync ./out/ s3://$bucket_name/ --delete # などなど
# 〜省略〜
# コピー用の空の index.html を作成
empty_index_html_directory=./out/tmp/
empty_index_html=${empty_index_html_directory}index.html
mkdir -p $empty_index_html_directory
touch $empty_index_html
# 追加する index.html のパスリストを作成
# `$html_files`には`.html`拡張子を削る前の対象ファイルのパスが全て含まれています(前記事参照)。
target_index_html_paths=()
for filepath_with_extension in $html_filepaths; do
filepath=${filepath_with_extension%\.html}
index_html_path=$filepath/index.html
target_index_html_paths+=( $index_html_path )
done
# Location ヘッダがついた空の index.html オブジェクトを作成
for index_html_path in $target_index_html_paths; do
key=${index_html_path#\.\/out\/}
# ここのドメインをバケットに当てられているドメインとは別のドメインにすることが重要。
# 同じドメインを指定すると正常にリダイレクトできない。
# この値は、例えば `https://www.my-domain.com/hoge` のように、末尾に`/index.html`がないURLになる。
destination=https://www.my-domain.com/${key%\/index.html}
aws s3api put-object \
--bucket $bucket_name \
--body $empty_index_html \
--key $key \
--website-redirect-location $destination
done
おしまい
ちょっとハック的な感じあるのと、S3(かもしくはCloudFront)の中の仕様がはっきりわかっていない状態でやっているので、何らかのタイミングで動かなくなるとかあるのかなあとか思ったり
ともあれ、これでひとまずNextのSSGをS3にホスティングするのは完了かな。
ただ、現代であればそもそもVercelとかNetlifyとかAmplifyコンソールとかを使う方が良いと思うので、まずはそちらの使用検討をオススメします。