Build1 publisher3 min readPublished
One fetch as Googlebot exposed the empty shell behind 270 unindexed articles
Googlebot got 0 characters inside a client-rendered blog's root div, its developer found after Google crawled 270 of the posts and indexed none. A bot-only branch in an existing Netlify edge function now hands crawlers the rendered article and JSON-LD.
The Engineer · Build desk

What happened
- Search Console reports exported on 13 August 2026 showed Google had crawled 270 of the author's articles and indexed none of them.
- Requested with Googlebot's User-Agent, an article's raw HTML held 0 characters inside the app's root div and no JSON-LD, and GPTBot got the same response.
- Bots now receive the rendered article body, BlogPosting JSON-LD and conditional hreflang from the Netlify edge function that already injected og tags.
- Dead WordPress-era product URLs returned 200 and the homepage through the site's catch-all redirect line, and Google files those responses as soft 404s.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision For a client-rendered site stuck in crawled-but-not-indexed, a raw request with the crawler's User-Agent is the first test to run, before any content rewrite, because it costs one command.
- constraint Bots and readers now get the article from two different renderers, DOMPurify in the browser and escape-first at the edge, so every markdown change has to land in both and stay consistent.
- exposure Any SPA deployed with the standard catch-all line accumulates soft 404s from bad inbound links without shipping anything, until something checks whether the requested record exists.
A zero from that request settles one narrow question: what the server sends before any JavaScript runs [3]. In a single-page app the first response is a shell, and the article appears only after the bundle executes [4]. "So besides Google, the AI-search side was blind too," the author wrote [18].
Google does execute JavaScript. According to the author, executing it and waiting for it to finish before scoring the page are different promises [4]. The curl result measures server output only. It transfers to another app if that app's server also returns an empty mount point; a site that already prerenders would get a nonzero count from the same request. The post does not include a rendered view of the page or indexing figures from after the fix.
The author's first guesses were keywords, depth and backlinks, and the author estimates that direction would have cost three more months of writing [2]. "That HTML was one curl away the entire time. I wrote 270 articles before I fetched it once," the author wrote [5].
The fix needed no new architecture [7]. The og-tag function already pulled each article from the database for social previews, so the new output uses a query the site was already making [6]. Real readers never reach the branch [7].
The renderer is the best engineering in the post. The frontend's markdown renderer sanitises with DOMPurify, which needs a DOM, and the Deno edge runtime has none [8]. The edge version escapes every character first and applies structure second [8]. According to the author, that ordering makes XSS structurally impossible, so safety does not depend on the developer having been careful [9]. In my view the escape-first order is the right tradeoff for a runtime without a DOM. Raw-HTML blocks degrade to plain text, and a database query put the exposure at 1 of 1,112 published text blocks [10], about 0.09 percent [11].
I would copy the test discipline first. The change passed 28 unit tests and 12 cross-layer contract tests, 40 in all [12][13]. The author then built 9 deliberately broken variants, confirmed each one turned the intended check red, and confirmed that restoring the code turned everything green [12]. One detour cost time. The first version marked inline-code boundaries with a literal NUL byte, and Git classified the file as binary [14]. The diff was `Bin 0 -> 21694 bytes` and nothing else [14]. An escape sequence carries the same meaning and stays readable [14].
The soft-404 count is the worse number because, as the author notes, it grows by itself [16]. According to the author, every SPA deployment guide recommends the `/* /index.html 200` line, because without it refreshing any sub-path returns a 404 [15]. The same line answers every dead URL with a 200 [15]. The first of two fix layers moves `/content/:slug`, `/product/:id` and `/works/:slug` into the edge function, which checks the database record before it answers [17].
What to watch
- A later Search Console export showing whether the 270 articles leave "Crawled - currently not indexed" now that bots get the rendered body.
- Soft-404 counts once the edge function answers /content, /product and /works paths from the database record.
- Whether the second fix layer covers dead paths outside the three edge patterns, such as the /product-tag/ URL in the author's sample.