テーマ
技術スタック
フレームワーク・ランタイム
- Next.js 15+ (App Router) — SSR/SSG/ISR、Server Components、Route Handlers を活用
- React 19 — Server Components + Client Components の使い分け前提
- TypeScript 5+ —
strict: true、noUncheckedIndexedAccess: true推奨 - Node.js 20 LTS 以上
スタイリング
- Tailwind CSS v4 — Figma Make のデモが v4 を採用しているため踏襲
- CSS変数によるデザイントークン —
--gold,--gold-light,--gold-dark,--rose-gold,--ivory,--charcoal,--font-display,--font-bodyをglobals.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-react | 0.487.0+ |
| 画像スライダー | react-slick + slick-carousel | 候補者詳細のメインスライダー。SSRに注意("use client" 必須) |
| Confetti演出 | motion の <motion.div> ループで実装(デモと同様) | 軽量なため canvas-confetti を使わず代替可 |
| フォーム | react-hook-form + zod | ニックネーム入力・決済・問い合わせ系フォームで使用 |
| 決済 UI | @stripe/react-stripe-js + @stripe/stripe-js | Stripe Elements / Payment Element / Link |
| サーバー側決済 SDK | stripe | PaymentIntent 作成・Webhook 署名検証 |
| ユーティリティ | clsx, tailwind-merge, class-variance-authority | shadcn/ui 標準セット |
| ORM | @prisma/client, prisma | Neon(取引データ: Vote / Purchase / PurchaseItem)アクセス |
| Headless CMS SDK | microcms-js-sdk | microCMS API クライアント。Server Component から直接呼び出す |
| レート制限 | @upstash/ratelimit + @upstash/redis | 決済エンドポイントの IP レート制限 |
ルーティング (App Router)
Figma Make のデモは react-router を使用しているが、Next.js 移植時はファイルベースルーティングに置き換える。本サイトは公開画面のみを保有し、運営者向けの app/admin/ 配下は持たない(運営者向け UI は microCMS の管理画面で提供される)。
app/
layout.tsx # 全体レイアウト(背景・装飾・Provider)
page.tsx # Home(候補者一覧)
candidate/
[id]/
page.tsx # 候補者詳細
ranking/
page.tsx # ランキング状態管理
- クライアント状態: React Context API および軽量フックを
app/providers.tsxに集約。本サイトはログイン状態を一切管理しない(運営者認証は microCMS、一般ユーザーは認証なし) - ニックネーム永続化:
features/voting/hooks/useNickname.tsでlocalStorageに保存し、ページ再訪時に再利用する - サーバー状態: Server Components / Server Actions で都度取得する。コンテンツ系は microCMS、取引系は Neon を一次情報源とする
データレイヤー
データソースは 3 系統に分かれる。それぞれの責務を分離し、自前で持つ状態を最小化する方針。
| 配置 | データ種別 | 採用理由 |
|---|---|---|
| microCMS(Headless CMS) | コンテンツ系: 候補者、候補者画像、票数パッケージ | 運営者向け管理 UI・認証・権限・監査・画像 CDN を全て委譲。自前管理画面を保有しない |
| Neon(マネージド PostgreSQL) | 取引系: 投票、決済、購入明細 | Stripe との一次連携・トランザクション境界・履歴保持に DB が必要 |
| Vercel 環境変数 | 投票期間(VOTING_PERIOD_START / VOTING_PERIOD_END) | サイト全体で唯一の値。テーブル + シングルトン制約を排除 |
microCMS(コンテンツ系)
- SDK:
microcms-js-sdkを使用し、Server Component から直接 fetch する - ラッパー:
features/cms/services/にgetCandidates/getCandidate/getActiveCreditPackagesを薄く配置 - API キー: 読み取り専用権限のものを
MICROCMS_API_KEY_READONLYで保持(書き込み権限キーはアプリケーションに混入させない) - バリデーション層: Zod は不採用(本プロジェクトの寿命内に発生するスキーマ drift リスクが小さいため)
- 業務ルール(
creditPackagesのイミュータブル制約等)は microCMS のロール / フィールド権限で強制する
Neon(取引系)
- ORM: Prisma — スキーマ駆動でマイグレーションを一元管理
- スキーマ範囲:
Vote/Purchase/PurchaseItemの 3 エンティティのみ(Candidate/CandidateImage/CreditPackage/VotingPeriod/AdminUserは撤回) - Branching 機能を活用し PR ごとに独立した環境を提供
- ローカル DB:
docker-compose.ymlでpostgres:17を起動 - データアクセス: Server Components / Server Actions / Route Handler 経由
Vercel 環境変数(投票期間)
- 開始日時・終了日時を ISO 8601(タイムゾーン付き)で保持
- 変更時は環境変数を更新して再デプロイ。シングルトン制約付きテーブルや自前管理画面は持たない
越境参照の整合性
Neon の Vote / Purchase / PurchaseItem は microCMS のコンテンツ ID(候補者・票数パッケージ)を文字列で参照する。DB レベルの外部キー制約は持たず、整合性は以下で吸収する:
- microCMS 側で物理削除を運用上禁止(フィールド権限 + 公開状態「停止」での実質非表示で対応)
- 集計時に microCMS から取得した公開済みコンテンツとの照合
画像配信の方針
候補者画像は microCMS の画像フィールドとして保持され、microCMS CDN から配信される。本サイトに自前画像配信エンドポイントは持たない。
| 観点 | 仕様 |
|---|---|
| オリジン | microCMS CDN |
| エッジキャッシュ | Vercel Edge Network + Next.js Image Cache |
| クライアント表示 | next/image 経由、next.config.ts の images.remotePatterns に microCMS 画像ホストを追加 |
| キャッシュバスト | microCMS が画像更新時に URL を変更するため自動的に切り替わる |
データモデルの要件・テーブル設計は specs/data-model/ を参照。
認証
本サイトの一般ユーザー(投票者)・運営者ともに、本サイト側でのログイン機能は 持たない。
- 一般ユーザー: 認証なし。投票時にニックネームを入力する
- 運営者: microCMS のメンバー機能で認証・ロール管理・操作監査を行う。本サイト側には運営者ログイン画面・運営者セッション・パスワードハッシュ・アカウントロック機構を持たない
- 過去仕様(NextAuth.js Credentials Provider + bcrypt +
AdminUserテーブル + Upstash でのログインレート制限)は全て撤回
決済
- 本番: Stripe Payment Element + Stripe Link 優先 + カードフォールバック
- Payment Element には Link, カード, Apple Pay, Google Pay を候補表示し、Link 登録済みユーザーはワンクリック決済、未登録ユーザーはそのままカード入力に進める
- PCI DSS スコープ最小化のため、PAN は Stripe Elements が直接 Stripe へ送信し、当アプリの DOM には流入させない
- PaymentIntent 1 件で 複数パッケージの組み合わせ合計金額 を一括決済する
- Webhook で支払い確認 → 該当候補者へ合計票数分の Vote を即時記録(残数として保留しない)
ディレクトリ構成
app/
(routes)/
page.tsx
candidate/[id]/page.tsx
ranking/page.tsx
api/
webhooks/stripe/route.ts # Stripe Webhook
actions/
purchase.ts # PaymentIntent 作成等の Server Actions
layout.tsx
globals.css
providers.tsx
components/
ui/ # shadcn/ui (Button, Dialog, etc.)
public/ # 投票サイト側コンポーネント
features/
voting/ # 投票成立後の UI 制御・ニックネーム永続化
candidates/ # 候補者表示用ロジック(microCMS から取得)
payment/ # 票数パッケージ取得・購入フロー
cms/ # microCMS クライアントラッパー (services 中心)
lib/
prisma.ts # Prisma Client 共有インスタンス (Neon)
microcms.ts # microCMS Client 共有インスタンス
date.ts # JST 日付ユーティリティ
voting-period.ts # 環境変数からの投票期間取得
utils.ts # cn() 等
public/
fonts/, images/app/admin/ components/admin/ features/admin/ app/api/auth/[...nextauth]/ app/api/images/[id]/ auth.ts は 保有しない(運営者向け管理機能を microCMS に委譲し、画像配信を microCMS CDN に委譲したため)。
コーディング規約
- Server Components がデフォルト。インタラクションが必要な場合のみ
"use client"を付与 - クライアントコンポーネントは
components/配下にまとめ、ファイル先頭に"use client"を明示 - 画像は
next/imageを使用(candidate.imageなどの外部URLはnext.config.tsのimages.remotePatternsで許可) - リンクは
next/linkの<Link href="...">を使用 - CSS変数を直接使う場合は
style={{ color: 'var(--gold)' }}のままで可(デモのスタイルを踏襲) - 環境変数は
.env.localに配置し、クライアント参照にはNEXT_PUBLIC_プレフィックスを付ける
デプロイ
- Vercel を第一候補(Next.js最適化)
- 環境変数(代表):
DATABASE_URL— Neon 接続MICROCMS_SERVICE_DOMAIN— microCMS のサービスドメインMICROCMS_API_KEY_READONLY— microCMS 読み取り専用 API キーVOTING_PERIOD_START/VOTING_PERIOD_END— 投票期間(ISO 8601、タイムゾーン付き)STRIPE_SECRET_KEY/NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY/STRIPE_WEBHOOK_SECRETUPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN— 決済エンドポイントのレート制限
- 過去仕様の
NEXTAUTH_SECRET/ADMIN_INITIAL_EMAIL/ADMIN_INITIAL_PASSWORDは撤回
パフォーマンス・最適化(全画面共通)
各画面の requirements.md で個別に記載しないでよい、サイト全体の共通方針を定める。
Core Web Vitals 目標
- LCP: 2.5秒以内
- INP: 200ms以内
- CLS: 0.1以下
レンダリング戦略
- Server Components がデフォルト。クライアント側のインタラクションが必要な部分のみ
"use client"で切り出す - 静的生成(SSG)を優先: 候補者一覧・候補者詳細のように更新頻度が低いページは
generateStaticParams()で全パターンを事前生成する - ISR: 動的に変わるデータは
revalidateオプションで再生成する(後述のキャッシュポリシー参照) - 動的データ(投票後のリアルタイム表示)はクライアント側で楽観的更新する
キャッシュポリシー(全画面共通)
データソースごとに鮮度要求にマッチした戦略を分け、Next.js のキャッシュ機構と CDN で多層キャッシュする。
採用するキャッシュ層
| 層 | 用途 |
|---|---|
| Vercel CDN(Edge Network) | 静的HTML・最適化済み画像のエッジキャッシュ |
| Next.js Data Cache | microCMS API 応答のサーバー側キャッシュ(fetch の next.revalidate オプション経由) |
| Next.js Image Cache | next/image の最適化結果(リサイズ・WebP変換後)のキャッシュ |
| ブラウザキャッシュ | 静的アセット(フォント、JS、CSS) |
| microCMS CDN | 画像本体配信 |
microCMS データのキャッシュ
fetch の next.revalidate で短 TTL(60〜180 秒)を指定する。microcms-js-sdk の customRequestInit に { next: { revalidate: 120 } } を渡すことで Next.js Data Cache に乗る。
ts
// 例: features/cms/services/candidateService.ts
import { client } from '@/lib/microcms';
export async function getCandidates() {
return client.getList({
endpoint: 'candidates',
customRequestInit: { next: { revalidate: 120 } },
});
}Webhook による即時 revalidate は採用しない: 配送失敗時に永続的な不整合状態(state++)が生まれることを避けるため、短 TTL の自動 revalidate のみに依存する。
Neon 集計のキャッシュ
Vote 集計(得票数・応援者ランキング)はキャッシュしない。Server Component で都度 Prisma 集計を行う。短期開催・候補者数 10 名前後のため、集計負荷は無視できる。得票数キャッシュテーブルは作らない(状態増加を避ける)。
データ別キャッシュ設定
| データ種別 | キャッシュ層 | revalidate | 無効化 |
|---|---|---|---|
| 候補者情報(microCMS) | Next.js Data Cache | 60〜180秒 | 時間経過のみ |
| 票数パッケージ一覧(microCMS) | Next.js Data Cache | 60〜180秒 | 時間経過のみ |
| 候補者画像本体 | microCMS CDN + Vercel Edge + ブラウザ | microCMS が管理 | URL 変更時 |
| 得票数集計(Neon) | キャッシュなし | — | — |
| 応援者ランキング(Neon) | キャッシュなし | — | — |
| 投票期間 | アプリ起動時に環境変数から読み込み | — | 再デプロイ時 |
ユーザー固有データ(ログイン状態など)は本サイトでは存在しないため、固有キャッシュ判定の必要はない。
設計原則
- データソースごとに鮮度戦略を分ける: コンテンツ系(microCMS)は短 TTL、取引集計(Neon)はキャッシュなし、設定値(ENV)はビルド時固定
- Webhook 同期は不採用: 「状態を増やして結合と複雑性を下げる」操作は ohbarye 原則の優先順位(状態 > 結合 > 複雑性 > コード量)に反する
- 画像は microCMS CDN に委譲: 本サイトに自前画像配信エンドポイントを持たない
画像最適化
next/imageを必須使用。生の<img>タグは禁止- 候補者画像は microCMS CDN がオリジン。
next.config.tsのimages.remotePatternsに microCMS の画像ホストを追加する - 画像更新時のキャッシュバストは microCMS が画像 URL を変更することで自動的に切り替わる
- 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で安定化 - アニメーションは
motionのtransform/opacityベースで GPU 加速を活用
セキュリティポリシー(全画面共通)
各 spec の requirements.md で個別に記載しないでよい、サイト全体の共通セキュリティポリシーを定める。
運営者認証
- 本サイトは運営者認証を保有しない。運営者の認証・ロール管理・パスワード保存・操作監査は microCMS のメンバー機能 に完全委譲する
- 本サイト側に運営者セッション Cookie・パスワードハッシュ(bcrypt)・アカウントロック機構・ログイン Server Action は持たない
- 過去仕様の NextAuth.js Credentials Provider +
AdminUserテーブルは撤回
一般ユーザーの保存値(ニックネーム)
- 一般ユーザーは認証セッションを持たない。ニックネームのみ
localStorageに保存する - ニックネームは個人を特定する性質を持たない任意文字列として扱い、Cookie には保存せずサーバーへも自動送信しない
- サーバーへのニックネーム送信は投票/購入リクエストの本文(JSON)に明示的に含める形のみ
- サーバー側で文字数バリデーション(最小1文字・最大32文字程度)と XSS 対策の文字種制限を行う
CSRF 対策
- CSRF トークン: 状態を変更する Server Action / Route Handler は CSRF トークンを検証する(Next.js Server Actions の標準保護で実質的に対応、必要に応じて
next-csrf等で補強) - Stripe Webhook は署名検証で実質的に検証する
入力検証
- すべての外部入力を検証: クライアント側の検証だけでなく、サーバー側でも
zodなどのスキーマバリデーションで再検証する - SQLインジェクション対策: ORM (Prisma など) を使用し、生のクエリ文字列結合を禁止
- XSS 対策: React のデフォルトエスケープに依存し、
dangerouslySetInnerHTMLを原則禁止。やむを得ず使う場合は DOMPurify でサニタイズ。ニックネームの表示も React のエスケープに任せる
通信
- HTTPS 必須: 本番環境のすべての通信は HTTPS で行う(Vercel デプロイ時はデフォルト)
- CSP (Content Security Policy) 設定:
next.config.tsのheadersでdefault-src 'self'を基本に必要なドメインを許可 - HSTS ヘッダー: ブラウザに HTTPS を強制する
Strict-Transport-Securityを設定
レート制限・不正対策
- 決済エンドポイント: 同一 IP からの過剰な PaymentIntent 作成リクエストを
Upstash Rate Limitでブロック - 運営者ログイン: 本サイト側に存在しないため対象外(microCMS 側で管理)
- 重複防止: 決済は Stripe
paymentIntentIdの UNIQUE 制約で重複処理を防止する
決済
- PCI DSS スコープ最小化: カード情報はサーバーに保存せず、Stripe Elements / Payment Element を使ってクライアント側でトークン化する(SAQ-A 対応)
- Stripe Link 利用: Link を Payment Element の優先候補として表示する設定とし、Link 未登録ユーザーにはカード入力経路を提供する
- Webhook 署名検証: Stripe からの Webhook は
stripe.webhooks.constructEventで署名検証を行う - Webhook 冪等性: 同じ
payment_intent.succeededを重複処理しないよう、Purchase.paymentIntentIdUNIQUE 制約で制御
個人情報
- 収集最小化: 一般ユーザーから氏名・メールアドレスを収集しない(Stripe Link 連携時のメールは Stripe 側で管理)。サイトが直接受け取るのはニックネームのみ
- ログマスキング: ログ出力時にカード情報・Stripe メタデータをマスクする
- 保存期間: 投票履歴は集計後に匿名化扱い、購入履歴は税法上の保存期間(7年)を超えて保持しない
環境変数
- シークレット管理: API キー・Stripe シークレットは Vercel の環境変数に保存し、リポジトリにコミットしない
- クライアント露出:
NEXT_PUBLIC_プレフィックスが付くものは公開前提として扱い、シークレットは絶対に付与しない
監査ログ(本番)
- 決済成功/失敗などの本サイト固有の重要イベントはログに記録し、改竄不能な形で保管する
- 運営者の認証・コンテンツ操作の監査ログは microCMS が提供する
テスト
- 単体: Vitest + Testing Library
- E2E: Playwright(投票フロー・決済フローを優先)