480 Pages, 40 Locales, Zero Frameworks — How I Beat Next.js at International SEO

Let me start with a fact that should terrify every SPA developer who cares about international SEO.
Googlebot's rendering queue can take days to weeks to process JavaScript.
During the initial crawl, Google sees your raw HTML. If your hreflang tags, meta descriptions, and structured data are injected by React at runtime, there's a window — potentially a long window — where Google doesn't see them.
For a platform operating in 40 locales across 12 public pages, that's 480 pages of potential SEO blindness.
When I built mentoring.oakoliver.com, I made one hard rule: every piece of SEO-critical markup must exist in the static HTML the server sends. No client-side rendering. No hydration dependency. No "Google will eventually see it."
If the UNIX utility curl can see it, Google sees it. Period.
The result: 480 pre-generated HTML files. Each with locale-specific title tags, meta descriptions, hreflang links for all 40 locales, Open Graph markup, Twitter Card metadata, and Organization schema.org structured data. All baked in at build time. Zero JavaScript required for any of it.
I – Why 40 Locales and Not Just 28 Languages
Our platform supports 40 locale variants spanning 28 languages. English in three regional flavors — US, UK, and Canadian. Spanish in six — Castilian, Mexican, Argentine, Chilean, Colombian, Costa Rican. Portuguese in two — Brazilian and European. Chinese in two — Simplified and Traditional.
Why distinguish between regions that speak the same language?
Because locale-specific SEO matters. A user searching in Mexican Spanish should land on a page with Mexican Spanish metadata, not Castilian Spanish. Google's hreflang system is designed precisely for this — to serve the right regional variant to the right searcher.
Including Brazilian Portuguese and European Portuguese as separate locales means a Brazilian user searching in Portuguese gets metadata tuned for them. "Plataforma de Mentoria" instead of "Plataforma de Orientação." Same language, different vocabulary. Different search intent.
The distinction matters at the margins. And SEO is margins.
II – The hreflang Specification Done Right
hreflang tells search engines: "This page exists in these other languages and regions. Here's where each version lives."
The specification is deceptively simple. Each localized page must contain link tags pointing to every other version of itself — including itself. For 40 locales, that's 41 link tags per page. Forty locales plus one "x-default" fallback.
Most implementations get the codes wrong.
This is a critical detail. The hreflang attribute uses language-region codes, but the rules aren't always intuitive.
German maps to just "de" — not "de-DE." Because there's only one German variant in our set, the region code is unnecessary, and Google actually prefers the simpler form.
Chinese Simplified maps to "zh-Hans" — not "zh-CN." Google's hreflang documentation explicitly recommends script subtags for Chinese. "zh-Hans" for Simplified and "zh-Hant" for Traditional distinguish by writing system, which is what users actually care about.
Spanish variants need their full region codes. "es" for Castilian, "es-MX" for Mexican, "es-AR" for Argentine. These are genuinely different content variants.
Getting these mappings wrong means Google either ignores your hreflang tags entirely or serves the wrong regional variant. We spent a full day getting them right, cross-referencing Google's documentation and the IANA language subtag registry.
One day of research saved us from being invisible in half the world's search results.
III – The Build-Time Generation Pipeline
At build time, the compiler iterates over every public route and locale combination and generates a complete HTML file. Twelve routes times 40 locales equals 480 HTML files.
All generated in roughly 200 milliseconds.
The generation is fast because it's string concatenation — not React rendering, not DOM manipulation. Template literals and string replacement. Each file is about 3-4KB of HTML template with all its meta tags.
The output structure is clean. A directory per locale, a file per route. The English US directory has an index, mentors, about, FAQ, and every other public route. The Portuguese Brazil directory has the same structure with Portuguese metadata. Forty locale directories in total.
Total disk usage: about 1.8MB. Total in-memory cache: about 1.8MB. The Bun process uses roughly 80MB total. The templates are less than 2.5% of memory. Serving any locale variant is an O(1) map lookup.
What if we grew to 100 public routes? That would be 4,000 HTML files and about 15MB in memory. Still negligible. The pattern scales linearly with a small constant factor.
IV – Locale-Specific Metadata Without a Translation Framework
Each route has metadata configurations for 9 primary language families — English, Portuguese, Spanish, German, French, Italian, Japanese, Russian, and Chinese. Other locales fall back to English.
The mentors page in English says "Find Expert Mentors." In Portuguese: "Encontre Mentores Especialistas." In Spanish: "Encuentra Mentores Expertos." In German: "Finden Sie Experten-Mentoren."
The locale-to-language mapping uses prefix matching. All Spanish variants — Mexican, Argentine, Chilean, Colombian, Costa Rican — share the same metadata. The differences between Mexican and Argentine Spanish meta descriptions are negligible for SEO purposes.
If we needed truly distinct metadata per regional variant, we could expand the config. But for now, language-level grouping is sufficient. You can always add granularity later. Starting with too much granularity means maintaining 40 slightly different versions of the same sentence.
Premature specificity is the root of all translation debt.
V – The Canonical URL Trap
Each localized page points to itself as canonical.
This is where many developers make a devastating mistake. They point all locale variants to the English version as canonical. This tells Google: "These pages are duplicates — only index the English one."
That defeats the entire purpose of localization.
Each locale variant is its own canonical URL. The hreflang tags tell Google they're related versions, not duplicates. Google uses the hreflang tags to choose which version to show each searcher based on their language and region. But it can only show versions that are indexed. And canonical tags control what gets indexed.
Self-referencing canonicals. Always. For every locale.
VI – Organization Schema: Feeding the AI
The homepage includes Organization schema.org markup in a JSON-LD script tag. Company name, URL, logo, founding date, founders, social media profiles, and contact information.
The contact point's "available languages" field lists all 40 locale names. This tells Google — and AI assistants like ChatGPT's browsing feature — that the platform is available in these languages. It's a signal that can influence which language variant surfaces in search results.
Schema is only on the homepage. Adding it to every page would be redundant. Google associates Organization schema with the domain, not individual pages. Keep it on the page most likely to be crawled first.
In 2026, structured data isn't just about search engines anymore. AI-powered assistants consume it too. When someone asks an AI "find me a mentoring platform that supports Japanese," the schema.org markup is what the AI reads to answer that question. Your invisible SEO layer is now your AI discovery layer.
VII – Open Graph: The Social Sharing Minefield
When someone shares a mentoring page on social media, the Open Graph tags determine the preview. Each locale variant needs its own OG tags with the correct title, description, URL, and — this is the one everyone gets wrong — locale code.
The Open Graph locale format uses underscores, not hyphens. Facebook's specification says "pt_BR" not "pt-BR." A tiny inconsistency between standards that causes silent failures. Your page will still share, but Facebook might misidentify the language and show the wrong preview to users.
A one-character difference. A completely different result.
Shipping a product to global markets? I've been building multi-locale platforms for years and run mentoring sessions on international architecture at mentoring.oakoliver.com. If you're building AI micro-apps for a global audience, check out vibe.oakoliver.com — 99 subdomains, all served from one VPS.
VIII – RTL Languages: The Layout Mirror
Hebrew and Arabic are right-to-left languages. The HTML template sets the lang attribute appropriately — "he" for Hebrew, "ar" for Arabic.
Browsers and screen readers use this attribute to apply text direction and font rendering. On the client side, the application detects RTL locales and sets the document's direction attribute, flipping the entire page layout. Buttons move to the left. Text aligns right. Navigation mirrors.
But for SEO purposes, the static lang attribute in the HTML is what matters. Crawlers don't execute JavaScript to determine text direction. They read the HTML attribute.
This is a recurring theme in our architecture. The static HTML handles crawlers. The client-side JavaScript handles humans. Two separate concerns, two separate mechanisms.
IX – The x-default Safety Net
Every hreflang set includes an x-default entry pointing to the English version.
x-default tells search engines: "If the user's language doesn't match any of my 40 locales, send them here."
Without x-default, users searching in unsupported languages — Swahili, Bengali, Tagalog — might not be served any version of the page. Google wouldn't know where to send them. The x-default ensures there's always a landing point.
It's one extra link tag. It covers every language you haven't explicitly supported. The ROI is infinite.
X – Auth Pages: Know What NOT to Index
Not every page should be indexed. Authentication pages, dashboard routes, and settings pages use noindex directives.
Auth pages don't get locale variants either. There's no SEO value in having a Portuguese-localized sign-in page. A single English version with noindex is sufficient.
Knowing what not to index is as important as knowing what to index. Crawl budget is finite. Every page Google spends time crawling is a page that could have been a high-value public page. Don't waste crawl budget on your password reset flow.
XI – Why Not Next.js?
Next.js would give us static generation with locale support out of the box. But it would also mean adopting the entire Next.js build pipeline, server runtime, and deployment model.
Our Bun plus Elysia stack is simpler. It's faster to build. It gives us total control over the HTML output, the build process, and the deployment.
Client-side i18n frameworks like i18next or react-intl are excellent for translating UI strings at runtime. But they don't help with SEO. hreflang tags and meta descriptions need to be in the HTML before JavaScript executes. You'd still need server-side generation for SEO even if you used a client-side i18n library.
Our approach decouples SEO from runtime internationalization. The static HTML provides SEO markup. The client-side application context provides runtime translations. They're independent systems that can evolve separately.
The SEO layer is completely invisible to the React application. Components don't know about hreflang tags, canonical URLs, or Organization schema. They don't import an SEO library. They don't call a head management hook.
The SEO markup exists in the HTML template before React even loads. It's a build-time concern, not a runtime concern. This separation means SEO updates don't require React code changes. New locales are added by extending an array and adding metadata. The React bundle is smaller — no SEO libraries. And SEO output is deterministic — the same build always produces the same markup.
XII – The Invisible Layer That Drives Global Traffic
For a mentoring platform targeting 40 global markets, this invisible SEO layer is the difference between organic traffic from Brazil, Japan, and Germany — and being invisible outside the English-speaking world.
The approach is brutally simple. Generate HTML at build time. Include every SEO tag in the static output. Let the client-side application handle everything else. No framework required. No SSR. No hydration mismatches.
480 pages. 200 milliseconds to generate. 1.8MB of memory. And every single one is perfectly optimized for its target locale.
The tools for international SEO have existed for over a decade. hreflang is not new. Schema.org is not new. Open Graph is not new. What's new is the assumption that you need a framework to use them.
You don't. You need a build step, a locale configuration, and the discipline to keep SEO in the HTML where it belongs.
How many locales does your product support — and are you confident Google is actually indexing all of them?
– Antonio