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ニックネーム入力・決済・問い合わせ系フォームで使用
決済 UI@stripe/react-stripe-js + @stripe/stripe-jsStripe Elements / Payment Element / Link
サーバー側決済 SDKstripePaymentIntent 作成・Webhook 署名検証
ユーティリティclsx, tailwind-merge, class-variance-authorityshadcn/ui 標準セット
ORM@prisma/client, prismaNeon(取引データ: Vote / Purchase / PurchaseItem)アクセス
Headless CMS SDKmicrocms-js-sdkmicroCMS 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.tslocalStorage に保存し、ページ再訪時に再利用する
  • サーバー状態: 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.ymlpostgres: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.tsimages.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.tsimages.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_SECRET
    • UPSTASH_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 CachemicroCMS API 応答のサーバー側キャッシュ(fetchnext.revalidate オプション経由)
Next.js Image Cachenext/image の最適化結果(リサイズ・WebP変換後)のキャッシュ
ブラウザキャッシュ静的アセット(フォント、JS、CSS)
microCMS CDN画像本体配信

microCMS データのキャッシュ

fetchnext.revalidate で短 TTL(60〜180 秒)を指定する。microcms-js-sdkcustomRequestInit{ 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 Cache60〜180秒時間経過のみ
票数パッケージ一覧(microCMS)Next.js Data Cache60〜180秒時間経過のみ
候補者画像本体microCMS CDN + Vercel Edge + ブラウザmicroCMS が管理URL 変更時
得票数集計(Neon)キャッシュなし
応援者ランキング(Neon)キャッシュなし
投票期間アプリ起動時に環境変数から読み込み再デプロイ時

ユーザー固有データ(ログイン状態など)は本サイトでは存在しないため、固有キャッシュ判定の必要はない。

設計原則

  • データソースごとに鮮度戦略を分ける: コンテンツ系(microCMS)は短 TTL、取引集計(Neon)はキャッシュなし、設定値(ENV)はビルド時固定
  • Webhook 同期は不採用: 「状態を増やして結合と複雑性を下げる」操作は ohbarye 原則の優先順位(状態 > 結合 > 複雑性 > コード量)に反する
  • 画像は microCMS CDN に委譲: 本サイトに自前画像配信エンドポイントを持たない

画像最適化

  • next/image を必須使用。生の <img> タグは禁止
  • 候補者画像は microCMS CDN がオリジン。next.config.tsimages.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 で安定化
  • アニメーションは motiontransform/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.tsheadersdefault-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.paymentIntentId UNIQUE 制約で制御

個人情報

  • 収集最小化: 一般ユーザーから氏名・メールアドレスを収集しない(Stripe Link 連携時のメールは Stripe 側で管理)。サイトが直接受け取るのはニックネームのみ
  • ログマスキング: ログ出力時にカード情報・Stripe メタデータをマスクする
  • 保存期間: 投票履歴は集計後に匿名化扱い、購入履歴は税法上の保存期間(7年)を超えて保持しない

環境変数

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

監査ログ(本番)

  • 決済成功/失敗などの本サイト固有の重要イベントはログに記録し、改竄不能な形で保管する
  • 運営者の認証・コンテンツ操作の監査ログは microCMS が提供する

テスト

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