背景:静态博客挂上橙云之后
我的个人博客(v2yy.com)是 Astro 写的,Firefly 主题,源码在 GitHub 仓库里。最开始部署在一台 VPS 上(下文用示例 IP 203.0.113.23 指代),nginx 起静态站点,certbot 签 Let's Encrypt 证书,域名接 Cloudflare 橙云代理——DNS 解析到的是 CF 的 anycast IP 而不是源站,CF 再回源。后来静态站整体迁到了 Cloudflare Pages,但橙云这层一直在。
静态站挂 CDN 本来是"一劳永逸"的配置,结果这半年我陆续踩了一串坑:图片大面积裂、www 跳转死循环、构建莫名报错、搜索引擎不收录、新文章沉底。回头看每一个都不是什么高深问题,但当时都花了排查时间,共性在于:流量多穿了一层 Cloudflare 之后,很多"源站正常"的现象在边缘是另一副样子。这篇把几个坑按"现象 → 排查 → 解法 → 复盘"记下来。
一、文章图片大面积裂:CF 边缘缓存了一个 403
现象。 某天打开博客,好几篇文章的配图全裂了,首页缩略图也缺一块。先看最不该看的:本地重新 build 一遍,图片正常;GitHub 仓库里对应文件也在,git 状态干净。前端代码、构建产物都没动过——这时候就别查前端了。
排查。 直接对裂图的 URL 发一个 HEAD 请求,看响应头:
$ curl -I https://v2yy.com/_astro/posts/xxx/cover.png
HTTP/2 403
content-type: text/html
age: 30521
cf-cache-status: HIT
server: cloudflare
cf-ray: 8a1f0c9d3b...-LAX
两个信号组合起来就是判决书:cf-cache-status: HIT 说明这个 403 是 Cloudflare 边缘直接吐的,根本没回源;age: 30000+ 说明它已经在边缘躺了十几个小时。也就是说,某个时刻源站返回了 403,被 CF 当成正常内容缓存了下来,之后所有访客看到的裂图都来自这份缓存。
回头查源站 nginx,根因在这:站点里有些路径是目录但没有 index.html,nginx 对这类目录默认返回 403;而配置里写的是——
# 有坑的写法:渲染了 404 页面,但状态码仍泄漏为 403
error_page 403 404 /404.html;
这种写法会把 /404.html 的内容渲染出来,可状态码还是 403。裸 403 响应在 CF 眼里是"源站错误响应",照样按 TTL 原样缓存,于是边缘就存了一批 403。
解法。 两步:
- 改 nginx 配置,加等号强制改写状态码:
error_page 403 404 =404 /404.html;
= 会把实际返回码也改成 404,404 这类响应 CF 默认不会长期缓存。改完 nginx -t && nginx -s reload。
- 清边缘缓存。这里我额外踩了一刀:先用 Regex Purge(按正则批量清),结果部分裂图 URL 纹丝不动——regex purge 存在命中不了的形态。改用 Purge Custom URL,把 curl 出来的所有 403 URL 列成清单逐条清,立刻恢复。
# 先取证:批量确认哪些 URL 被边缘缓存了 403
for u in $(cat broken_urls.txt); do
curl -sI "$u" | grep -H -E 'HTTP|cf-cache-status'
done
# 然后 CF 控制台 → Caching → Purge By URL 粘贴清单
复盘。 挂 CDN 之后,"页面正常吗"这个问题要拆成三层问:源站返回什么、边缘缓存了什么、浏览器又缓存了什么。curl -I 看 cf-cache-status 和 age 是成本最低的分诊手段。顺带一提同族坑:在 CDN 后面替换同名静态资源(比如换一张同路径的图)会拿到数小时的旧字节,plain curl 验证时好时坏非常迷惑——持久解法是在配置里给引用路径加 ?v=YYYYMMDD 查询串,换一个缓存 key,而不是反复重传。
二、www → 裸域跳转搞出 308 死循环
现象。 把站点绑上 Pages 双域名(apex 和 www 各一个 Pages domain)之后,浏览器访问 https://www.v2yy.com 报 ERR_TOO_MANY_REDIRECTS,curl 看是在 www 和 apex 之间反复横跳,状态码 308。
排查。 源站 nginx 里我原本写了个 www 的 server block 做跳转,图省事用了变量化的写法:
# 危险写法:变量在 CF 代理链下会自跳
server {
server_name www.v2yy.com;
return 301 $scheme://v2yy.com$request_uri; # 早期正确形态
# 后来某版被写成 return 301 $scheme://$host$request_uri; ← 自己跳自己
}
变量 $host 一旦写成跟随请求主机名的形态,在 CF 代理下就会拿着 www 的 Host 跳回 www,308 跳 308 自己转圈;再加上 Pages 双域名绑定后源站 block 和 CF 的跳转规则互相叠加,链路更乱。
解法。 主机名之间的跳转(www→apex 这类 host-to-host redirect)放 CF zone 的 Redirect Rules 里做最干净:
匹配:https://www.v2yy.com/*
目标:https://v2yy.com/${1}
状态:301
理由有二:一是配置留在 CF 不依赖源站,源站换成 Pages、换成别的 VPS 都不用重做;二是源站不再参与跳转决策,杜绝叠加。源站侧只保留裸域一个 server block(Pages 侧则解绑 www 的跳转职责),把 nginx 里那个 www block 删掉再 reload。
顺带说 certbot:HTTP-01 验证在橙云下是通的(CF 会转发 80 端口回源),certbot --nginx -d v2yy.com -d www.v2yy.com --redirect 能签成功——但它签完会重写你的 nginx 配置(注入它自己的 80→443 if ($host) server、挪 listen 行)。我在被它注入的写法上继续手改,等于给死循环埋了另一颗雷。规则:certbot 跑完必须重新读一遍文件再做后续修改。
复盘。 跳转分层原则:路径级跳转留在 _redirects/nginx,主机级跳转留在 zone 规则。写 return 时永远不要用会跟随请求 Host 的变量去拼目标主机名。
三、Astro frontmatter 少打两个冒号,构建直接崩
现象。 新增一篇文章推送上去,Pages 构建红叉,本地 build 也失败:
InvalidContentEntryDataError: [articles] › posts/my-post.md
✖ "published" — Invalid date
排查。 frontmatter 里我写的是:
---
title: 一篇文章
published: 2026-09-28 17:00
---
Astro 内容集合的 schema 用 z.date() 校验日期字段,YYYY-MM-DD HH:MM 这种到分钟的精度过不了,必须带秒:published: 2026-09-28 17:00:00。这个错还有个隐蔽性:我是在 detached 后台构建里跑的,日志尾部只有一个 exit 哨兵,光看退出码不知道原因,得 grep 构建日志里的 schema 错误文本才定位到。
解法。 手写文章容易忘,所以把格式化收口到发布脚本里:脚本入参日期统一 YYYY-MM-DD HH:MM:SS,缺秒补 :00,从源头保证构建期校验必过。
复盘。 构建期校验类错误,第一优先级是看具体哪个字段报的,而不是先怀疑环境;能收口到脚本的约束就不要靠人记忆。
四、收录提速:GSC、Bing、IndexNow 的三连坑
新博客最急的是搜索引擎收录。这中间踩了三个坑,逐个说。
坑一:提交错了 GSC 资源。 Search Console 有两种资源:「网域」资源(DNS TXT 验证,覆盖全站子域,我这个域名的验证走的就是 CF DNS 加 TXT 记录)和「网址前缀」资源(如 https://v2yy.com/,未验证)。我当时把 sitemap 提交进了未验证的网址前缀资源,结果所有条目报「无法抓取」——一度怀疑站点本身有毛病,用 Googlebot UA 挨个 curl 了 sitemap-index 和全部页面(全 200,没有 meta-404),才反应过来是资源不对:未验证的前缀资源里一切提交都是假报错。换成已验证的网域资源后,sitemap 显示「成功/已发现」,正常爬取。
顺带两例域名相关的疑虑:CF 代理的 DNS 是 anycast 双 A + 双 AAAA,Googlebot 走 IPv6 也没问题(CF 两端都支持);CF 免费版的安全级别现在是自动化的「始终受保护」,不存在独立的 bot fight 开关会拦合法 Googlebot。站点健康就站点健康,别过度怀疑基础设施。
坑二:Bing 重复验证。 Bing Webmaster 直接支持「Import from GSC」,一键把 GSC 的资源拿过来,免掉二次所有权验证,省时省力。
坑三:IndexNow 与那个点不动的按钮。 Bing 系(含 Yandex 等)支持 IndexNow:在站点根目录放一个 key 文件(<key>.txt,内容就是 key 本身),然后新文章 URL 主动 ping 给索引端,秒级受理:
curl -s -X POST "https://api.indexnow.org/indexnow" \
-H "Content-Type: application/json; charset=utf-8" \
-d '{
"host": "v2yy.com",
"key": "ab12cd34ef56...",
"urlList": [
"https://v2yy.com/archives/new-post"
]
}' -w '%{http_code}\n'
# 202 = 已受理,无需等抓取
Google 这边,网页端「请求编入索引」按钮我自动化时扑了个空:用 JS 合成 element.click() 点它,侧边面板毫无反应。原因是该按钮有 isTrusted 校验——只认真实鼠标事件,合成点击一律无效。老老实实用坐标真实点击(CDP 鼠标事件)才走通。
复盘。 收录问题按"资源对不对 → robots/sitemap 拉得到吗 → 提交通道"的顺序排查,每步都有独立证据;IndexNow 是目前性价比最高的主动通知通道,值得在发布脚本里固化成每次发文自动 ping。
五、新文章总沉底:不是 bug,是主题排序规则
现象。 发了一篇新文章,首页列表里它沉在下面,旧文章反而在上面。第一反应是 Astro 的 frontmatter 日期解析又出问题,或者构建缓存了旧列表。
排查。 核对生成的列表页,新文日期明明正确。翻主题源码的排序逻辑才看清:Firefly 首页按 置顶(pinned)优先 → 日期降序 排,而模板 demo 遗留了几篇 pinned: true 的文章——置顶位永远压着新文,"沉底"只是被这几篇占住了前面。
# 全站搜一遍谁被置顶了
grep -rl "pinned: *true" src/content/posts/
posts/demo-1.md
posts/about-site.md
解法。 删掉这几篇 demo 遗留、或把它们的 pinned 去掉,新文立刻回顶。个人博客全站一律别用置顶,顺序交给日期就好。
复盘。 "排序不对"这类体感 bug,先读主题/框架的排序实现再动手,两分钟的事;grep 一遍相关字段(这次是 pinned)就能把"全站数据里谁有这个属性"看光,比盯着新文章本身猜有效得多。
写在最后
五个坑连起来看,其实都是同一个教训的变体:加了 Cloudflare 这层之后,你的站不再等于源站。边缘会缓存错误响应(裂图)、zone 规则和源站规则会打架(308 循环)、搜索引擎看到的 IP 不再是你的服务器(收录疑云)。所以排障姿势也要升级一层:先 curl -I 分诊边缘还是源站,跳转归 CDN、内容归源站,构建约束收口进脚本。证据链在手,这些坑每个只需要十分钟。
评论