DEV Community

Mark
Mark

Posted on

Cloudflare Pages Blank Page? The index.txt Trap with Next.js Static Export

After deployment, the site occasionally showed a blank screen on the homepage — refreshing fixed it.
Debugging this took half a day. The culprit was a subtle interaction between Next.js and Cloudflare.

The Problem

After deploying to Cloudflare Pages, most visits work fine. But occasionally — especially when navigating to the homepage from another page — a blank screen appears. Refresh and it's fine.

Sentry reports no errors. The Network tab looks fine — at first glance.

Debugging Process

Step 1: Check Cloudflare Cache

First instinct: Cloudflare cached an intermediate state. Cleared cache. Problem persisted.

Step 2: Browser DevTools

Opened Network tab, enabled "Preserve log", reproduced the issue.

When the blank screen appeared, the / request had Content-Type: text/plain in the response headers, with an empty body.

Normally it should be Content-Type: text/html with index.html content.

Step 3: Content Negotiation

Why does the same URL sometimes return HTML and sometimes text/plain?

Key clue: Before navigating to /, the browser sends a prefetch request:

GET / HTTP/1.1
Accept: text/plain
Accept-Encoding: gzip, deflate, br
Enter fullscreen mode Exit fullscreen mode

This prefetch with Accept: text/plain is from Next.js 15's content negotiation prefetch mechanism.

Root Cause Analysis

Next.js 15 Prefetch Behavior

Before client-side navigation, Next.js 15 prefetches the target resource. For efficiency, it sometimes requests with Accept: text/plain — expecting lightweight metadata instead of full HTML.

Next.js Static Export Creates index.txt

During output: 'export' build, Next.js creates out/index.html for the / route. But internal processes (content negotiation support) also generate out/index.txt.

Cloudflare Pages File Matching

Cloudflare Pages static hosting routing rules:

  • GET / + Accept: text/html → matches index.html
  • GET / + Accept: text/plain → matches index.txt ✅ (if it exists)

When index.txt exists, Cloudflare Pages does content negotiation based on the Accept header and returns index.txt.

If index.txt is empty, users see a blank page.

The Complete Bug Chain

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
          → Blank screen!
Enter fullscreen mode Exit fullscreen mode

Evolution: From Blank Screen to 404 Flood

The delete approach fixed the blank screen, but introduced a new problem.

The Problem

With 150+ tool pages, each page triggers an Accept: text/plain prefetch request during client-side navigation. With index.txt deleted, every request returns 404.

F12 Network panel is filled with 404s:

/              404 (text/plain)
/tools/json-formatter/  404 (text/plain)
/tools/base64/          404 (text/plain)
/tools/uuid-generator/  404 (text/plain)
...                  (100+)
Enter fullscreen mode Exit fullscreen mode

Impact

  • Visual noise: Network panel full of red 404s during debugging
  • Wasted bandwidth: Each 404 costs a Cloudflare request and response
  • Connection contention: HTTP/1.1 has 6 concurrent connections per domain; 404s steal slots from real resources
  • Cloudflare doesn't cache 404s: Every visit hits the origin

Better Approach: Create Empty index.txt

Instead of deleting, create them — let the prefetch hit a valid 200 response:

// 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'))
Enter fullscreen mode Exit fullscreen mode

Update build script:

{
  "scripts": {
    "build": "next build && node scripts/create-index-txt.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why Creating Is Better Than Deleting

Aspect Delete index.txt Create empty index.txt
Prefetch result 404 200 OK (0 bytes)
Cloudflare cache ❌ Doesn't cache 404 ✅ Caches 200
Connection contention Every request hits origin Cache HIT, instant return
Network panel Full of red All green
Blank screen risk None None (empty file won't override HTML)
File size 0 0 bytes × page count

Key insight: empty index.txt won't cause a blank screen. When Next.js client receives empty content, it knows this isn't valid HTML and automatically falls back to requesting the full index.html with Accept: text/html. Once Cloudflare caches this 200 response, subsequent prefetches are cache HITs — no wasted connections.

Lessons Learned

  1. Read your hosting platform's file matching docs — Cloudflare Pages' content negotiation differs from standard static servers
  2. Browser prefetches are a double-edged sword — They optimize load time but add edge cases
  3. Inspect the build output directoryls -la out/ to check for unexpected files
  4. Blank page → check Network first — Wrong Content-Type is often the root cause
  5. Don't fight the platform — Instead of deleting files and flooding with 404s, work with the mechanism

Project

Full build config and deployment flow at UtlKit — 150+ free online tools, Next.js 15 static export + Cloudflare Pages zero-cost deployment.


If this helped, leave a ❤️.

Top comments (1)

Collapse
 
nazar_boyko profile image
Nazar Boyko

This is a great example of working with the platform instead of against it. Deleting index.txt feels right until you realize you've traded a blank page for a pile of 404s that Cloudflare won't cache, one on every prefetch. The empty file flips it nicely, a zero byte 200 that does get cached, and the client falls back to the real HTML on its own. The fact that none of this throws an error is what makes it such a nasty one to chase down.