yoheisuda.me
- Role
- 設計・実装・運用(すべて)
- Period
- 2026-04–17
- Stack
- Nuxt 4
- Nuxt Content v3
- Tailwind CSS v4
- SST v4
- AWS Lambda
- CloudFront
- Route 53
- Site
- https://yoheisuda.me
yoheisuda.me
この個人ポートフォリオ兼ブログそのもの。Nuxt 4 + Nuxt Content v3 で組んで、SST v4.7.5 で AWS に乗せている。ビルド時に書き上がる静的資産は S3/CloudFront 経由で、SSR は Lambda で回る構成。
Architecture
ざっくり全体像。
graph LR
U["User"] --> CF["CloudFront"]
CF -->|"static"| S3["S3 Bucket<br/>(public assets)"]
CF -->|"SSR"| L["Lambda<br/>(Nuxt server)"]
L --> DB[("SQLite<br/>/tmp/contents.sqlite")]
L -. "SQL dump" .- B[/"Bundled<br/>SQL dump"/]
B --> DB
R53["Route 53"] --> CF
ACM["ACM (us-east-1)"] --> CF
- CloudFront が窓口。静的アセット(
/_nuxt/*, 画像等)は S3 へ、それ以外は Lambda へルーティング - Lambda は Nitro の
aws-lambdapreset でビルドした Nuxt サーバ。コールドスタート時に/tmpに SQLite DB を再構築してから返す - Route 53 + ACM は SST 設定時に既存を参照(新規発行・ゾーン作成なし)
Request Lifecycle
トップページにアクセスしたときの流れ。
sequenceDiagram
participant U as User
participant CF as CloudFront
participant L as Lambda
participant Dump as Bundled dump
participant DB as /tmp/*.sqlite
U->>CF: GET /
alt cache hit
CF-->>U: cached HTML
else cache miss
CF->>L: invoke SSR
Note over L: cold start?
alt DB not built
L->>Dump: fetch sql_dump.txt (in-handler)
Dump-->>L: SQL statements
L->>DB: replay INSERTs
end
L->>DB: SELECT path = '/'
DB-->>L: page row
L-->>CF: rendered HTML
CF-->>U: HTML
end
Content v3 は SQLite ファイルをバンドルせず、ビルド時に生成した SQL ダンプを Lambda 内部の Nitro route で返す設計になっている。初回リクエスト時に /tmp に INSERT し直す。結果、read-only な Lambda FS でも動く。
Stage Separation
SST の stage で本番と検証を分離。
graph TB
subgraph personal["Stage: personal"]
PA["Lambda"] --> PB["CloudFront (auto URL)"]
end
subgraph production["Stage: production"]
QA["Lambda"] --> QB["CloudFront"]
QB --> QC["yoheisuda.me (Route 53 A record)"]
QB --- QD["ACM cert (existing)"]
end
Dev["git push / local"] --> CMD["npx sst deploy --stage ..."]
CMD --> personal
CMD --> production
personalは検証用。ドメインなし、CloudFront が自動発行する URL で動作確認してから本番に上げるproductionはyoheisuda.me。既存 Route 53 ホストゾーンと既存 ACM(us-east-1)を再利用するようsst.config.tsのdomainにcertとdnsを指定
Hurdles
実装中に踏んだ落とし穴。
1. Runtime.HandlerNotFound: index.handler
sst.aws.Nuxt が期待する Lambda ハンドラ形式で Nitro が出力されず、index.mjs に export default E だけがある状態だった(node-server preset の出力)。nuxt.config.ts で明示:
nitro: {
preset: 'aws-lambda',
}
これで export const handler が生え、index.handler で解決できるようになる。
2. Content v3 全ページ 504 → 404
504 の原因は Lambda 起動失敗(上と同じ)、504 解消後に全ページ 404 になった二次症状。Content v3 のデフォルト SQLite パスは ./contents.sqlite(= /var/task/contents.sqlite)で、Lambda FS は read-only なのでオープンできない。
content: {
database: {
type: 'sqlite',
filename: '/tmp/contents.sqlite',
},
}
/tmp は Lambda で書き込み可能な唯一の場所。ここに逃がすとコールドスタート時にダンプから再構築できる。
3. Route 53 / CloudFront の既存レコード衝突
既存 CloudFront distribution に yoheisuda.me が Alternate Domain Name として残っていて CNAMEAlreadyExists。同じドメインは 1 つの distribution にしか付けられないので、旧 distribution から先に外す必要がある。さらに Route 53 の既存 A レコードも邪魔するのでコンソールから削除してから再 deploy。
Design System
- Brutalist geometric を採用: Space Grotesk + JetBrains Mono、2px 純黒縁、角丸なし、Vermilion
#ff3d00を唯一のアクセント - モノクロ主体のなかで、Featured バッジや active nav、
::selectionだけに accent を使う - Hero は
clamp()+ staggeredfade-upで開くが、それ以外の micro-interaction は最小限
ソース: 本サイトそのもの。記事の末尾にあるこれを含め、Markdown 1 枚と YAML フロントマターで全プロジェクトページが成立している。