EEltaCrew
HomeBlog

One language per page: why I dropped IP-country detection and bilingual pages for browser language plus hreflang

Published 2026-09-16Measured 2026-08-28About 3 min readelta · EltaCrew

Six web services each picked their initial language differently. The one using IP country served English to Googlebot only, so the search-result language could not be controlled; the landing page mixed Korean and English on one page, against Google's "one language per page" expectation. How I unified every site under one rule.

Six sites, six ways to pick a language

Each of the six web services decided its first language differently.

Each had a reason, and each was a problem.

The problem with IP country

A Korean resident on an English browser gets Korean. That is tolerable. The real problem is Googlebot. It crawls from the US, so it always received the English page. There was no way for a Korean title to appear in Korean search results. The server had no control over which language went into search. I deleted langFromCountry().

Time-zone detection went for the same reason: if the client decides in JS, the first HTML a crawler sees is the pre-decision state.

The problem with bilingual pages

The root landing had a Korean paragraph and an English paragraph next to each other. I suspect it came from misreading an early ad-network rejection as "not enough text" and padding the page. But Google expects one language per page. A mixed page is ambiguous about which language's results it belongs in, and lang can only take one value.

The unified rule

Set as the standard for every site on 11 August 2026, with the root, the last exception, brought in line on 28 August.

  1. Initial language comes from the browser: Accept-Language starting with ko means Korean, anything else English. Server-rendered sites read the header; static sites decide on first load in JS; either way, each language has its own URL.
  2. Separate URLs per language. / is Korean (lang="ko"), /en/ is English (lang="en"). The converter site was split into /mdhtml/ and /mdhtml/en/ too.
  3. Both pages carry hreflang pointing at each other, x-default points at Korean, and each has its own self-canonical and og:locale.
  4. A language-switch link in the header, so the user can fix a wrong auto-detection in one tap.
  5. Both URLs go into the sitemap.

For the root I kept the bilingual file as the source and wrote a script that splits the Korean and English sentences of its 22 cards (16 apps + 6 web services) into two generated pages. The bilingual file survives only as a backup.

Fonts unified the same day

I also collapsed fonts to one. A single dynamic-subset CSS for the Pretendard variable font, and nothing else. Out went Geist on the portfolio, Clash Display on showcase headings, Inter + Space Grotesk on the directory, Roboto + Noto Sans KR on the converter. Different fonts per page and it stops looking like one site.

The exception is the monospace font for code. Pretendard is proportional, so code alignment breaks, and the web SSH client is an xterm.js terminal that falls apart without monospace. JetBrains Mono stays.

Verification

On the live site I fetched each page with curl -H "Accept-Language: en" and ko, checked <html lang> and the language of the first paragraph, confirmed the two pages' hreflang point at each other, and that both are in the sitemap. Search results follow after re-crawl, so that part is a matter of waiting.

Summary

webSEOlocalizationhreflang

Related

A 301 cannot be taken back: how 43 minutes of a wrong redirect came back as a 404 five days later, plus the Cloudflare edge-cache incidentMoving two web services to subdomains, a broken 301 went out for 43 minutes. The server was fixed at once, but the browser was still following that 301 into a 404 five days later. What to do before emitting a permanent redirect, the only way to rescue one already sent, and the day a deploy "did nothing" because Cloudflare's edge kept the old CSS.Why switching language inside the app sometimes does nothing: setApplicationLocales only recreates activities that are on screen"I switched to English and it stayed Korean until I restarted the app." The cause is that AppCompat recreates only the activities that are started at that moment; screens that were stopped behind it come back in the old language. The fix I applied to 8 apps, and five traps that came with it.