Prompt-to-PR: 添加定价页面
在Next.js Tailwind项目中生成包含月/年切换、功能矩阵和Stripe Checkout链接的生产级定价页面的标准操作流程。
CursorClaude CodeCodexWindsurf Next.jsTypeScriptTailwind
AI编码会话中最常生成的页面之一。本指南约束代理生成真实的、连接到Stripe Checkout的链接,而非占位按钮。
1. 需求
添加一个/pricing页面,包含两到三个层级、月/年计费切换(年度享20%折扣)、功能对比矩阵以及Stripe Checkout集成。价格必须来自环境变量,而非硬编码在JSX中。
2. 初始提示
Add a /pricing page to this Next.js 15 App Router project with Tailwind.
Requirements:1. Create `src/app/pricing/page.tsx` — a Server Component that renders `<PricingCards>`.2. Create `src/components/PricingCards.tsx` — a Client Component with: - Monthly/annual toggle (useState). Annual prices are monthly × 0.8. - Three tiers: Hobby (free), Pro, Business. Read plan data from `src/config/pricing.ts` — not hardcoded in JSX. - A feature matrix table below the cards showing which features each tier includes. Use checkmarks and dashes; do not use emojis in code. - A "Get started" CTA for Hobby (links to /sign-up) and "Subscribe" for paid tiers.3. Create `src/config/pricing.ts` exporting a `PLANS` array. Each plan has: { id, name, monthlyPriceUsd, stripePriceIdMonthly, stripePriceIdAnnually, features: string[], highlighted: boolean } Read Stripe price IDs from env vars: NEXT_PUBLIC_STRIPE_PRICE_PRO_MONTHLY NEXT_PUBLIC_STRIPE_PRICE_PRO_ANNUALLY NEXT_PUBLIC_STRIPE_PRICE_BIZ_MONTHLY NEXT_PUBLIC_STRIPE_PRICE_BIZ_ANNUALLY4. Create `src/app/api/checkout/route.ts` (POST). Accept `{ priceId, userId }`. Create a Stripe Checkout Session (mode: "subscription") with: - success_url: NEXT_PUBLIC_APP_URL + /dashboard?checkout=success - cancel_url: NEXT_PUBLIC_APP_URL + /pricing Return `{ url }`.5. In PricingCards, the Subscribe button POSTs to /api/checkout and redirects to the returned url.6. Install `stripe` package. Use env var STRIPE_SECRET_KEY.3. 预期的文件变更
package.json (stripe)src/app/pricing/page.tsx (new — Server Component shell)src/components/PricingCards.tsx (new — Client Component with toggle)src/config/pricing.ts (new — plan definitions)src/app/api/checkout/route.ts (new — Stripe Checkout Session).env.local.example (STRIPE_* and NEXT_PUBLIC_* vars)4. 审查清单
STRIPE_SECRET_KEY绝不会在客户端组件中导入,也不会通过NEXT_PUBLIC_暴露。- Checkout Session 使用
mode: "subscription"而非mode: "payment"以实现定期计费。 - 年度价格ID是单独的Stripe Price对象——而不是代码中计算的折扣。
- 切换开关清晰显示年度节省金额(例如“节省20%”)。
PLANS数组是单一数据源——功能矩阵复用相同数据,而不是单独的硬编码表格。- 结账POST的错误处理:如果Stripe抛出异常,返回包含消息的500状态码。
- 代码中没有留下
console.log输出Stripe密钥或客户数据。
5. 测试命令
bun dev
# Toggle monthly/annual — confirm prices update correctly# Confirm annual = monthly * 0.8
# Test checkout endpoint with a test price IDcurl -X POST http://localhost:3000/api/checkout \ -H "Content-Type: application/json" \ -d '{"priceId":"price_test_xxx","userId":"user_1"}' | jq .url# Expect a Stripe Checkout URL
bun tsc --noEmit6. 常见失败
STRIPE_SECRET_KEY泄露到客户端 —— 代理在PricingCards.tsx中导入stripe。将所有Stripe调用移至API路由。- 年度切换显示与月度相同的价格 —— 代理计算
price * 0.8但忘记使用isAnnual状态。确认切换状态已传递给价格显示。 - 结账返回500错误“No such price” —— 价格ID环境变量未设置或环境错误(测试 vs 生产)。使用Stripe CLI的
stripe prices retrieve <id>验证。 success_url是相对路径 —— Stripe要求绝对URL。请以NEXT_PUBLIC_APP_URL为前缀。
7. 修复提示
The annual/monthly toggle renders correctly visually but the Subscribe buttonalways sends the monthly priceId regardless of which toggle state is active.
In PricingCards.tsx, the `priceId` passed to the checkout POST must be`isAnnual ? plan.stripePriceIdAnnually : plan.stripePriceIdMonthly`.Check that the isAnnual state variable is read at the point the button'sonClick handler calls the checkout API.8. PR描述
## Feature: Pricing page with Stripe Checkout
- `/pricing` page with monthly/annual toggle and three tiers (Hobby, Pro, Business)- Feature matrix driven from `src/config/pricing.ts` — one source of truth- POST `/api/checkout` creates a Stripe Subscription Checkout Session- Price IDs read from env vars — no hardcoded Stripe IDs in code- Annual plan = 20% discount via separate Stripe Price objects
**Required env vars**: `STRIPE_SECRET_KEY`, `NEXT_PUBLIC_STRIPE_PRICE_*`,`NEXT_PUBLIC_APP_URL` (see `.env.local.example`)