Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,19 @@ aws --profile ic-gr cloudfront create-invalidation \

`.com` ドメイン側の CloudFront には今回のデプロイでは invalidation を打っていないため、`*.ic-gr.com` でも即時反映したい場合は手動で `--distribution-id E3CUYP7CXV3V06` の invalidation を実行する。

### SPA 直リンク問題は解消済み
### サブパス直リンク / リロード対応

旧 CRA では CloudFront に Custom Error Response 未設定だったため `/overview` などのリロードで 403 を返していたが、Next.js の static export + `trailingSlash: true` で各ルートが `out/overview/index.html` として生成されるため、ハードリロードでも 200 が返る。
`trailingSlash: true` で `out/<route>/index.html` は生成されるが、CloudFront の `DefaultRootObject` はバケットルートにしか効かないため、それだけでは `/company/` などへのリロードで `403 AccessDenied` が返る (S3 REST origin + OAC 構成のため、サブパスのキーが見つからない → 非公開バケットなので 403)。

これを解消するため CloudFront Function `ic-gr-url-rewrite` を viewer-request として紐付けている。コード本体は `infra/cloudfront/url-rewrite.js`、デプロイ手順は同ディレクトリ `README.md` を参照。

```
/company/ → /company/index.html
/company → /company/index.html
/foo.ext → そのまま (拡張子があるリクエストはリライトしない)
```

現在 `.net` (`EHFD30ZL5XZ0U`) のみ紐付け済み。`.com` (`E3CUYP7CXV3V06`) も同じ症状になるため、必要なら同じ関数 (`arn:aws:cloudfront::058264181659:function/ic-gr-url-rewrite`) を viewer-request として紐付ければよい。

## ディレクトリ構成

Expand Down
59 changes: 59 additions & 0 deletions infra/cloudfront/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# CloudFront Function — `url-rewrite`

Next.js static export の `out/<route>/index.html` を、CloudFront のサブパスリクエスト
(例: `/company/`, `/overview/`)に対して正しく解決させるための viewer-request 関数。

S3 (REST origin + OAC) では `DefaultRootObject` がバケットルートにしか効かないため、
これが無いとサブパスへの直リンク/リロードで `403 AccessDenied` になる。

## 関連リソース

- 関数名: `ic-gr-url-rewrite`
- ランタイム: `cloudfront-js-2.0`
- 紐付けイベント: viewer-request
- 対象ディストリビューション:
- `EHFD30ZL5XZ0U` (`*.ic-gr.net`)
- `E3CUYP7CXV3V06` (`*.ic-gr.com`) ※同じ S3 を origin にしているため同じ関数を共用

## 初回デプロイ(CLI 例)

```bash
PROFILE=ic-gr
NAME=ic-gr-url-rewrite

# 1. 関数作成
aws --profile $PROFILE cloudfront create-function \
--name $NAME \
--function-config Comment="Rewrite /foo/ to /foo/index.html for Next.js static export",Runtime=cloudfront-js-2.0 \
--function-code fileb://url-rewrite.js

# 2. publish(DEVELOPMENT → LIVE)
ETAG=$(aws --profile $PROFILE cloudfront describe-function --name $NAME --query 'ETag' --output text)
aws --profile $PROFILE cloudfront publish-function --name $NAME --if-match "$ETAG"

# 3. 各ディストリビューションの DefaultCacheBehavior.FunctionAssociations に紐付け
# (マネコンの方が安全。CLI でやる場合は get-distribution-config → JSON 編集 → update-distribution)

# 4. キャッシュ無効化
aws --profile $PROFILE cloudfront create-invalidation \
--distribution-id EHFD30ZL5XZ0U --paths "/*"
```

## 更新

`url-rewrite.js` を編集したら:

```bash
ETAG=$(aws --profile ic-gr cloudfront describe-function --name ic-gr-url-rewrite --query 'ETag' --output text)
aws --profile ic-gr cloudfront update-function \
--name ic-gr-url-rewrite \
--if-match "$ETAG" \
--function-config Comment="Rewrite /foo/ to /foo/index.html for Next.js static export",Runtime=cloudfront-js-2.0 \
--function-code fileb://url-rewrite.js

ETAG=$(aws --profile ic-gr cloudfront describe-function --name ic-gr-url-rewrite --query 'ETag' --output text)
aws --profile ic-gr cloudfront publish-function --name ic-gr-url-rewrite --if-match "$ETAG"
```

publish 後、各ディストリビューションへの反映は数分かかる。CloudFront Function の
更新だけならディストリビューション側の再デプロイは不要(紐付け済みなら自動で新版が使われる)。
26 changes: 26 additions & 0 deletions infra/cloudfront/url-rewrite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// CloudFront Function (viewer-request) — Next.js static export ルーティング補正
//
// 目的: S3 (REST origin + OAC) で配信している `out/` を、サブディレクトリへの
// 直リンク / リロードでも正しく `index.html` に解決させる。
// CloudFront の DefaultRootObject は `/` にしか効かないので、
// `/company/` → `/company/index.html` のリライトはここで行う。
//
// ルール:
// 1. 末尾が `/` のとき: `index.html` を末尾に付与
// 2. 末尾が `/` でなく拡張子を含まないとき: `/index.html` を付与(末尾スラッシュ無しでも閲覧可)
// 3. 拡張子があるとき (例: /_next/static/foo.js, /favicon.ico): そのまま通す
function handler(event) {
var request = event.request;
var uri = request.uri;

if (uri.endsWith('/')) {
request.uri = uri + 'index.html';
} else {
var lastSegment = uri.substring(uri.lastIndexOf('/') + 1);
if (lastSegment.indexOf('.') === -1) {
request.uri = uri + '/index.html';
}
}

return request;
}
Loading