Cloudflare Pages 白页之谜
Next.js 静态部署到 Cloudflare Pages 出现白页?index.txt 的坑
站点部署后偶尔打开是白屏,刷新就好了。这个问题排查了将近半天, 最后发现是 Next.js 和 Cloudflare 之间一个微妙的配合问题。
问题现象
📚 本文是 UtlKit 技术系列第 4 篇 — 系列索引 →
- 上一篇:Hydration Mismatch →
- 下一篇:浏览器兼容性 →
部署到 Cloudflare Pages 后,大部分时间访问正常。但偶尔——特别是从其他页面导航到首页时——会出现白屏。刷新后恢复正常。
Sentry 没有报错。Network 面板看起来也没问题——至少表面上看起来。
排查过程
第一步:检查 Cloudflare 缓存
第一反应是 Cloudflare 缓存了某个中间状态。清了缓存,问题还在。
第二步:用浏览器 DevTools 追踪
打开 Network 面板,勾选 "Preserve log",然后复现问题。
发现问题出现时,/ 请求的 Response Header 里 Content-Type: text/plain,响应体是空的。
正常情况应该是 Content-Type: text/html,响应体是 index.html 的内容。
第三步:Content Negotiation
为什么同一个 URL 有时候返回 HTML,有时候返回 text/plain?
关键线索:浏览器在导航到 / 之前,先发了一個 prefetch 请求:
GET / HTTP/1.1
Accept: text/plain
Accept-Encoding: gzip, deflate, br
这个 prefetch 请求的 Accept: text/plain 是 Next.js 15 的 content negotiation prefetch 机制发出的。
根因分析
Next.js 15 的 prefetch 行为
Next.js 15 在客户端导航前会 prefetch 目标资源。为了提高效率,它对某些资源会尝试用 Accept: text/plain 去请求——期望服务端返回轻量级的元数据而不是完整 HTML。
Next.js 静态导出创建了 index.txt
output: 'export' 构建时,Next.js 会为 / 路由创建 out/index.html。但内部某些流程(content negotiation 支持)还会额外生成 out/index.txt。
Cloudflare Pages 的文件匹配规则
Cloudflare Pages 静态托管的文件路由规则:
GET /+Accept: text/html→ 匹配index.html✅GET /+Accept: text/plain→ 匹配index.txt✅(如果存在)
当 index.txt 存在时,Cloudflare Pages 会根据 Accept header 做 content negotiation,返回 index.txt。
如果 index.txt 是空的(或内容不完整),用户就会看到白屏。
完整的 bug 链条
App Router Link prefetch
→ browser sends GET / Accept: text/plain
→ Cloudflare Pages finds index.txt
→ returns empty text/plain response
→ browser uses this as page content
→ 白屏!
演进:从白页到 404 泛滥
上面的删除方案解决了白屏问题,但带来了一个新问题。
现象
站点有 100+ 个工具页面,每个页面在客户端导航时,Next.js 15 的 App Router 都会发一个 Accept: text/plain 的 prefetch 请求。index.txt 被删后,每个请求都返回 404。
F12 Network 面板里密密麻麻全是 404:
/ 404 (text/plain)
/tools/json-formatter/ 404 (text/plain)
/tools/base64/ 404 (text/plain)
/tools/uuid-generator/ 404 (text/plain)
... (100+)
影响
- 视觉干扰:开发调试时 Network 面板全是红色 404
- 带宽浪费:每个 404 都消耗一次 Cloudflare 请求和响应
- 连接竞争:HTTP/1.1 每域名并发 6 个连接,404 请求挤占了正常资源的带宽
- Cloudflare 不缓存 404:每次访问都打到源站,没有命中率
更好的方案:创建空的 index.txt
与其删除,不如创建——让 prefetch 请求命中一个合法的 200 响应:
// scripts/create-index-txt.js
const fs = require('fs')
const path = require('path')
function createIndexTxtFiles(dir) {
let count = 0
function walk(currentDir) {
const indexPath = path.join(currentDir, 'index.html')
if (fs.existsSync(indexPath)) {
const txtPath = path.join(currentDir, 'index.txt')
if (!fs.existsSync(txtPath)) {
fs.writeFileSync(txtPath, '')
count++
}
}
try {
const entries = fs.readdirSync(currentDir, { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory()) {
walk(path.join(currentDir, entry.name))
}
}
} catch (e) {}
}
walk(dir)
console.log(`Created ${count} index.txt files`)
}
createIndexTxtFiles(path.join(__dirname, '..', 'out'))
构建流程改为:
{
"scripts": {
"build": "next build && node scripts/create-index-txt.js"
}
}
为什么创建比删除好
| 维度 | 删除 index.txt | 创建空 index.txt |
|---|---|---|
| Prefetch 结果 | 404 | 200 OK (0 bytes) |
| Cloudflare 缓存 | ❌ 不缓存 404 | ✅ 缓存 200 |
| 连接竞争 | 每个都打到源站 | 命中缓存,瞬间返回 |
| Network 面板 | 满屏红色 | 全部绿色 |
| 白屏风险 | 无 | 无(空文件不会覆盖 HTML) |
| 文件大小 | 0 | 0 bytes × 页面数 |
关键点:空的 index.txt 不会造成白屏。因为 Next.js 客户端拿到空内容后,知道这不是有效 HTML,会自动 fallback 到 Accept: text/html 请求完整的 index.html。Cloudflare 缓存了这个 200 响应后,后续 prefetch 都是缓存 HIT,不会浪费连接。
教训
- 部署平台的文件匹配规则要看文档 — Cloudflare Pages 的 content negotiation 行为和普通静态服务器不同
- 浏览器 prefetch 是双刃剑 — 优化了加载速度,但也增加了边界情况
- 静态导出时检查输出目录 —
ls -la out/看一眼有没有不该有的文件 - 白屏问题优先看 Network — Content-Type 不对往往是根因
- 不要和平台对抗 — 与其删文件让 404 泛滥,不如顺应机制,创建空文件让它缓存
📚 本文是 UtlKit 技术系列第 4 篇 — 系列索引 →
- 上一篇:Hydration Mismatch →
- 下一篇:浏览器兼容性 →
完整的构建配置和部署流程见 UtlKit — 103 个免费在线工具,Next.js 15 静态导出 + Cloudflare Pages 零成本部署。
如果觉得有帮助,欢迎点个⭐️。