WorkFeatured

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
yoheisuda.me runtime components / click to zoom
  • CloudFront が窓口。静的アセット(/_nuxt/*, 画像等)は S3 へ、それ以外は Lambda へルーティング
  • Lambda は Nitro の aws-lambda preset でビルドした 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
Cold start replays the dump, warm requests hit /tmp directly / click to zoom

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
Same IaC, two isolated deploy targets / click to zoom
  • personal は検証用。ドメインなし、CloudFront が自動発行する URL で動作確認してから本番に上げる
  • productionyoheisuda.me。既存 Route 53 ホストゾーンと既存 ACM(us-east-1)を再利用するよう sst.config.tsdomaincertdns を指定

Hurdles

実装中に踏んだ落とし穴。

1. Runtime.HandlerNotFound: index.handler

sst.aws.Nuxt が期待する Lambda ハンドラ形式で Nitro が出力されず、index.mjsexport 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() + staggered fade-up で開くが、それ以外の micro-interaction は最小限

ソース: 本サイトそのもの。記事の末尾にあるこれを含め、Markdown 1 枚と YAML フロントマターで全プロジェクトページが成立している。

END © 2026 Yohei Suda
yoheisuda.me