{
  "id": "add-a-pricing-page",
  "type": "playbooks",
  "category": "playbooks",
  "locale": "de",
  "url": "/de/playbooks/add-a-pricing-page",
  "title": "Prompt-to-PR: Eine Preisseite hinzufügen",
  "description": "SOP zum Erstellen einer Produktions-Preisseite mit monatlichem/jährlichem Umschalter, Feature-Matrix und Stripe Checkout-Links in einem Next.js Tailwind-Projekt.",
  "tools": [
    "Cursor",
    "Claude Code",
    "Codex",
    "Windsurf"
  ],
  "stack": [
    "Next.js",
    "TypeScript",
    "Tailwind"
  ],
  "tags": [
    "nextjs",
    "tailwind",
    "typescript"
  ],
  "difficulty": "easy",
  "updated": "2026-06-08",
  "markdown": "Eine der am häufigsten generierten Seiten in KI-Codierungssitzungen. Dieses Playbook zwingt den Agenten, echte, eingebundene Stripe Checkout-Links zu erstellen, anstatt Platzhalter-Buttons.\n\n## 1. Anforderung\n\nFüge eine `/pricing`-Seite mit zwei oder drei Tarifen, einem monatlichen/jährlichen Abrechnungsumschalter (jährlich gibt 20% Rabatt), einer Funktionsvergleichsmatrix und Stripe Checkout-Integration hinzu. Preise müssen aus Umgebungsvariablen stammen, nicht in JSX hartcodiert sein.\n\n## 2. Erster Prompt\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. Erwartete Dateiänderungen\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. Review-Checkliste\n\n- `STRIPE_SECRET_KEY` wird nie in einem Client-Komponenten importiert oder über `NEXT_PUBLIC_` offengelegt.\n- Der Checkout-Session verwendet `mode: \"subscription\"` nicht `mode: \"payment\"` für wiederkehrende Abrechnung.\n- Jährliche Preis-IDs sind separate Stripe-Preisobjekte – kein berechneter Rabatt im Code.\n- Der Umschalter zeigt jährliche Ersparnisse deutlich an (z.B. „20% sparen“).\n- Das `PLANS`-Array ist die einzige Quelle der Wahrheit – die Funktionsmatrix verwendet dieselben Daten, nicht eine separate hartcodierte Tabelle.\n- Fehlerbehandlung beim Checkout-POST: Gibt einen 500 mit einer Nachricht zurück, wenn Stripe einen Fehler wirft.\n- Kein `console.log` von Stripe-Schlüsseln oder Kundendaten im Code.\n\n## 5. Testbefehle\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. Häufige Fehler\n\n- **`STRIPE_SECRET_KEY` clientseitig durchgesickert** – Agent importiert `stripe` in `PricingCards.tsx`. Verschiebe alle Stripe-Aufrufe in die API-Route.\n- **Jährlicher Umschalter zeigt denselben Preis wie monatlich** – Agent berechnet `price * 0.8` vergisst aber, den `isAnnual`-Zustand zu verwenden. Bestätige, dass der Umschalterzustand an die Preisanzeige übergeben wird.\n- **Checkout gibt 500 mit „No such price“ zurück** – Preis-ID-Umgebungsvariable nicht gesetzt oder falsche Umgebung (Test vs. Live). Überprüfe mit `stripe prices retrieve <id>` in der Stripe CLI.\n- **`success_url` ist relativ** – Stripe benötigt eine absolute URL. Füge `NEXT_PUBLIC_APP_URL` voran.\n\n## 7. Fehlerbehebungsprompt\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. PR-Beschreibung\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```"
}