← the writing notes 9 min

Next.js i18n in 2026: Routing, hreflang, and SEO

Next.js i18n looks straightforward until your German pages get indexed under the wrong locale or your hreflang tags cancel each other out. Here's how I actually set it up without breaking SEO.

Worn paper map of Europe on a wooden desk with an espresso cup, overcast editorial light, 35mm film grain

Back in 2023 I shipped a multilingual e-commerce site for a Berlin-based client. Twelve languages, App Router, everything felt tidy. Three months later their German product pages were ranking in the UK SERPs and their English pages were getting served to users in Munich. The hreflang tags were technically present. They were just wrong in a way that took me an embarrassing afternoon to unpick. That experience taught me more about Next.js i18n than any documentation page ever has.

This is not a "here's the official docs summarised" post. It's what I've learnt building multilingual sites at Seahawk Media, what bites people repeatedly, and the specific decisions that actually affect rankings in 2026.

---

The App Router Changed Everything (And Not All of It for the Better)

The Pages Router had built-in i18n support. You dropped a block into next.config.js and Next.js handled locale detection, sub-path routing, and Link component locale switching almost automatically. It wasn't perfect, but it was opinionated, which meant fewer footguns.

The App Router removed native i18n routing entirely. No i18n key in next.config.js. No automatic locale detection. You are now responsible for the full routing logic, and that responsibility is where most teams quietly make a mess.

The blessed approach in 2026 is a [locale] dynamic segment at the root of your app/ directory. Something like app/[locale]/page.tsx . Pair that with next-intl or i18next via react-i18next, a middleware file that reads Accept-Language headers and redirects accordingly, and you have a working foundation.

Why Middleware Gets Misunderstood

The middleware file is doing two separate jobs that people conflate. First, locale detection: reading the Accept-Language header (or a cookie, or a URL prefix) and deciding which locale to serve. Second, redirecting or rewriting the URL so the user ends up at /de/produkte rather than /produkte with some hidden locale state.

Both need to happen before the page renders. That sounds obvious. But I've seen teams run locale detection inside a React context provider, which means the initial HTML served to Googlebot has no locale signal at all. The bot sees a page, it indexes it under the root URL, and suddenly you have a canonical mess.

Keep locale resolution in middleware. Always.

---

Sub-path vs Sub-domain: Pick One and Commit

The eternal debate. /en/about vs en.yoursite.com/about . For most projects I recommend sub-path routing, and here's why: it concentrates your domain authority. Every link pointing at your .com benefits all locales. With sub-domains you're effectively running separate sites in Google's eyes, which means you need to build authority for each one independently.

There's one exception. If you're in a market where the local TLD matters enormously for trust (.de in Germany, .fr in France), a country-code TLD beats both sub-paths and sub-domains for perceived local credibility. That's not an SEO claim, it's a conversion claim. I've seen A/B tests where .de vs yourbrand.com/de moved trust metrics meaningfully even when rankings were identical.

But you can't run a ccTLD setup inside a single Next.js app easily. So for 99% of projects: sub-paths, one app, one domain.

---

hreflang: The Tag That Lies to You

This is where I see the most damage. hreflang is not hard conceptually. It tells search engines: "this page has equivalents in other languages, here they are." In practice, the implementation has three specific failure modes that I keep encountering.

Failure Mode 1: The Tags Are Not Reciprocal

Every page in your hreflang set must reference every other page, including itself. If /en/about has an hreflang pointing to /de/about but /de/about does not have an hreflang pointing back to /en/about, Google ignores the whole set. Not penalises. Ignores. As if the tags don't exist.

Seahawk had a fintech project last year where 40% of their hreflang tags were non-reciprocal because two developers had independently implemented them in different components without coordinating. The fix took an hour. The rankings recovery took two months.

Failure Mode 2: The x-default Tag Is Missing or Misplaced

x-default is for pages that don't target a specific language or region. Typically your root / or a language-selector page. Some teams put x-default on their English page, which is fine if English is genuinely your fallback for all unmatched locales. But if you serve a different page to users whose language you don't support, x-default should point there.

I've seen x-default omitted entirely. Google doesn't throw an error. It just makes its own decision about which version to show for unmatched queries, and that decision is often wrong.

Failure Mode 3: Trailing Slashes and Canonical Conflicts

Your hreflang tag says https://yoursite.com/de/ueber-uns . Your canonical tag on the same page says https://yoursite.com/de/ueber-uns/ (note the trailing slash). Google treats those as different URLs. Your canonical and hreflang are now pointing at different things, and you've created a signal conflict that quietly suppresses rankings without any visible error in Search Console.

Set trailingSlash in next.config.js and be consistent everywhere. Check it. Actually check it by curling your own URLs and reading the output.

The Google Search Central documentation on hreflang is genuinely worth reading in full. It's one of those rare official docs that's specific enough to be useful.

---

Generating hreflang at Scale in Next.js

For a 5-language site you could write hreflang tags by hand. For 12 locales across 3,000 product pages, you need a systematic approach.

