Skip to content

技術スタック

フレームワーク・ランタイム

  • Next.js 15+ (App Router) — SSR/SSG/ISR、Server Components、Route Handlers を活用
  • React 19 — Server Components + Client Components の使い分け前提
  • TypeScript 5+strict: truenoUncheckedIndexedAccess: true 推奨
  • Node.js 20 LTS 以上

スタイリング

  • Tailwind CSS v4 — Figma Make のデモが v4 を採用しているため踏襲
  • CSS変数によるデザイントークン--gold, --gold-light, --gold-dark, --rose-gold, --ivory, --charcoal, --font-display, --font-bodyglobals.css:root で定義
  • shadcn/ui — Radix UI ベースの UI コンポーネント群(Dialog, Button, Slider 等)。Figma Make 出力の components/ui/* をそのまま components/ui/ に移植
  • Webフォントnext/font/google 経由で Cormorant Garamond(display)と Jost(body)を読み込む

主要ライブラリ

用途ライブラリ備考
アニメーションmotion (旧 framer-motion)12.x 系
アイコンlucide-react0.487.0+
画像スライダーreact-slick + slick-carousel候補者詳細のメインスライダー。SSRに注意("use client" 必須)
Confetti演出motion<motion.div> ループで実装(デモと同様)軽量なため canvas-confetti を使わず代替可
フォームreact-hook-form + zod決済・問い合わせ系フォームで使用
ユーティリティclsx, tailwind-merge, class-variance-authorityshadcn/ui 標準セット
ORM@prisma/client, prismaDB アクセス
CSV読み込みcsv-parseSeedスクリプトで使用

ルーティング (App Router)

Figma Make のデモは react-router を使用しているが、Next.js 移植時はファイルベースルーティングに置き換える。

app/
  layout.tsx           # 全体レイアウト(背景・装飾・Provider)
  page.tsx             # Home(候補者一覧)
  candidate/
    [id]/
      page.tsx         # 候補者詳細
  ranking/
    page.tsx           # ランキング

状態管理

  • クライアント状態: React Context API(AuthContext, VotingContext)を app/providers.tsx に集約
  • サーバー状態: 将来 API 連携時は TanStack Query または Server Actions を採用
  • 永続化(将来): useState の代わりに localStorage ラッパー or サーバーDB

データレイヤー

  • 現行: PostgreSQL + Prisma に移行済み(下記「本番段階」の構成で稼働中)
    • スキーマ: prisma/schema.prisma(User, Candidate, CandidateImage, Vote, Purchase 等)
    • Seed: prisma/seed.ts、CSV+画像からの一括投入は scripts/seed-candidates.ts
    • ローカルDB: docker-compose.ymlpostgres:17 を起動(mwj / mwj_dev / miss_world_japan)
  • 本番段階:
    • DB: PostgreSQL 15+(全データを集約)
    • ORM: Prisma — スキーマ駆動でマイグレーションも一元管理
    • マネージドDB: Neon を採用(Branching機能を活用しPRごとに独立した環境を提供)
    • 画像保存: PostgreSQL の bytea 型でDB内にBLOBデータとして保存(候補者画像は CandidateImage テーブル)
    • 管理画面: 自前実装(/admin 配下)。候補者・画像のCRUDを提供
    • 初期データ投入: CSV + 画像ファイルから Seedスクリプトで一括投入
    • 投票数の集計は DB クエリで実施し、必要に応じて集計結果をキャッシュ
    • データアクセスは Server Actions または Route Handler 経由で実施
    • 楽観的更新(useOptimistic)でUI反応性を高める

画像配信の方針

候補者画像はDB内BLOBとして保持し、専用エンドポイント(/api/images/[id])経由で配信する。

観点仕様
配信エンドポイントGET /api/images/[id]
キャッシュヘッダーCache-Control: public, max-age=31536000, immutable
キャッシュバストURLパラメータ ?v={updatedAt} で画像更新時に自動更新
next/image との連携同一ドメインなので remotePatterns 設定不要、自動最適化が機能する
認証公開エンドポイント(URLはUUIDで推測困難)

CDN/ブラウザキャッシュが効くため、初回以外は通常の外部ストレージ配信と同等のパフォーマンスとなる。

データモデルの要件・テーブル設計は specs/data-model/ を参照。 管理画面の要件・設計は specs/admin/ を参照。

認証

  • 現行: NextAuth.js (Auth.js) v5 で実装済み
    • 設定: ルート直下の auth.ts(認証ハンドラ + 設定をエクスポート)
    • Route Handler: app/api/auth/[...nextauth]/route.ts
    • 管理画面ログイン: app/admin/login/page.tsx(Credentials Provider 想定)
    • パスワードハッシュ: bcrypt
  • 本番: 上記に加え Facebook Provider + Instagram Provider を追加予定
    • セッションは JWT or DB セッション
    • middleware.ts で投票エンドポイント保護

決済

  • デモ: StripePaymentModal でカード番号入力 → setTimeout で成功演出
  • 本番: Stripe Checkout (Hosted) または Stripe Elements + Payment Intents
    • Webhook で支払い確認 → 投票券(クレジット)を加算

ディレクトリ構成

app/
  (routes)/
    page.tsx
    candidate/[id]/page.tsx
    ranking/page.tsx
  api/
    vote/route.ts            # 将来: 投票エンドポイント
    checkout/route.ts        # 将来: Stripe決済セッション
  layout.tsx
  globals.css
  providers.tsx
components/
  ui/                        # shadcn/ui (Button, Dialog, etc.)
  layout/                    # Header, Footer, ScrollToTop
  candidate/                 # CandidateCard, CandidateGrid, ImageSlider
  modals/                    # SocialLoginModal, PurchaseCreditsModal, StripePaymentModal
  voter/                     # VoterRanking
lib/
  contexts/                  # AuthContext, VotingContext
  data/                      # candidates.ts, voters.ts (デモ用)
  utils/                     # cn(), date helpers
  types/                     # Candidate, Voter, User 型定義
public/
  fonts/, images/

コーディング規約

  • Server Components がデフォルト。インタラクションが必要な場合のみ "use client" を付与
  • クライアントコンポーネントcomponents/ 配下にまとめ、ファイル先頭に "use client" を明示
  • 画像next/image を使用(candidate.image などの外部URLは next.config.tsimages.remotePatterns で許可)
  • リンクnext/link<Link href="..."> を使用
  • CSS変数を直接使う場合は style={{ color: 'var(--gold)' }} のままで可(デモのスタイルを踏襲)
  • 環境変数.env.local に配置し、クライアント参照には NEXT_PUBLIC_ プレフィックスを付ける

デプロイ

  • Vercel を第一候補(Next.js最適化)
  • 環境変数: NEXTAUTH_SECRET, STRIPE_SECRET_KEY, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, FACEBOOK_CLIENT_ID/SECRET, INSTAGRAM_* など

パフォーマンス・最適化(全画面共通)

各画面の requirements.md で個別に記載しないでよい、サイト全体の共通方針を定める。

Core Web Vitals 目標

  • LCP: 2.5秒以内
  • INP: 200ms以内
  • CLS: 0.1以下

レンダリング戦略

  • Server Components がデフォルト。クライアント側のインタラクションが必要な部分のみ "use client" で切り出す
  • 静的生成(SSG)を優先: 候補者一覧・候補者詳細のように更新頻度が低いページは generateStaticParams() で全パターンを事前生成する
  • ISR: 動的に変わるデータは revalidate オプションで再生成する(後述のキャッシュポリシー参照)
  • 動的データ(投票後のリアルタイム表示)はクライアント側で楽観的更新する

キャッシュポリシー(全画面共通)

DB クエリの結果と画像配信エンドポイントの応答を、Next.js のキャッシュ機構と CDN で保持し、ほぼすべてのリクエストを高速応答させる。

採用するキャッシュ層

用途
Vercel CDN(Edge Network)静的HTML・画像配信エンドポイント・最適化済み画像のエッジキャッシュ
Next.js Data Cache (unstable_cache)DBクエリ結果のサーバー側キャッシュ
Next.js Image Cachenext/image の最適化結果(リサイズ・WebP変換後)のキャッシュ
ブラウザキャッシュ画像本体・静的アセット(フォント、JS、CSS)

unstable_cache の挙動

DBクエリ結果をタグ付きでキャッシュする。

ts
import { unstable_cache } from 'next/cache';

export const getCandidates = unstable_cache(
  async () => prisma.candidate.findMany({ /* ... */ }),
  ['candidates'],                                // キャッシュキー
  { revalidate: 600, tags: ['candidates'] },     // 10分キャッシュ + タグ付け
);

