静态站点怎么检测访客国家——别用时区猜,Cloudflare 早给你留好后门了
给一个纯静态站加「访客在中国大陆则提示切换镜像」的功能,第一反应往往是用 navigator.language === 'zh-CN' 或者读时区 Intl.DateTimeFormat().resolvedOptions().timeZone === 'Asia/Shanghai'。写起来三行代码,看起来挺合理。
但上线一周后收到反馈:开了 VPN 的用户在国内一直看不到提示;把 OS 语言设成英文的用户同样被漏掉。这两种启发式本质都是「猜用户偏好」,不是「定位客户端 IP」,完全不是同一件事。
现象
- 静态站(Next.js
output: export+ Cloudflare Pages)没有 Server,拿不到CF-IPCountry请求头 - 用
navigator.language判断:设英文系统的国内用户全部漏掉 - 用时区判断:挂着美国节点 VPN 的用户会被错判为海外
- 想装个 Cloudflare Worker 来读请求头,为了一个 banner 显得过于重型
根因
Cloudflare 在代理层(橙云模式)做了 IP 归因,这个信息只有边缘节点才有。浏览器端能拿到的 navigator.language、时区、Accept-Language header 都是客户端的偏好设置,和物理位置没有可靠关联。
真正精准的数据藏在 Cloudflare 的内置调试端点里:
GET /cdn-cgi/trace
这个路径在任何 Cloudflare 橙云代理的域名上都存在,由 Cloudflare 边缘托管,与你的站点代码无关。它返回纯文本的 key=value 格式,其中 loc 字段就是边缘节点判定的客户端 ISO 国家码:
fl=123abc
ip=1.2.3.4
ts=1700000000.123
visit_scheme=https
loc=CN ← 这就是你要的
tls=TLSv1.3
colo=HKG
navigator.language === 'zh-CN' 只能告诉你用户的系统语言设置,VPN 用户、英文系统用户、海外华人都会被漏判或误判。时区同理。如果你真正需要的是「访客 IP 来自哪个国家」,这两个方案从根子上就错了。
正解
在浏览器端 fetch('/cdn-cgi/trace'),解析 loc 字段即可。
// utils/cf-trace.ts
export function parseCfLoc(traceText: string): string | null {
for (const line of traceText.split("\n")) {
const eq = line.indexOf("=");
if (eq === -1) continue;
// 精确匹配 "loc",避免把 "colo=" 也捞进来
if (line.slice(0, eq).trim() === "loc") {
return line.slice(eq + 1).trim().toUpperCase() || null;
}
}
return null;
}在 React 客户端组件里调用:
// "use client"
useEffect(() => {
// 只在 Cloudflare 主站触发,镜像站没有 /cdn-cgi/trace
if (location.hostname !== MAIN_HOST) return;
fetch("/cdn-cgi/trace", { cache: "no-store" })
.then((r) => (r.ok ? r.text() : Promise.reject()))
.then((t) => {
if (parseCfLoc(t) === "CN") setShowBanner(true);
})
.catch(() => {
// geo 未知时静默失败,不打扰用户
});
}, []);几个实现细节不能省:
- 只在主站触发:镜像站(自建 nginx / 阿里云等)没有
/cdn-cgi/trace,fetch 会 404。用location.hostname === MAIN_HOST做门卫,保证只在 CF 代理的域名上调用。 - Banner 而非自动跳转:加
localStorage记录「用户已关闭」,避免反复弹出;跳转镜像时保留当前路径https://${MIRROR}${location.pathname}${location.search}${location.hash}。 - SSR 安全:组件初始返回
null,effect 触发后才切换显示,不产生 hydration mismatch。
只要站点走了 Cloudflare 橙云代理,这个端点就一定存在。浏览器直接调,拿到的是 Cloudflare 边缘节点基于 IP 数据库判定的国家码,准确度足够「推荐切换镜像」这类场景使用。服务端如果有 Edge Function / Worker,改用 request.cf.country 或 CF-IPCountry 请求头更简洁。
实测验证
本地开发时 /cdn-cgi/trace 不可用(localhost 没经过 CF 边缘),可以先用 curl 确认部署后能拿到:
curl https://<your-cf-domain>/cdn-cgi/trace单元测试只需覆盖 parseCfLoc 纯函数:正常 loc=CN → 返回 "CN";loc= 空值 → 返回 null;colo=HKG → 不被匹配;CRLF 换行容错。
一句话外卖
Cloudflare 在每个橙云代理域名上都内置了
/cdn-cgi/trace,loc字段就是边缘判定的国家码——静态站检测访客地区,先查这个,别猜时区。