表单提交成功却收不到询盘:静态站转化链路审计四步法与7类送达通道实测

2026-10-04 · AI智工坊 · 网站转化工程

2026-08-30,我们在自家站点 aizhigongfang.com 上做了一次完整审计,结论很难看:页面有表单、地址填了能提交、屏幕上弹出"提交成功"、浏览器控制台一个红色报错都没有——但一条询盘都收不到。

原因不复杂:那个表单根本没有后端。访客填的内容只写进了他自己浏览器的 localStorage,然后被一句"提交成功"的动画送走了。数据从来没离开过访客的电脑。

这类故障是静态站(Cloudflare Pages / Vercel / Netlify 一类)的头号杀手,而且它有一个极其恶劣的性质:常规健康检查完全发现不了。首页 200、页面能打开、移动端正常、速度很快——所有指标都是绿的,只有成交是零。这篇文章把我们踩过的坑和实测口径完整写下来,四步排查法可以直接照着做。

第一步:确认表单到底有没有真后端

不要看设计稿,不要问开发,直接抓线上页面的提交逻辑:

curl -s https://你的域名/ | grep -n -A25 "function.*[Ss]ubmit"

命中以下任意一条,就可以判定为假表单:

这一步的意义在于:先分清"没送到"和"没送"。很多团队发现问题后一头扎进邮件服务商后台查日志,查了半天才发现记录压根没产生——后端从来不存在。

第二步:给纯静态站补一个真后端

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——别被它骗了。

第三步:送达通道选型(7 类实测结论)

有了后端只是让数据"能离开访客电脑",接下来要保证它"能到达你手里"。静态站没有数据库,询盘只能靠外部服务转送。以下是我们逐个实测后的结论,按可用性排序:

通道实测结论真实卡点
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 天后会静默失效的桶

兜底腿能救命,但它自带一颗定时炸弹:匿名收件桶 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 只代表"请求发出去了",只有桶里出现记录才代表"真的收到了"。

验证口径:一个把 25 条正常链接误判成全部正常的坑

排查到最后一步是验证全站链接,这里有一个几乎人人都会踩的口径错误——不要用 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
口径错了要显式纠正,不能悄悄改完就算。伪装成"一直是对的",比犯错本身危害更大——因为那意味着你的所有历史报告都不可信了。

三条反直觉的结论

  1. 本机能调通 ≠ 线上能调通。Cloudflare Functions 是共享出口 IP,所有"免费、无需注册"的公共服务第一反应就是限流它。每换一条送达通道,都必须从线上真实 POST 验证一次,本机 curl 成功不能作为"修好了"的证据。
  2. 统计代码是"想要",不是"必需"。先解决询盘通道,统计可以后置。我们在这上面绕了很远:域名 DNS 不在 Cloudflare(zone 数为 0)时,CF Web Analytics、GraphQL httpRequests*、pagesFunctionsInvocationsAdaptiveGroups 全都用不了,全是白费功夫;第三方统计(51.la 之类)又有人机滑块 + IP 级限流,触发后"操作过于频繁"要冷却数小时,重开浏览器也清不掉。核心健康指标用"收录页数 + 询盘到达"替代就够。
  3. 收件箱必须是"你自己能读"的那个。把询盘发到老板的 163/QQ 邮箱是经典错误:邮件确实到了,但 IMAP 被风控拦下(163 的经典 "Unsafe Login"),你读不到 = 等于没到。让收件地址先落到一个你能用程序直读的邮箱,老板邮箱降为抄送。

顺带说收录:IndexNow 是唯一免账号的正解

链路通了,流量从哪来?静态站提交收录有几个已知死路:Bing 的 ping?sitemap= 接口已废弃(返回 410 Gone)、Google 的同类接口国内网络根本连不上、Baidu/Bing 站长平台需要注册加验证。

唯一免注册免账号的方案是 IndexNow,Bing / Yandex / Seznam / Naver 都吃:生成一个 key、把 <key>.txt 放到站点根目录并部署、POST 提交 URL 列表即可(200 或 202 都算受理)。

IndexNow 也有个小坑:如果站点根目录下存在多个 32 位 hex 格式的 key 文件,用 shell 通配去取 key 会一次拿到两行,拼进 JSON 后直接返回 400。写法固定成常量,或取一行,并先验 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。

怎么验证 Pages Functions 真的上线了?

看部署输出里有没有 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 成功后就宣布修好。

sitemap 里的链接怎么验证才靠谱?

不要用 curl -L。带 -L 会跟随重定向,把 308 → 404 掩盖成"看起来正常"——我们曾据此误判 25 条全部 200,实际其中 16 条是 404。正确做法是不带 -L 直接看状态码,要求直接 200,并且 URL 写成去掉 .html 的最终形态,避免全站 308 跳转。