キャッシュが更新されるタイミング:

  1. revalidate の時間経過後の最初のアクセス — そのリクエストには古い値が即座に返り、裏で再生成される(stale-while-revalidate)
  2. revalidateTag('tagName') の明示的呼び出し — 管理画面での編集後など任意のタイミングで即時無効化
  3. revalidatePath('/path') の呼び出し — ページ単位で無効化
  4. デプロイ — 新ビルドで完全クリア
  5. キャッシュ容量超過 — LRU方式で古いエントリから破棄

データ別キャッシュ設定

データ種別revalidateタグ無効化トリガー
候補者の表示用情報600秒(10分)candidates管理画面での編集時に revalidateTag('candidates')
投票券パッケージ一覧3600秒(1時間)credit-packages運営による変更時に手動 revalidateTag
候補者ごとの得票数集計30秒vote-countsrevalidate時間経過のみ
応援者貢献度ランキング60秒voter-rankingsrevalidate時間経過のみ
ユーザー固有データキャッシュしない

画像配信のキャッシュ設定

候補者画像は /api/images/[id] 経由で配信する。Route Handler のレスポンスヘッダーで CDN/ブラウザキャッシュを最大限活用する。

設定効果
Cache-Controlpublic, max-age=31536000, immutable1年間キャッシュ、ブラウザは絶対に再リクエストしない
ETag"{imageId}-{updatedAt}"内容識別
URL設計?v={updatedAt} をクエリで付与画像更新時にURLが変わり、新しいキャッシュエントリを生成

