首页 200 却不代表站点活着:Cloudflare Pages 生产部署悄悄回滚的体检与修复

2026-10-07 · AI智工坊 · 静态站 / 部署运维

我们踩过一次很安静的坑,安静到几乎没有任何一个常规监控能报警。

那天早班照例巡检,第一项 curl -I https://站点/ 返回 200 OK。sitemap 也 200。如果巡检到这里就收工,结论会是"站点一切正常"——但实际上,那一刻全站所有博客文章和商品页都是 404,询盘接口也是坏的。访客搜进来的每一个内容页都打不开,而首页看起来完好无损。

这篇文章记录完整的发现过程、判据和修复动作,全部数字来自我们自己的生产站在 2026-10-07 早班 的实测输出。

一、故障现场:一个"看起来正常"的站点

按固定巡检脚本跑 8 个 URL,结果是这样的:

URL返回判读
/200首页正常 → 会让人误以为站点没事
/sitemap.xml200也 200,但内容陈旧(关键线索在下面)
/products/404商品聚合页没了
/products/suzhou-dialect-lexicon404主商品页没了
/blog200(空响应体)最迷惑的一项:状态码对、内容是空的
/blog/<最新一篇>404抽查任意近期文章均 404
/api/lead(GET)404Functions 端点消失
/wechat-pay-qrcode.jpg200老静态资源还在 → 更强化"没事"的错觉
把这张表看一遍就会发现规律:能 200 的都是"很久以前就存在于部署包里"的文件;凡是我们最近两个月新加的东西(商品页、新文章、Functions),全部消失。这不是"某几个页面坏了",而是整个线上版本回到了过去——生产部署漂移(deployment drift)。

二、一个数字定性:线上跑的是哪一版

判断"线上是哪一版",最省事的办法不是去翻控制台的部署历史,而是数 sitemap 条数——它是我们每次上线都会同步增长的计数器官。

# 线上
curl -s https://站点/sitemap.xml | grep -c "<loc>"
→ 18

# 本地源码目录
grep -c "<loc>" sitemap.xml
→ 35

18 对 35,一句话就定性了。再补一刀,看线上 sitemap 里最新的 <lastmod>:

curl -s https://站点/sitemap.xml | grep -o "<lastmod>[^<]*" | sort -u | tail -3
→ <lastmod>2026-06-19
→ <lastmod>2026-06-24

线上最新内容是 2026-06-24,而此刻是 2026-10-07——线上落后了整整三个多月。这期间我们上线的所有文章、商品页、询盘 Functions,一个都没生效。

为什么这个判据比监控更可靠

因为它不依赖外部服务,也不需要凭证。任何静态站只要在 sitemap 里维护条数,就有了一个自带的"版本号"。同理,"最新一篇博客 URL 是否 200" 也是一个极佳的探针——它天然覆盖"部署是否生效"这件事。

部署成功 ≠ 线上生效。要证明"线上生效",必须拿一个只有新版本才会有的东西去线上验证,而不是看部署命令有没有报错。

三、Functions 是怎么消失的:405 与 404 的分工

询盘接口的表现很有信息量:GET 返回 404,POST 返回 405。这两个码一起出现,几乎可以确定成因:

