案例研究 1:一个多语言落地页 + 支付页
想法
一位朋友在卖一份售价 29 美元、面向移居韩国的外籍人士的 PDF 指南。她想要一个单页,全球加载都快,会说英语、韩语和中文,并且能在她完全不碰服务器的情况下收取银行卡支付。
Spec
在写任何代码之前,我们把 spec 控制在了一段话:
Build a single-page marketing site for a $29 digital product.
Three languages (en/ko/zh), auto-detected from the browser but
switchable with a toggle. One "Buy" button that opens Stripe
Checkout and redirects to a thank-you page on success. No backend
server I have to maintain. Must load fast from anywhere.
注意这段 spec 没有说什么:没有框架,没有设计系统,没有分析统计,没有邮件收集。这其中每一项都本可以是一个合理的功能,而每一项也都会拖慢首次上线。一段话 spec 的纪律,大半是把东西留在外面的纪律。
技术栈
"没有我要维护的后端"加上"全球都快",直接指向了在边缘 CDN(Content Delivery Network,内容分发网络——把你的文件复制成多份存放在世界各地的数据中心里,于是每位访客都从就近的一份加载)上的静态托管,外加一个用于唯一需要密钥的事情——创建 Stripe ——的微型 serverless 函数。我们选择了在 Cloudflare Pages 上的一个静态站点,配上一个单独的 Pages Function。整个架构一句话就能装下——一个静态页调用一个函数、那个函数再跟 Stripe 对话——而正是这份简单,让它后来变得可调试。
整个系统就是三个盒子,而其中只有中间那个会接触到密钥:
┌─────────────┐ POST /api/checkout ┌──────────────┐ 密钥 ┌────────┐
│ 静态页面 │─────────────────────▶│ PAGES │─────────────▶│ STRIPE │
│ (HTML/JS) │◀─────────────────────┤ FUNCTION │◀─────────────┤ │
│ en/ko/zh │ 结账 URL │ (唯一处理 │ 会话 URL └────────┘
└─────────────┘ │ 密钥的一侧) │
从 CDN 边缘提供 └──────────────┘
关键的 prompt
我们从宽泛处起步,让 AI 提议结构:
Scaffold a static landing page (plain HTML/CSS/JS, no framework)
with a language toggle for en/ko/zh. Store the copy in a single
JS object keyed by language. Detect the default from
navigator.language. Keep it to one index.html plus one main.js.
然后是支付路径,密钥就住在这里:
Add a Cloudflare Pages Function at /api/checkout that creates a
Stripe Checkout session for a single $29 product and returns the
redirect URL. Read STRIPE_SECRET_KEY from the environment, never
hardcode it. The Buy button should POST to this function and then
window.location to the returned URL.
"never hardcode it"这一句配得上它的位置。任由它按默认行事,AI 会很乐意把一个占位密钥内联进去,好让示例"跑起来",而占位密钥往往会变成一个真上线的真密钥。在 prompt 里大声说一次,比在评审里抓出来要便宜。
障碍
第一次线上测试失败了:Buy 按钮毫无反应,浏览器控制台显示一个 CORS(Cross-Origin Resource Sharing,跨源资源共享——浏览器关于哪些站点被允许调用哪些站点的规则)报错。我们没有去猜。我们把确切的报错粘了回去:
Clicking Buy logs: "Access to fetch at '/api/checkout' blocked
by CORS policy." The function and page are on the same domain.
Here's the function code: [pasted]. What's actually wrong?
AI 立刻发现了它:这个函数确实在以 (JavaScript Object Notation,JavaScript 对象表示法——程序之间用来交换结构化数据的一种简单、易读的文本格式)形式返回 Stripe ,但 fetch 在函数的 OPTIONS 预检(preflight)被处理之前就发起了,因为我们误把静态页和这个函数部署到了两个不同的 Pages 项目里。真正的修复是部署拓扑,而不是代码。我们把函数移进了同一个项目的 /functions 目录,报错就消失了。
这个教训值得多停留一下:症状在代码里(一个来自 fetch 的 CORS 报错),但病因在基础设施里(两个项目而不是一个)。如果我们没有把配置粘上去,只让 AI"修这个 CORS 报错",它就会硬塞上 Access-Control-Allow-Origin 头——一个能编译通过、把症状糊住、却让真 bug 继续活着的修复。把字面的报错连同它周围的上下文一起粘上去,并且如果第一个貌似合理的解释跟你的实际配置对不上,就别接受它。
上线
我们把正式的 Stripe key 作为加密的环境变量加进了 Pages 控制面板(绝不进仓库),把她域名的 (Domain Name System,域名系统,互联网的地址簿,把一个名字映射到一台服务器)指向了 Pages,并用一张银行卡跑了一次真实的 29 美元测试购买,然后退了款。我们还特意走了一遍失败路径——一张会被拒的测试卡——以确认她不会被扣费,并且看到的是一条像样的消息而不是一片空白。一个下午就上线了。她当晚就做成了第一笔销售。