これにより、初回アクセス以降はDB に到達せず、CDN/ブラウザキャッシュから即時応答する。実質的に外部ストレージ配信と同等のパフォーマンス。

設計原則

  • 編集頻度が低いデータほど revalidate を長く: 候補者情報は10分(編集時に revalidateTag で即時無効化可能)
  • 編集頻度が高いデータは revalidate を短く: 投票数は30秒程度で擬似的なリアルタイム感を出す
  • ユーザー固有のデータはキャッシュしない: ログイン状態・自分の投票履歴など、ユーザーごとに異なる値はリクエストごとに取得
  • 更新時にタグ無効化できるものは長めに設定して問題ない: 編集者が反映を待つことなく、ユーザーは常に新しい値を見られる
  • 画像はURLパラメータでキャッシュバスト: ファイル更新時は ?v= の値が変わるためURLが変わり、新しいキャッシュエントリが作られる

キャッシュ無効化のフロー

運営者が管理画面で候補者編集

Server Action で DB更新

revalidateTag('candidates')

次のアクセスで DB再取得 → 新しい値でキャッシュ
運営者が画像差し替え

CandidateImage の updatedAt が更新される

画像URL `?v={updatedAt}` の値が変わる

ブラウザは新しいURLとして再取得 → 新しいキャッシュエントリ

画像最適化

  • next/image を必須使用。生の <img> タグは禁止

画像最適化

  • next/image を必須使用。生の <img> タグは禁止
  • 候補者画像は DB BLOB を /api/images/[id] 経由で配信(同一ドメインなので remotePatterns 設定不要)
  • 画像更新時のキャッシュバストは URLクエリ ?v={updatedAt} で対応
  • above-the-fold(初期表示領域)の画像priority 属性を付与して優先読み込みする
    • 候補者詳細画面のメイン画像
    • home画面の最初の数枚の候補者カード画像
  • 候補者画像は適切な sizes を指定してレスポンシブに最適化
  • next/image の自動WebP/AVIF変換・リサイズ・lazy loading を活用

