Build1 distinct publisher2 min readUpdated
A Next.js 15 tool site on Cloudflare Pages tried a query parameter first. The cache and the search index both key on URL, and a static export has no runtime left to fix it.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
The query parameter loses because two systems read the URL and nothing else, and neither of them is part of the application. Search engines fold `?lang=zh` and `/` into one page, so the Chinese version arrives as duplicate content [6]. People then share the link without the parameter attached, which means the shared copy is the default language whatever the recipient speaks [8].
On a server-rendered deployment none of this is terminal. You add the query to the cache key, or you rewrite the request before it reaches the cache. A static export removes both, and the same missing runtime shows up in the writeup as its own separate bug: `redirect()` on the root path did nothing, because `output: 'export'` has no server to execute it and Next.js skipped it in silence [13]. The replacement was an inline script reading `navigator.language` and calling `window.location.replace`, with a `noscript` meta refresh behind it [14]. That is the shape of every recovery path when the only code you can run is code the browser downloads.
Here is the arithmetic the pros-and-cons lists skip. The site is 100+ pages [c1b]. Put the locale in the path and `generateStaticParams` prerenders 200+ URLs at two languages, each one its own object in the CDN [11][2]. Keep the locale in the query and you have roughly 100 objects being asked to serve an audience split about evenly between China and everywhere else [2][2], with the author reporting that the copy without the query is the one that wins the cache [7]. Nothing in the application is broken. The edge is simply answering a question it was never given.
The other four failures in the same writeup are cheap by comparison. Two layouts each rendering `html` and `body` produced nested documents and doubled hydration [12]. `params` arrived as a Promise, so `generateMetadata` read `locale` as undefined and shipped English titles under `/zh-CN/` [15]. A `setLocale` that only touched React state changed the URL and left the content alone [17]. All of those fail loudly, on first load, on the machine that built them.
The URL decision fails somewhere else: in an index you do not control and a cache you do not own, on a delay. It is also the one decision the tooling cannot express without you. `alternates.languages` and a per-locale canonical only mean something when there is a distinct URL to point them at [16]. Pick the query string and you have built a site whose language variants cannot be described to the only two consumers that matter.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The author built utlkit.com, a 150+ tool website, with Next.js 15 deployed on Cloudflare Pages, and added internationalisation to it.
According to the author, half the site's traffic came from China and half from elsewhere.
The author expected the i18n work to take a day; it took four days and the approach was rewritten three times.
The author went through query string, then subdirectory, then path prefix, before settling on the Next.js [locale] route segment approach.
The query string approach used URLs of the form /tools/bmi-calculator/?lang=zh-CN and required minimal code changes, just reading the query parameter.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
First-hand code, single unverified source
The mechanics are documented at code level — root vs locale layout, awaited props.params, setLocale hard navigation, the client redirect script — which makes them inspectable and reproducible by a reader. But the cluster has exactly one source and one publisher, and the outward claims (search-engine deduplication of ?lang= variants, GitHub/Stripe as exemplars) carry no data or citation. No before/after metrics of any kind are published.
One self-reported site
The only adoption fact is the author's own deployment of bilingual [locale] routing on utlkit.com. The pattern is claimed to be widespread, but the cluster supplies no usage counts, no other implementers, and no traffic or index outcomes after the change.
Mildly overstated at the edges
The implementation core is well matched to its evidence — the pitfalls are demonstrated in code and the fixes are unglamorous. Overstatement sits in the framing: an absolute 'never do this' verdict and an SEO-disaster diagnosis rest on no measurement, and 'industry best practice' is asserted rather than shown. The headline promise of six pitfalls is delivered, so the gap is small and localised rather than systemic.
Self-promotional build log
The post is authored by the operator of the site it describes, names and links that site repeatedly, and is published on a developer platform where such write-ups build personal and product visibility. That is a mild but real promotional incentive shaping the 'here is how I solved it' framing; there is no disclosed vendor sponsorship, paid placement, or commercial relationship with Next.js or Cloudflare in the supplied material.
Confident on mechanics, thin on scope
Confidence is reasonable for the framework-specific behaviours because they are shown in code and are the kind of thing a reader can verify in minutes. It is low for anything beyond this one site: single publisher, single member, self-reported outcomes, no metrics, and generalisations about search engines and other companies that the cluster cannot check.
build
A build step instead of a backend: 1,025 records, 8 locales, no runtime API1 distinct publisher
invest
The AI deal frame flipped: buy at 15 times revenue, pay with paper marked at 401 distinct publisher
build
Your agent's retry logic is reading a timeout as a fact it does not have1 distinct publisher
build
Stripe webhooks are not exactly-once, and the standard Next.js handler assumes they are1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 23, 2026