A 133-page site. Bing indexed zero of it. Not because the content was bad, not because it was never submitted — because every non-existent URL on the site returned HTTP 200.
Google had indexed 57% of it. Bing had indexed none. Bing only started to respond after the fix.
Worth writing down, because the cause was not an SEO technique. It was a single field in a config file that no one thinks to check.
Symptoms
pureip.app is an IP diagnostics toolbox: Cloudflare Workers plus a single-page app. The symptoms looked exactly like "new site, no traffic":
- Bing
site:query returned 0 results - Google Search Console, previous 14 days: 3 clicks, 48 impressions
- Index coverage check: 73 / 127 = 57% (Google side)
- Bingbot visited every single day — 1 to 9 hits per day, 72 in seven days
That last line is the important one. The crawler was visiting. The pages were reachable. They just weren't being indexed.
Five things that were not the cause
Five suspects, checked in order, all clean:
| Check | Result |
|---|---|
| robots.txt | Allow: /, Bingbot not blocked |
| Sitemaps | 8 sitemaps, all HTTP 200, 133 URLs in total |
| Page content | SSR returns 8,424 bytes of HTML containing 929 characters of rendered text — not a JS-only shell |
| IndexNow submission | Automated daily at 09:35, returns HTTP 200 |
| Bingbot fetching | Homepage 200, sitemap 200, daily visits |
All five green. That is what makes this class of bug hard: every check passes and the result is still zero.
Root cause: one line
This line in wrangler.toml:
[assets] not_found_handling = "single-page-application" It is the standard Cloudflare Workers SPA setting. What it does: any unmatched path returns index.html with status 200.
For a single-page app that is correct — client-side routing depends on it. The cost:
/wp-admin → 200 (returns the homepage) /this-page-does-not-exist → 200 (returns the homepage) /phpmyadmin → 200 (returns the homepage) /anything-you-type → 200 (returns the homepage) My client router does render a "404 — page not found" component. But the HTTP status code is 200.
From a crawler's point of view, that is an unlimited supply of identical 200-status pages.
Google absorbs some of this on its own — it indexed 57%. Bing penalises soft 404s far more aggressively than Google, and will reject the entire domain.
How to confirm it
A useful diagnostic: look at what Bing actually returns for a site: query.
If a domain is merely unranked, site:example.com returns zero results or your own pages. What it actually returned was:
hobbylobby.com (first test) seaofthieves.com (second test) Bing dropped the site: filter entirely and substituted unrelated sites. That is confirmation that no page from the domain is in the index at all — not "not ranking", but "not present".
One near-miss worth mentioning. Bing's HTML contains the string There are no results, which reads like a "no results" notice. Running the identical query against site:cloudflare.com — a domain that is certainly indexed — that string still appears. It is Bing's own JavaScript template text, not a signal.
So do not use There are no results as evidence. Check which domains Bing actually returned.
The fix
Add a top-level path allowlist in the Worker; anything not on it gets a real 404:
const KNOWN_TOP = new Set(["", "ai", "ai-check", "ai-unlock", "all", "api", "articles", "assets", "browser", "cdn", "claude", "compare", /* ... 45 segments */]); if (!/\.[a-z0-9]{1,5}$/i.test(P) && !KNOWN_TOP.has(P.split("/")[1].toLowerCase())) { return new Response("Not found", { status: 404 }); } The hard part is not those lines. It is completing the allowlist. Miss one legitimate path and you de-index pages that were already indexed. I cross-checked five sources:
- The full sitemap — 133 URLs
- The site's own
/allindex page — 93 internal links - The
App.tsxroute table (including:ip-style dynamic params) -
legacyRoutes— old redirect rules, 22 entries - Path prefixes the Worker handles itself (
/api/,/go/,/worker/…)
Before deploying, run a two-way regression:
- 162 legitimate paths must all return 200 (zero false positives)
- 19 junk paths must all return 404
On the first deploy, several paths flipped between 200 and 404 — edge nodes were still rolling out the new version. It stabilised after about 90 seconds of propagation.
Two real bugs fixed along the way
1. Legacy status pages were being killed. /gpt/status.html and /claude/status.html are legitimate old links in legacyRoutes that should 301 to /status/openai. Because they carry an .html extension, the earlier extension rule was 404-ing them. The project's own test suite had a backend source paths are never served case failing on exactly this and it had been ignored. Fixed together with the rest; the suite went from 111 pass / 10 fail to 142 pass / 0 fail.
2. A brittle assertion in the test suite. One test read:
tree.props.children[1].props.children[0].length A later change added a <HomePageSearch /> component, which took over children[1], shifting every index and making the test throw Cannot read properties of undefined.
The functionality was fine — the deduplication logic works. I rewrote the traversal to find nodes by predicate instead of positional index, and added the missing negative case:
assert.equal(merged, 1, "same IP should collapse to 1 card"); assert.equal(distinct, 2, "different IPs should produce 2 cards"); The old test only verified "same IP merges". It never verified "different IPs produce two cards" — so if deduplication had swallowed everything, the old test would still have passed. After the rewrite I mutation-tested it: delete the dedupe line and the assertion fails immediately, confirming it is not vacuous.
Reusable lessons
1. "All checks pass" is itself a clue. robots fine, sitemap fine, crawler visiting daily, submission returning 200 — five greens and zero indexed means the problem is lower down than you are looking.
2. Nobody proactively checks not_found_handling. It is not in your code, not in your logs, only in wrangler.toml. If an SPA has an indexing anomaly, look at that line first.
3. Separate "genuinely zero indexed" from "broken measurement". Curling a search page and regex-counting results will report "0 indexed" whenever the HTML structure changes. Check three things together: are there results for your own domain, are there unrelated results, is the page empty. Those three mean completely different things.
4. Always regression-test both directions after the fix. Verifying "junk now 404s" is not enough. Killing a legitimate page costs more than the junk was worth.
5. Wait for propagation after deploying. During edge rollout the same path returns 200 and 404 alternately. Do not draw conclusions from a single probe.
Result
Deployed (Version a6c9773b), then re-submitted all 133 URLs through IndexNow.
Google-side coverage was unaffected — 162 legitimate paths all 200, zero collateral damage.
Bing — still zero at the time of writing. Index updates work on a daily scale; I am still waiting.
If it is still zero in four weeks, the problem was elsewhere, and there will be a follow-up post saying so.
Tool: PureIP — check an IP before you buy a VPS: datacenter vs residential, blocklists, reverse DNS, AI-service reachability. Free, no signup.
Every figure in this post comes from the site's own public endpoints and logs and is reproducible.
