How I Added 13 Languages to My Next.js Site Without Wrecking SEO
I built 13-language support into Next.js App Router for UX, not SEO. Here's the full setup: typed translations, ISR, hreflang, noindex strategy, and what actually happened to traffic.
wonsukchoi.co supports 13 languages. Not because I expect to rank in 13 countries — most of my search traffic is in English. I built it because clients come from everywhere, and letting someone read a page in Korean, Vietnamese, or Arabic reduces friction before they reach out.
Here's how the setup actually works, and the SEO decisions that matter.
The 13 languages#
The list: English, Korean, Vietnamese, Japanese, Simplified Chinese, Traditional Chinese, Spanish, Thai, Arabic, Brazilian Portuguese, Hindi, Indonesian, French.
I picked these based on spoken language population globally, not keyword research. The goal wasn't to chase search rankings in each market. It was to cover the largest possible share of potential clients who might land on the site and prefer to read in their native language.
The architecture#
The site runs on Next.js App Router. The language lives in the URL as the first segment:
/en/blog/some-post
/ko/blog/some-post
/ja/blog/some-post
This is a [lang] dynamic segment at the top of the app directory. Every page receives lang as a param and uses it to look up the right translations.
UI strings — nav labels, section headings, CTA text, form copy — live in a single typed translations.ts file:
export const languages = [
'en', 'ko', 'vi', 'ja', 'zh', 'zh-tw',
'es', 'th', 'ar', 'pt-br', 'hi', 'id', 'fr'
] as const;
export type Lang = (typeof languages)[number];
export const t = {
en: { nav: { blog: 'blog', hire: 'hire' }, ... },
ko: { nav: { blog: '블로그', hire: '의뢰' }, ... },
// ...all 13
};
This keeps translations in-code and fully typed. If a key is missing for a language, TypeScript catches it at build time.
How I handled translations#
For UI strings, I used AI. Short, structured text with clear context translates accurately — a nav label or a CTA button has almost no ambiguity. I translated the full translations.ts file once and occasionally update individual keys when copy changes.
Blog posts are a different story. I keep all blog content in English and do not translate posts. The reason is simple: long-form translation quality degrades significantly, and publishing inaccurate translated posts hurts both credibility and SEO. Non-English blog URLs are noindexed so they don't compete with the English version or get flagged for thin content.
The SEO decisions that matter#
hreflang#
Every page outputs hreflang alternates pointing to all 13 language versions:
<link rel="alternate" hreflang="en" href="https://wonsukchoi.co/en/blog/post" />
<link rel="alternate" hreflang="ko" href="https://wonsukchoi.co/ko/blog/post" />
<!-- ...all 13 -->
This tells Google these pages are alternate versions, not duplicates. Without it, you risk having 13 versions of the same content compete against each other.
Noindex on untranslated content#
Any non-English page that has no actual translation returns robots: { index: false } in its metadata. This covers blog posts (all English-only) and any page where a translation hasn't been written yet.
The rule is: if Google can't find meaningfully different content at a URL, don't let it index the URL.
The root redirect#
Hitting wonsukchoi.co with no language prefix returns a 307 redirect to the appropriate language based on the Accept-Language header. If the browser signals Korean, you land on /ko. Default is /en.
307 (temporary) rather than 301 (permanent) is intentional — the destination changes per user, so it shouldn't be cached permanently.
Sitemap#
The sitemap includes only pages with real indexable content. Blog posts appear only once under /en/, not under all 13 language paths, since non-English blog URLs are noindexed anyway. This keeps the sitemap clean and avoids inflating it with URLs that return noindex.
ISR: one build, 13 cached versions#
Each page uses ISR with a 1-hour revalidation:
export const revalidate = 3600;
generateStaticParams returns every language × slug combination so all 13 versions of each page are pre-built and edge-cached. A visitor hitting the Korean homepage gets a cached static page, not a server render.
What actually happened#
The multilingual setup hasn't moved the needle on SEO. Most search traffic is in English, and English-language SEO effort has a much better return than trying to rank in 13 languages at once.
The benefit is UX, not rankings. Clients from Japan, Korea, Vietnam, and the Middle East have reached out and mentioned reading the site in their language. That's the actual value — reducing the friction between a cold visit and a contact form submission.
The honest setup advice#
If you're considering this for your own site:
- Do it for UX, not SEO, unless you're specifically targeting non-English markets with localized content
- Translate UI strings with AI — the accuracy is high enough for short copy
- Do not auto-translate long-form content and index it — it's a quality and credibility problem
- Get hreflang right before launching — it's easy to get wrong and hard to audit after the fact
- Noindex any page that doesn't have real translated content
- Keep one canonical language as your SEO priority and treat others as UX improvements
Freelance
¿Necesitas ayuda con esto?
Puedo ayudarte con migraciones, nuevos productos y rendimiento web.
Contactar →