Here's what I use:

  1. Define your locale list once, in a single config file. Something like locales.config.ts exporting an array: ['en', 'de', 'fr', 'es', 'nl'...].
  2. Build a utility function getHreflangAlternates(pathname: string) that takes a locale-agnostic path (/about ) and returns an array of { hreflang, href } objects for every locale.
  3. Call that function in your root layout.tsx and inject the results via Next.js's metadata API using the alternates.languages field. This was added in Next.js 13.3 and it generates the correct <link rel="alternate" hreflang="..."> tags in <head> automatically.
  4. For pages with dynamic content (blog posts, product pages) where not every locale has a translation, pass a supportedLocales array to your utility and only generate tags for locales where the content actually exists. A hreflang tag pointing to a page that returns 404 is worse than no hreflang tag.

The alternates.languages approach in the metadata API is the cleanest way I've found to handle this. No custom <Head> juggling, no third-party packages for the tags themselves.

---

Locale Detection Without Being Annoying

There's a UX trap here that also has SEO implications. Aggressive automatic redirection based on browser locale is bad for both.

If a user in Germany visits yoursite.com/en/article and your middleware hard-redirects them to /de/artikel without asking, you've just broken their intended navigation. You've also potentially broken link sharing: someone shares the English URL, the recipient in France gets bounced to French, and the content they were meant to see might not exist in French yet.

My rule: detect, suggest, don't force. Use the Accept-Language header to set a sensible default on first visit (stored in a cookie), but let users override it manually and honour that override on every subsequent visit. The cookie takes priority over the header.

For SEO specifically: Googlebot does not send meaningful Accept-Language headers. It will crawl your URLs as-is. So make sure every locale URL is directly accessible without redirection, and that your sitemap includes all locale variants explicitly.

The next-intl middleware documentation covers this detection strategy well, and it's where I'd send anyone starting a new project.

---

Sitemaps for Multilingual Sites

A single sitemap.xml that includes every locale variant is fine for smaller sites. For large sites (tens of thousands of URLs across multiple locales) I split it: one sitemap index at /sitemap.xml , then separate sitemap files per locale at /sitemaps/en.xml , /sitemaps/de.xml, and so on.

Each URL entry should include <xhtml:link rel="alternate"> tags mirroring your hreflang setup. This is redundant with the in-page tags but it helps Googlebot connect the dots on pages it hasn't crawled yet.

In Next.js 14+ you can generate sitemaps dynamically via app/sitemap.ts . The return type supports alternates.languages in the same shape as the metadata API. Wire it up to the same getHreflangAlternates utility you built earlier and you get consistency for free.

One concrete number: on a project with 8 locales and 4,500 products, splitting sitemaps reduced average crawl time per locale by roughly 30% based on GSC crawl stats. Not earth-shattering, but meaningful.

---

Translation Libraries: What I Actually Use

There are three worth knowing. next-intl is my default for App Router projects. It has first-class support for server components, handles pluralisation properly, and the API is clean. I've used it on about 40 projects since mid-2023.

react-i18next is battle-tested and has a huge ecosystem, but its mental model was built for client components and adding server component support via the i18next core feels bolted-on compared to next-intl's native approach.

Lingui is worth looking at if your translation workflow involves professional translators rather than a CMS. Its extraction tooling is superior. But for most agency projects where content comes from Contentful or Sanity, next-intl is simpler.

What I'd avoid: rolling your own. I did it once in 2020 for a project where the client insisted their existing PHP translation files be reused directly. It worked, barely, and I spent two years maintaining something that next-intl now handles in a single useTranslations() call.

---

FAQ

Does Next.js App Router support the old `next.config.js` i18n block?

No. The i18n configuration key was specific to the Pages Router. If you're migrating to App Router, you need to implement routing via a [locale] dynamic segment and a middleware file. The old config block is silently ignored in App Router projects.

Should I use `lang` in the URL path or rely on subdomains?

For most projects, lang in the URL path (sub-path routing like /de/) is the right call. Subdomains fragment your domain authority and require separate DNS configuration. ccTLDs are only worth the complexity if local trust signals are genuinely moving conversion rates in your specific markets.

How does Googlebot handle JavaScript-rendered locale content?

Badly, sometimes. If your locale detection runs purely client-side in a React context and the initial HTML has no locale marker, Googlebot may index the pre-hydration state. Always resolve locale in middleware and ensure the correct lang attribute is on the <html> element in the server-rendered HTML. Check this by viewing source, not by using browser devtools (which show post-hydration state).

What's the right `x-default` hreflang value for a site with no language selector page?

Point it at your primary locale's homepage, typically /en/. It signals to Google that English is your fallback for users whose language you don't support. It's not a penalty to do this, it's just a soft preference signal Google may or may not honour.

Can I verify my hreflang implementation without a paid SEO tool?

Yes. Google Search Console's URL Inspection tool will show you detected hreflang tags for any indexed URL. It won't validate reciprocity automatically, but you can manually check a few representative pages. For bulk validation, Screaming Frog (free up to 500 URLs) handles reciprocity checks in its hreflang tab.

---

Getting multilingual Next.js right is genuinely unglamorous work. It's config files, URL consistency, and making sure two developers haven't implemented the same thing twice in different components. But the sites where I've taken the time to do it properly have rankings that hold up across regions, and the ones where I cut corners have taught me the lessons I've written down here.

The Berlin client, by the way, eventually got their German pages ranking in Germany. Took us a full re-crawl cycle and a lot of coffee.

Need this done, not just read?

start a project book 30 minutes