Cloudflare Pages 白页:index.txt 的坑

31 阅读4分钟

Cloudflare Pages 白页之谜


Next.js 静态部署到 Cloudflare Pages 出现白页?index.txt 的坑

站点部署后偶尔打开是白屏,刷新就好了。这个问题排查了将近半天, 最后发现是 Next.js 和 Cloudflare 之间一个微妙的配合问题。

问题现象

📚 本文是 UtlKit 技术系列第 4 篇系列索引 →

部署到 Cloudflare Pages 后,大部分时间访问正常。但偶尔——特别是从其他页面导航到首页时——会出现白屏。刷新后恢复正常。

Sentry 没有报错。Network 面板看起来也没问题——至少表面上看起来。

排查过程

第一步:检查 Cloudflare 缓存

第一反应是 Cloudflare 缓存了某个中间状态。清了缓存,问题还在。

第二步:用浏览器 DevTools 追踪

打开 Network 面板,勾选 "Preserve log",然后复现问题。

发现问题出现时,/ 请求的 Response HeaderContent-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 结果404200 OK (0 bytes)
Cloudflare 缓存❌ 不缓存 404✅ 缓存 200
连接竞争每个都打到源站命中缓存,瞬间返回
Network 面板满屏红色全部绿色
白屏风险无(空文件不会覆盖 HTML)
文件大小00 bytes × 页面数

关键点:空的 index.txt 不会造成白屏。因为 Next.js 客户端拿到空内容后,知道这不是有效 HTML,会自动 fallback 到 Accept: text/html 请求完整的 index.html。Cloudflare 缓存了这个 200 响应后,后续 prefetch 都是缓存 HIT,不会浪费连接。

教训

  1. 部署平台的文件匹配规则要看文档 — Cloudflare Pages 的 content negotiation 行为和普通静态服务器不同
  2. 浏览器 prefetch 是双刃剑 — 优化了加载速度,但也增加了边界情况
  3. 静态导出时检查输出目录ls -la out/ 看一眼有没有不该有的文件
  4. 白屏问题优先看 Network — Content-Type 不对往往是根因
  5. 不要和平台对抗 — 与其删文件让 404 泛滥,不如顺应机制,创建空文件让它缓存

📚 本文是 UtlKit 技术系列第 4 篇系列索引 →


完整的构建配置和部署流程见 UtlKit — 103 个免费在线工具,Next.js 15 静态导出 + Cloudflare Pages 零成本部署。


如果觉得有帮助,欢迎点个⭐️。