2026-08-30,我们在自家站点 aizhigongfang.com 上做了一次完整审计,结论很难看:页面有表单、地址填了能提交、屏幕上弹出"提交成功"、浏览器控制台一个红色报错都没有——但一条询盘都收不到。
原因不复杂:那个表单根本没有后端。访客填的内容只写进了他自己浏览器的 localStorage,然后被一句"提交成功"的动画送走了。数据从来没离开过访客的电脑。
这类故障是静态站(Cloudflare Pages / Vercel / Netlify 一类)的头号杀手,而且它有一个极其恶劣的性质:常规健康检查完全发现不了。首页 200、页面能打开、移动端正常、速度很快——所有指标都是绿的,只有成交是零。这篇文章把我们踩过的坑和实测口径完整写下来,四步排查法可以直接照着做。
不要看设计稿,不要问开发,直接抓线上页面的提交逻辑:
curl -s https://你的域名/ | grep -n -A25 "function.*[Ss]ubmit"
命中以下任意一条,就可以判定为假表单:
localStorage.setItem / getItem,全文没有 fetch( 也没有 XMLHttpRequest;window.open('mailto: 或 location.href='mailto:——这类在部分浏览器上会被静默拦截;fetch,但指向一个已经死掉的第三方接口。别猜,直接 curl 那个地址确认存活。Cloudflare Pages 的 Functions 是这个问题最省事的解:不需要服务器、不需要额外账号、不额外花钱。在部署目录里建 functions/api/lead.js,导出 onRequestPost 和 onRequestOptions,文件随站点一起部署,自动挂载为 POST /api/lead。
这里有一个必须记住的坑,我们踩过:
# ❌ 错:上级目录若有 package.json,wrangler 会把上级当项目根, # 去那儿找 functions → 不编译 → POST /api/lead 返回 405 cd ...\ai-service-portal npx wrangler pages deploy cf-deploy-cn ... # ✅ 对:在部署目录内部执行 cd ...\ai-service-portal\cf-deploy-cn npx wrangler pages deploy . --project-name <项目名> --branch main --commit-dirty true
失败现象很迷惑人:POST /api/lead 返回 405,不是 404。/api/lead 这个路径被当成静态资源处理了,所以既不是"接口不存在"也不是"服务器错误",很多人会误以为是 CORS 或方法不对,越查越远。
每次部署必查的唯一判据:部署输出里必须出现 Uploading Functions bundle 这一行。没有出现,就是 Functions 没上线,此时日志照样会打印 Deployment complete——别被它骗了。
有了后端只是让数据"能离开访客电脑",接下来要保证它"能到达你手里"。静态站没有数据库,询盘只能靠外部服务转送。以下是我们逐个实测后的结论,按可用性排序:
| 通道 | 实测结论 | 真实卡点 |
|---|---|---|
| MailChannels (CF 生态免费 SMTP) | ✅ 首选 | 2024 年起强制 DKIM,缺 DNS TXT 直接返回 401。DNS 若托管在 DNSPod 等第三方(不是 Cloudflare),CF API token 加不了记录,只能人工加——这是唯一真正的阻塞点。 |
| 企业微信群机器人 webhook | ✅ 到手机最快 | 需在群设置里手动"添加机器人"(约 30 秒人工动作),拿到 URL 配环境变量即可;无 IP 白名单限制。 |
| 自建收件桶 (webhook.site 一类) | ✅ 最稳的兜底腿 | 桶"只收不推",不做转发不做限流判定,因此不受日配额影响;代价是必须主动拉取,且有7 天到期问题(见第四步)。 |
| FormSubmit | ❌ 已激活也不可用 | 从 Cloudflare 出口 IP 调用被限流 429(本机 curl 正常,线上必 429)。 |
| ntfy.sh | ❌ 免费 tier 不可用 | 免费服务器日配额被共享出口 IP 刷爆(daily message quota reached),且消息 TTL 仅 12 小时。 |
| Web3Forms | ❌ 注册都做不了 | 注册页被人机验证挡住。 |
| Cloudflare KV / D1 | ❌ 通常没权限 | 部署用 token 往往只有 Pages 权限,建 namespace 返回 Authentication error 10000,要另开带 KV 权限的 token。 |
四条腿同时挂掉不是罕见事故,而是静态站的常态。所以正确架构不是"选一个最好的通道",而是多腿并行、任一成功即算送达、单一失败不报警:主腿用 MailChannels(配好 DKIM 后最正规),快腿用企微机器人,兜底腿用自建收件桶。写进代码就是:
const results = {};
for (const url of LEAD_SINKS) {
try { await fetch(url, {...}); results[url] = 'sent'; }
catch (e) { results[url] = 'failed:' + (e.status || e.message); }
}
// 只要有一腿 sent,就对访客返回 ok:true —— 别让客户为后端问题买单
兜底腿能救命,但它自带一颗定时炸弹:匿名收件桶 7 天过期,过期后询盘静默丢失,前端完全无感。
这个我们吃过两次。最近一次是 2026-10-02 早上的例行探针,返回体里长这样:
{"ok":true,"results":{"sink":"sent | failed:404", ...}}
第二腿是 failed:404——不是超时、不是限流,是 404,意思是那个桶已经被平台回收删除了。当时第一腿还在正常工作,所以前端一切正常,ok 依然是 true,如果没有专门盯 sink 字段的多腿结果,这次失效会一直被掩盖到第一腿也到期为止。
sink 里出现 failed:404,就是"某个桶已过期被删"的特征信号——直接去查各腿到期日,不要当网络抖动去重试。这一点要和"连续超时"区分开:超时(exit 28)可以先重试再判故障,404 重试一万次也还是 404。我们现在的做法是双桶并行 + 每次开班必查到期日,桶的到期时间写在代码注释里,轮换流程三步:
# 1. 建新桶(返回体里的 expires_at 就是到期时间,UTC) curl -s -X POST "https://webhook.site/token" -H "Content-Type: application/json" # 2. 改 functions/api/lead.js 的 LEAD_SINKS,同步更新注释里的到期日,然后部署 python cf_deploy_cn.py # 3. 端到端复验:POST /api/lead 应 sink:sent,且【新桶实收探针】 curl -s "https://webhook.site/token/<新uuid>/requests?sorting=newest"
第 3 步的判据必须强调:看到 sink:"sent" 不算通过,必须在新桶里查到那条探针。因为 sent 只代表"请求发出去了",只有桶里出现记录才代表"真的收到了"。
排查到最后一步是验证全站链接,这里有一个几乎人人都会踩的口径错误——不要用 curl -L。
Cloudflare Pages 会把 /x.html 和 /x/ 都重定向到 /x。带 -L 会跟随这个重定向,于是 308 → 404 被掩盖成"看起来正常"。我们曾据此误判"25 条 sitemap 链接全部 200",实际其中 16 条是 404——等于三分之一以上的站点入口是死的,而报告上的数字是漂亮的。
# ❌ 带 -L,会把 308 → 404 洗白
curl -s -o /dev/null -w "%{http_code}" -L "https://你的域名/某页"
# ✅ 不带 -L,要求直接 200
curl -s -o /dev/null -w "%{http_code}" "https://你的域名/某页"
# 全量核查(不带 -L)
curl -s "https://你的域名/sitemap.xml" | grep -oE "https://[^<]+" | while read u; do
c=$(curl -s -o /dev/null -w "%{http_code}" "$u"); [ "$c" != "200" ] && echo "❌ $c $u"
done
口径错了要显式纠正,不能悄悄改完就算。伪装成"一直是对的",比犯错本身危害更大——因为那意味着你的所有历史报告都不可信了。
httpRequests*、pagesFunctionsInvocationsAdaptiveGroups 全都用不了,全是白费功夫;第三方统计(51.la 之类)又有人机滑块 + IP 级限流,触发后"操作过于频繁"要冷却数小时,重开浏览器也清不掉。核心健康指标用"收录页数 + 询盘到达"替代就够。链路通了,流量从哪来?静态站提交收录有几个已知死路:Bing 的 ping?sitemap= 接口已废弃(返回 410 Gone)、Google 的同类接口国内网络根本连不上、Baidu/Bing 站长平台需要注册加验证。
唯一免注册免账号的方案是 IndexNow,Bing / Yandex / Seznam / Naver 都吃:生成一个 key、把 <key>.txt 放到站点根目录并部署、POST 提交 URL 列表即可(200 或 202 都算受理)。
https://你的域名/<key>.txt 返回 200 再提交。如果你在做的项目本身需要一套已跑通的内容资产(比如方言语料、数字人素材),站内有对应的可直接下载商品;但上面这套排查流程,你不需要买任何东西就能自己跑一遍。
看看站内已上架的数字资产 →最常见的原因是那个表单根本没有后端。数据只写进了访客浏览器的 localStorage,或者弹了一个 mailto: 窗口——数据留在访客电脑里,站长永远看不到。这种表单点下去有动画、有"提交成功"提示、控制台零报错,HTTP 状态码也是 200,常规健康检查完全发现不了。判断方法:抓页面里的提交逻辑,如果只有 localStorage.setItem 而没有 fetch(,就是假表单。
可以。Cloudflare Pages Functions 不需要服务器、不需要额外账号:在部署目录里建 functions/api/lead.js 导出 onRequestPost,文件随站点一起部署,自动挂载为 POST /api/lead。关键前提是必须在部署目录内部执行 wrangler,否则 wrangler 会把上级目录当项目根,functions 目录不被编译,POST 会返回 405。
看部署输出里有没有 Uploading Functions bundle 这一行。没有出现就是 Functions 没编译进去,此时部署日志照样会打印 Deployment complete,别被它骗了。部署后立刻 POST 一个空对象到接口,期望 400(参数校验生效)而不是 405;OPTIONS 请求期望 204。
MailChannels 首选,但 2024 年起强制 DKIM,缺 DNS TXT 记录直接 401;DNS 若托管在 DNSPod 等第三方而非 Cloudflare,CF API 加不了记录,必须人工加。企业微信群机器人 webhook 到手机最快、无 IP 白名单限制。FormSubmit 从 CF 出口 IP 调用会被限流 429;ntfy.sh 免费服务器日配额常被刷爆;Web3Forms 注册页有人机验证挡路。
因为"本机能调通"不等于"线上能调通"。Cloudflare Functions 是共享出口 IP,所有免费无需注册的公共服务第一反应就是限流这个 IP。每换一个通道都必须从线上真实 POST 验证一次,不要在本机 curl 成功后就宣布修好。
不要用 curl -L。带 -L 会跟随重定向,把 308 → 404 掩盖成"看起来正常"——我们曾据此误判 25 条全部 200,实际其中 16 条是 404。正确做法是不带 -L 直接看状态码,要求直接 200,并且 URL 写成去掉 .html 的最终形态,避免全站 308 跳转。