{
  "id": "add-a-pricing-page",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "fr",
  "url": "/fr/playbooks/add-a-pricing-page",
  "title": "Prompt-to-PR: Ajouter une page de tarification",
  "description": "SOP pour générer une page de tarification de production avec bascule mensuelle/annuelle, matrice de fonctionnalités et liens Stripe Checkout dans un projet Next.js Tailwind.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "L'une des pages les plus générées dans les sessions de codage IA. Ce playbook contraint l'agent à produire de véritables liens Stripe Checkout câblés plutôt que des boutons factices.\n\n## 1. Exigence\n\nAjouter une page `/pricing` avec deux ou trois niveaux, une bascule de facturation mensuelle/annuelle (annuelle donne une réduction de 20 %), une matrice de comparaison des fonctionnalités et une intégration Stripe Checkout. Les prix doivent provenir de variables d'environnement, ne pas être codés en dur dans JSX.\n\n## 2. Première invite\n\n```txt title=\"First Prompt\"\nAdd a /pricing page to this Next.js 15 App Router project with Tailwind.\n\nRequirements:\n1. Create `src/app/pricing/page.tsx` — a Server Component that renders\n   `<PricingCards>`.\n2. Create `src/components/PricingCards.tsx` — a Client Component with:\n   - Monthly/annual toggle (useState). Annual prices are monthly × 0.8.\n   - Three tiers: Hobby (free), Pro, Business. Read plan data from\n     `src/config/pricing.ts` — not hardcoded in JSX.\n   - A feature matrix table below the cards showing which features each tier\n     includes. Use checkmarks and dashes; do not use emojis in code.\n   - A \"Get started\" CTA for Hobby (links to /sign-up) and \"Subscribe\" for\n     paid tiers.\n3. Create `src/config/pricing.ts` exporting a `PLANS` array. Each plan has:\n   { id, name, monthlyPriceUsd, stripePriceIdMonthly, stripePriceIdAnnually,\n     features: string[], highlighted: boolean }\n   Read Stripe price IDs from env vars:\n     NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY\n     NEXT_PUBLIC_STRIPE_PRICE_PRO_ANNUALLY\n     NEXT_PUBLIC_STRIPE_PRICE_BIZ_MONTHLY\n     NEXT_PUBLIC_STRIPE_PRICE_BIZ_ANNUALLY\n4. Create `src/app/api/checkout/route.ts` (POST). Accept `{ priceId, userId }`.\n   Create a Stripe Checkout Session (mode: \"subscription\") with:\n   - success_url: NEXT_PUBLIC_APP_URL + /dashboard?checkout=success\n   - cancel_url: NEXT_PUBLIC_APP_URL + /pricing\n   Return `{ url }`.\n5. In PricingCards, the Subscribe button POSTs to /api/checkout and redirects\n   to the returned url.\n6. Install `stripe` package. Use env var STRIPE_SECRET_KEY.\n```\n\n## 3. Modifications de fichiers attendues\n\n```txt\npackage.json                             (stripe)\nsrc/app/pricing/page.tsx                 (new — Server Component shell)\nsrc/components/PricingCards.tsx          (new — Client Component with toggle)\nsrc/config/pricing.ts                    (new — plan definitions)\nsrc/app/api/checkout/route.ts            (new — Stripe Checkout Session)\n.env.local.example                       (STRIPE_* and NEXT_PUBLIC_* vars)\n```\n\n## 4. Liste de vérification\n\n- `STRIPE_SECRET_KEY` n'est jamais importé dans un composant client ni exposé via `NEXT_PUBLIC_`.\n- La session Checkout utilise `mode: \"subscription\"` et non `mode: \"payment\"` pour la facturation récurrente.\n- Les IDs de prix annuels sont des objets Stripe Price séparés — pas une remise calculée dans le code.\n- La bascule montre clairement les économies annuelles (par exemple « Économisez 20 % »).\n- Le tableau `PLANS` est la source unique de vérité — la matrice de fonctionnalités réutilise les mêmes données, pas un tableau codé en dur séparé.\n- Gestion des erreurs sur le POST checkout : renvoie une erreur 500 avec un message si Stripe échoue.\n- Aucun `console.log` des clés Stripe ou des données client laissé dans le code.\n\n## 5. Commandes de test\n\n```bash\nbun dev\n\n# Visit http://localhost:3000/pricing\n# Toggle monthly/annual — confirm prices update correctly\n# Confirm annual = monthly * 0.8\n\n# Test checkout endpoint with a test price ID\ncurl -X POST http://localhost:3000/api/checkout \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"priceId\":\"price_test_xxx\",\"userId\":\"user_1\"}' | jq .url\n# Expect a Stripe Checkout URL\n\nbun tsc --noEmit\n```\n\n## 6. Échecs courants\n\n- **`STRIPE_SECRET_KEY` divulgué côté client** — l'agent importe `stripe` dans `PricingCards.tsx`. Déplacez tous les appels Stripe vers la route API.\n- **La bascule annuelle affiche le même prix que mensuel** — l'agent calcule `price * 0.8` mais oublie d'utiliser l'état `isAnnual`. Confirmez que l'état de la bascule est passé à l'affichage du prix.\n- **Checkout renvoie 500 avec « No such price »** — la variable d'environnement de l'ID de prix n'est pas définie ou le mauvais environnement (test vs live). Vérifiez avec `stripe prices retrieve <id>` dans la Stripe CLI.\n- **`success_url` est relative** — Stripe nécessite une URL absolue. Préfixez avec `NEXT_PUBLIC_APP_URL`.\n\n## 7. Invite de correction\n\n```txt title=\"Fix Prompt\"\nThe annual/monthly toggle renders correctly visually but the Subscribe button\nalways sends the monthly priceId regardless of which toggle state is active.\n\nIn PricingCards.tsx, the `priceId` passed to the checkout POST must be\n`isAnnual ? plan.stripePriceIdAnnually : plan.stripePriceIdMonthly`.\nCheck that the isAnnual state variable is read at the point the button's\nonClick handler calls the checkout API.\n```\n\n## 8. Description de la PR\n\n```md title=\"PR description\"\n## Feature: Pricing page with Stripe Checkout\n\n- `/pricing` page with monthly/annual toggle and three tiers (Hobby, Pro, Business)\n- Feature matrix driven from `src/config/pricing.ts` — one source of truth\n- POST `/api/checkout` creates a Stripe Subscription Checkout Session\n- Price IDs read from env vars — no hardcoded Stripe IDs in code\n- Annual plan = 20% discount via separate Stripe Price objects\n\n**Required env vars**: `STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PRICE_*`,\n`NEXT_PUBLIC_APP_URL` (see `.env.local.example`)\n```"
}