The route nothing imports: how eleven broken endpoints hid behind one Search Console error
A Next.js file convention generated eleven locale routes that had never returned a 200. The only monitor that noticed was Google's crawler, and it had only tried one of them.
The Engineer · Build desk

What happened
- Google Search Console flagged a single 5xx URL on a site of roughly 1,700 pages, which is easy to read as noise.
- Checking by hand showed the generated OpenGraph image route returning 502 on all eleven locales, and it had never worked on any of them.
- Because the image response streams, Next closed the connection mid-body and nginx converted that into a 502 for the client.
Why it matters
- constraint A route created by file naming rather than by code sits outside every check most teams own: no import to type-check, no link to crawl in a smoke test, no page for a text-grep deploy gate.
- exposure The pages that inherited the broken route were the boring ones nobody previews, privacy and terms, so the visible damage happened on other people's link cards rather than in any log the owner reads.
- decision It changes how you read a count of one in a crawler report on a parameterised route: the cheap move is to enumerate the parameter rather than triage the single URL.
- precedent Any streamed generated response can turn an application-level render error into a proxy-level gateway error, which sends the next on-call engineer to the wrong layer first.
The mechanism worth taking away is the gap between what generates a route and what exercises it. `app/[locale]/opengraph-image.tsx` is a file convention: you do not import it, you do not link to it, and no page renders it, according to the author's account [2]. Its only output is a meta tag pointing at a route Next builds on your behalf [2]. So the normal feedback loop has nothing to grab. Clicking through the site shows nothing, because no page requests the image [3]. The build compiles, because the failure happens per request at render time [4]. Deploy checks that fetch pages and grep for text skip it, because it is not a page [5]. And the user-visible symptom lands on someone else's server: a blank preview card on a social site [6].
The failure itself is small enough to be invisible in review. `next/og` renders through Satori, which requires a div with more than one child to declare `display: flex` or `display: none` [7]. The offending line reads as one sentence to a person and as two JSX children to the compiler: the expression container and the adjacent text node [8]. Every other div in the file either declared display or genuinely had one child, so the broken one looked like its neighbours [9].
Then the diagnosis gets pushed two layers away from the code. `ImageResponse` streams, so by the time Satori throws, headers are sent and the body has started; Next cannot render a 500 page and closes the connection [10]. nginx, still waiting on the upstream, logs a premature close and returns 502 [11]. That is infrastructure vocabulary. It sends you to memory, to the process, to the proxy config, when the cause is a JSX child count [12].
The fix the author chose was not the one the error message asks for. Wrapping the content in a single template literal makes it one text node, so the multi-child rule never applies [13]. Adding `display: flex` also works, but it turns the text into a flex item and changes how it wraps against the `maxWidth: 1000` [14]. One of those fixes the crash and leaves layout alone.
The blast radius was not uniform, and that is the part the author says turned a curiosity into a bug. Pages that set `openGraph.images` explicitly override the file convention and were fine; pages that did not, including the privacy policy and terms pages in all eleven locales, inherited the broken route and published a 502 as their og:image [15]. That is 22 pages serving a dead image reference on a site of roughly 1,700 pages [22][1], which is why the sampling logic matters more than the ratio: Search Console reports what it happened to crawl, so one 5xx on a locale-parameterised route means at least one [16]. A for-loop over the locale list took the count from 1 to 11 [17].
The durable lesson is the checking discipline, and it is unglamorous: curl the route after you touch it, because nothing in the normal loop covers a file nothing imports [18]. After the change, all eleven locales returned 200 with a 244,560-byte PNG at 1200x630 [19].
What to watch
- Whether Next or Satori gains a way to surface a streamed render failure as a real error response instead of a closed socket, moving the symptom out of the proxy layer.
- Whether deploy checks in projects like this add per-locale curls for file-convention routes rather than page fetches only.
- Whether social caches holding the failed cards for the privacy and terms pages refresh now that the route returns 200.
Clarity's read
What the record supports and how the coverage leans. The claims behind it follow.
Reality
- Evidence54
- Adoption14
- Hype gap+10
- Incentives32
- Confidence46
Claim ledger
Ranked by verification strength, evidence, and original report placement.
- [1]
Search Console showed one URL in the "Server error (5xx)" bucket on a site of about 1,700 pages.
- [2]
app/[locale]/opengraph-image.tsx is a Next.js file convention: it is not imported, not linked, and no page renders it; its job is to make Next inject a <meta property="og:image"> tag pointing at a route it generates.
- [3]
The browser could not have caught it, because no page requests the image and clicking through the site shows nothing wrong.
- [4]
The build could not have caught it: the code compiles fine and the failure is at render time, per request.
- [5]
The author's deploy checks fetch pages and grep for text, and this route is not a page.
- [6]
The failure surface for a user is a social preview card being blank on someone else's site, so no user report was likely.
- [7]
next/og renders with Satori, which implements a subset of CSS and requires a <div> with more than one child node to have explicit "display: flex" or "display: none".
- [8]
The breaking div contained {TOOL_COUNT} followed by the text "+ Free Online Text Tools", which reads as one sentence but is two JSX children: an expression container and a text node.
- [9]
Every other div in the file either declared display: flex or genuinely had one child, so the broken div looked exactly like the others.
- [10]
ImageResponse streams, so by the time Satori throws, Next has already sent response headers and started the body and cannot turn the failure into a 500 page; it closes the connection and logs "failed to pipe response".
- [11]
nginx, waiting upstream, logs "upstream prematurely closed connection while reading response header from upstream" and returns 502 to the client.
- [12]
The outward symptom is a gateway error, the vocabulary of infrastructure problems, sending the operator to look at memory, the process and the proxy config, while the actual cause is a JSX child count.
- [13]
The author's fix was to wrap the content in a single template literal, since one template literal is one text node and the multi-child rule never applies.
- [14]
Adding display: flex also works but makes the text a flex item, changing how it wraps against maxWidth: 1000; the template literal fixes the crash and changes nothing about the layout.
- [15]
Pages that set openGraph.images explicitly override the file convention and were unaffected; pages that did not, including the privacy policy and terms pages in all eleven locales, inherited the route and published a 502 as their og:image.
- [16]
Search Console reports only what it happened to crawl, so one 5xx URL on a multi-locale route means "at least one" and should be treated as a sample, not a total.
- [17]
Checking the other locales showed 502 on en, ko, th, ja and de among all eleven; the route had never worked on any locale, and a for-loop over the locale list turned 1 into 11.
- [18]
The author's first recommendation is to curl the route after touching it, because it is the only check that exists for a file nothing imports.
- [19]
After the fix, all eleven locales returned 200 image/png, 244560 bytes, 1200x630 in production.
- [20]
The author suggests curling a page and grepping for the og:image meta tag, then using an Open Graph previewer to see the card a crawler would build.
- [21]
The site involved is textmachine.org, a set of text tools that do their work in the browser.
- [22]
Two page types (privacy policy and terms) across eleven locales gives 22 pages publishing a 502 as their og:image.
Get this ledger inside ChatGPT. Ask about any of our stories and the answer brings its figure, each claim’s verdict and the sources. Free, no account.
Sources
1 independent publisher whose own reporting we read for this story.
- dev.toSearch Console found one 502. All 11 locales were broken, and no test could have caught it.
1 article · August 22, 2026
Topics and entities
Follow any of these and your For You feed starts watching them — no settings page required.