フォント最適化

  • next/font/google 経由で Webフォントを読み込む(FOIT/FOUT を抑制し CLS を防止)
  • フォントのプリロードは Next.js が自動で実施

バンドル最適化

  • 重いクライアントライブラリ(react-slick, motion など)は使用画面のみで動的 import を検討
  • shadcn/ui は使用するコンポーネントのみコピーして含める(不要なものは取り込まない)

レンダリング最適化

  • 候補者カードなどリスト要素は React.memo 化、コールバックは useCallback で安定化
  • アニメーションは motiontransform/opacity ベースで GPU 加速を活用

セキュリティポリシー(全画面共通)

各 spec の requirements.md で個別に記載しないでよい、サイト全体の共通セキュリティポリシーを定める。

認証・セッション管理

  • OAuth 2.0 + PKCE フロー必須: ソーシャルログインは PKCE (Proof Key for Code Exchange) フローを使用する
  • state パラメータ検証: OAuth リダイレクト時のCSRF攻撃を防ぐため、state パラメータを生成・検証する
  • HttpOnly Cookie でセッション管理: アクセストークン・セッションIDは HttpOnly, Secure, SameSite=Lax 属性付きCookieで管理する
  • クライアント側にトークンを露出させない: アクセストークンはサーバー側のみで保持し、localStorage / sessionStorage には保存しない
  • NextAuth.js (Auth.js) を採用: 上記のベストプラクティスを標準提供

CSRF 対策

  • CSRF トークン: 状態を変更するすべての Server Action / Route Handler は CSRF トークンを検証する
  • NextAuth.js のミドルウェアまたは next-csrf などで実装

入力検証

  • すべての外部入力を検証: クライアント側の検証だけでなく、サーバー側でも zod などのスキーマバリデーションで再検証する
  • SQLインジェクション対策: ORM (Prisma など) を使用し、生のクエリ文字列結合を禁止
  • XSS 対策: React のデフォルトエスケープに依存し、dangerouslySetInnerHTML を原則禁止。やむを得ず使う場合は DOMPurify でサニタイズ

通信

  • HTTPS 必須: 本番環境のすべての通信は HTTPS で行う(Vercel デプロイ時はデフォルト)
  • CSP (Content Security Policy) 設定: next.config.tsheadersdefault-src 'self' を基本に必要なドメインを許可
  • HSTS ヘッダー: ブラウザに HTTPS を強制する Strict-Transport-Security を設定

レート制限・不正対策

  • 投票エンドポイント: 同一ユーザー/IPからの過剰な投票リクエストを Upstash Rate Limit などでブロック
  • 時刻判定はサーバー側: 「1日1票」のような時刻依存ロジックはクライアント時刻に依存させず、サーバー側で判定する
  • 重複防止: 投票・決済はサーバー側で冪等キーを用いて重複処理を防止する

決済

  • PCI DSS スコープ最小化: カード情報はサーバーに保存せず、Stripe Elements / Checkout を使ってクライアント側でトークン化する(SAQ-A 対応)
  • Webhook 署名検証: Stripe からの Webhook は stripe.webhooks.constructEvent で署名検証を行う
  • Webhook 冪等性: 同じ payment_intent.succeeded を重複処理しないよう、冪等キーで制御

個人情報

  • 収集最小化: 氏名・メールアドレス以外の個人情報は原則収集しない
  • ログマスキング: ログ出力時にメールアドレス・カード情報などをマスクする
  • 保存期間: 投票履歴は集計後に匿名化、購入履歴は税法上の保存期間(7年)を超えて保持しない

環境変数

  • シークレット管理: API キー・OAuth シークレットは Vercel の環境変数に保存し、リポジトリにコミットしない
  • クライアント露出: NEXT_PUBLIC_ プレフィックスが付くものは公開前提として扱い、シークレットは絶対に付与しない

監査ログ(本番)

  • 認証失敗・決済成功/失敗・管理者操作などの重要イベントをログに記録し、改竄不能な形で保管する

テスト

  • 単体: Vitest + Testing Library
  • E2E: Playwright(投票フロー・決済フローを優先)