核心结论是:Functions 没有被打包进这次部署。Cloudflare Pages 的 Functions 依赖部署目录下的 functions/ 目录被 wrangler 识别并编译;一旦 wrangler 找错了项目根,函数就不会被编译,/api/* 就退化成普通静态路径。

最常见的根因:在错误的目录执行 wrangler

如果部署目录的父目录里也有一个 package.json,wrangler 会把父目录当成项目根,于是它去父目录找 functions/——找不到,就不编译。表现就是:部署"成功",但 Functions 全灭。

# ✅ 正确:进入部署目录内部再 deploy .
cd /path/to/cf-deploy-cn
npx wrangler pages deploy . --project-name <项目名> --branch main --commit-dirty true

# ❌ 错误:在上级目录执行(父级 package.json 会带偏 wrangler)
cd /path/to/parent
npx wrangler pages deploy ./cf-deploy-cn --project-name <项目名>
部署日志的隐藏判据:正常的、带 Functions 的部署,输出里必定出现 Compiled Worker successfully 与 Uploading Functions bundle。如果这两行没出现,哪怕显示 "Deployment complete",也是一次没有后端的部署。

一个反向佐证:用请求头回溯 Functions 的死亡时间

我们还有一个意外好用的物证。询盘 Functions 在把数据转发到 webhook 桶时,Cloudflare 会在请求头里带上 cf-worker 标识:

"headers":{"cf-worker":["<项目名>.pages.dev"], "x-lead-source":["..."]}

翻桶里的历史记录,最后一次带 cf-worker 头的投递停留在 2026-10-05 上午。也就是说,我们可以精确知道 Functions 是在那个时间点之后消失的,而不必靠猜测。凡是链路中间有第三方旁路(webhook、日志、埋点)的系统,都能用这种方式做"历史取证"。

四、修复:一次正确的部署,四条验证

修复动作本身只有一步——回到正确的目录、用 wrangler 重新部署:

cd C:\Users\...\ai-service-portal
python cf_deploy_cn.py

脚本内部做的正是上一节的"正确姿势"(PowerShell 调 wrangler、Set-Location 进部署目录、清理 wrangler 缓存)。这次的输出:

Compiled Worker successfully
Uploading... (86/86)
Success! Uploaded 2 files (84 already uploaded) (1.62 sec)
Uploading _headers
Uploading _redirects
Uploading Functions bundle      ← ★ 关键行,出现即 Functions 上线
Deployment complete! ... https://5ad4f299.<项目名>.pages.dev

注意 Uploaded 2 files (84 already uploaded):这不代表"只上传了 2 个文件所以没更新"。Cloudflare Pages 用内容寻址去重,内容没变的文件不会重复上传。判断部署是否生效只能靠线上实测。

四条验证,缺一不可

  1. 新页面 200:抽查 /products/suzhou-dialect-lexicon 与最新一篇 /blog/<slug> → 实测全部 200(此前均为 404);
  2. sitemap 条数回归:线上 <loc> 从 18 → 35,与本地一致;
  3. Functions 活体探针:POST /api/lead 返回 {"ok":true},且投递腿为 sink:"sent | sent"(双桶均送达);
  4. 桶内实收:直接在 webhook 桶里查到这条探针记录 —— 只看接口返回 sent 不够,要看下游真的收到了。
第 4 条是很多人会省掉的一步。接口返回 sent 只说明"我们发出去了",不说明"对面收到了"。当投递目标是第三方(webhook / 邮件 / 推送)时,必须到收方确认,否则你会以为链路是通的。

五、把这次事故变成固定巡检项

事后复盘,真正的问题不是"部署会失败",而是巡检项的粒度太粗——只测首页,等于没测。现在我们的固定巡检至少覆盖四类:

类别探针通过判据
版本一致性线上 sitemap <loc> 条数等于本地源码目录的条数
新增内容可达最新一篇博客 / 最新商品页200(这两个一定是新版本才有)
后端存活POST /api/leadok:true 且 sink:"sent"
下游实收第三方桶 / 邮箱查记录能查到刚才那条探针

三条可以带走的纪律

这套体检法本身与平台无关——静态托管、CDN、容器部署都一样:能 200 的东西不代表对的东西,只有拿新版本独有的内容去追问线上,才能问出真相。如果你也在维护这类"看起来一直好好的"站点,建议今天就去数一次 sitemap 条数。顺便,我们站内上架了可商用的方言语料资产与数字人素材,做内容站、语音类应用时可以直接取用。

看看站内已上架的数字资产 →

常见问题

首页返回 200,是不是就说明站点没问题?

不是。首页是部署包里的一个静态文件,只要生产环境有任意一版部署,首页就会 200。真正要判断的是"线上跑的是哪一版"——用内容指纹,最简单的就是数 sitemap.xml 里的 <loc> 条数并与本地源码目录对比。我们实测线上 18 条、本地 35 条,一眼就能确认线上是两个月前的旧版本。

为什么 Cloudflare Pages 的 POST 接口返回 405?

405 通常意味着 Functions 没有被编译进这次部署,请求被当成静态路径处理了。最常见根因是在错误的目录执行 wrangler:如果父目录里也有 package.json,wrangler 会把父目录当成项目根,于是找不到 functions/ 目录。部署输出里不会出现 Uploading Functions bundle,也不会有 Compiled Worker successfully。修复方式是在部署目录内部执行部署。

部署输出说 Uploaded 0 files 是不是失败了?

不是。Cloudflare Pages 用内容寻址去重,文件内容未变就不会重复上传,所以出现 Uploaded 2 files (84 already uploaded) 是正常的。判断部署是否生效不能看上传文件数,只能看线上实测:新 slug 是否 200、sitemap 条数是否 +1、Functions 是否收到请求。

怎么一眼看出线上用的是旧版本?

三个高性价比的内容指纹:①sitemap.xml 的 <loc> 条数(本地 35 / 线上 18);②最新一篇博客 URL 是否 200;③最新 lastmod 日期是否等于你最后一次部署的日期(线上最新是 2026-06-24)。这三个都不依赖任何监控平台,一条 curl 就能拿到。

站点回滚后询盘数据会丢吗?

会静默丢失。Functions 失效后,表单提交要么返回 404/405,要么被当成静态文件处理而没有任何后端接收。这段时间的询盘没有任何地方留存。所以巡检必须把 POST 探针当作固定项,返回 sink:sent 才算通。