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
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→ matchesindex.html✅ -
GET /+Accept: text/plain→ matchesindex.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!
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+)
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'))
Update build script:
{
"scripts": {
"build": "next build && node scripts/create-index-txt.js"
}
}
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
- Read your hosting platform's file matching docs — Cloudflare Pages' content negotiation differs from standard static servers
- Browser prefetches are a double-edged sword — They optimize load time but add edge cases
-
Inspect the build output directory —
ls -la out/to check for unexpected files - Blank page → check Network first — Wrong Content-Type is often the root cause
- 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)
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.