Web Development
Next.jsi18nSEOApp RouterInternationalizationContentReact

Next.js i18n with the App Router: Localized Routing, SEO, and Content Workflows (2026 Guide)

AO
Adrijan Omićević
·14 min read

# What You’ll Build#

This guide shows a production-ready approach to Next.js i18n App Router with locale-prefixed URLs, language detection, SEO-safe metadata, and scalable translation workflows. It’s written for teams shipping real products, where caching, dynamic routes, and content operations matter more than hello-world demos.

You’ll learn how to structure routes like /en/blog/... and /hr/blog/..., how to detect and persist language, and how to generate correct canonical and hreflang URLs for Google.

You’ll also see three translation workflow options, from JSON files to headless CMS to localization platforms, with decision criteria and pitfalls.

RequirementVersionNotes
Next.js14.2 or newerApp Router stable patterns, metadata API, middleware
Node.js18 or newerLTS recommended
HostingVercel or equivalentCDN caching makes i18n bugs more visible
i18n librarynext-intl recommendedNot required, but reduces custom plumbing
Content sourceJSON, CMS, or localization serviceChoose based on content volume and team size

If SEO is a priority, read these two guides first to align expectations and avoid common mistakes: Why Next.js for SEO and SEO for developers.

# Localized Routing in the App Router#

The App Router does not use the old i18n config from Pages Router to automatically generate locale routes. In practice, you implement localization using a top-level locale segment.

The most maintainable pattern is:

  • /en/... for English
  • /hr/... for Croatian
  • optionally /de/... etc.

That gives you:

  • clear separation for caching and indexing
  • stable, shareable URLs
  • easy hreflang generation

Folder Structure#

A typical structure:

  • app/[locale]/layout.tsx for locale-aware layout
  • app/[locale]/page.tsx for locale home
  • app/[locale]/blog/[slug]/page.tsx for localized blog posts
  • middleware.ts to redirect users from / to the best locale

This also scales to nested segments like app/[locale]/(marketing)/... and app/[locale]/(app)/... without mixing languages.

Defining Supported Locales#

Create a single source of truth in src/i18n/config.ts:

TypeScript
export const locales = ["en", "hr"] as const;
export type Locale = (typeof locales)[number];
 
export const defaultLocale: Locale = "en";
 
export function isLocale(value: string): value is Locale {
  return (locales as readonly string[]).includes(value);
}

This reduces errors where one part of the app thinks you support en-GB and another thinks it’s just en.

Locale-Aware Layout#

In app/[locale]/layout.tsx, validate the locale early. If it’s unsupported, return 404 to avoid indexing garbage URLs.

TypeScript
import { notFound } from "next/navigation";
import type { ReactNode } from "react";
import { isLocale, type Locale } from "@/i18n/config";
 
export default function LocaleLayout(props: {
  children: ReactNode;
  params: Promise<{ locale: string }>;
}) {
  return props.params.then(({ locale }) => {
    if (!isLocale(locale)) notFound();
    return props.children;
  });
}

This ensures only /en and /hr exist from the router’s perspective.

🎯 Key Takeaway: Locale-prefixed routes are the simplest way to keep caching, SEO, and analytics clean, because the locale is part of the URL and not inferred from headers.

# Language Detection: Redirects, Cookies, and UX#

Language detection is where many i18n implementations break SEO and caching. The goal is:

  • Users landing on / should be routed to the right locale
  • Search engines should still be able to crawl every locale URL deterministically
  • Returning users should stay on their chosen locale

Detection Priority Order#

A practical priority order:

  1. 1
    Explicit locale in URL, like /hr/... always wins
  2. 2
    Locale cookie from a user’s manual selection
  3. 3
    Accept-Language header for first-time visitors
  4. 4
    Fallback to default locale

This matches user intent and avoids redirect loops.

Middleware for Redirecting / to a Locale#

Add middleware.ts and only redirect when there is no locale prefix. Avoid redirecting locale-prefixed URLs because that creates unnecessary hops and can confuse crawlers.

TypeScript
import { NextRequest, NextResponse } from "next/server";
import { defaultLocale, isLocale, locales } from "@/i18n/config";
 
function getPreferredLocale(req: NextRequest) {
  const cookieLocale = req.cookies.get("locale")?.value;
  if (cookieLocale && isLocale(cookieLocale)) return cookieLocale;
 
  const header = req.headers.get("accept-language") || "";
  const first = header.split(",")[0]?.trim().slice(0, 2);
  if (first && isLocale(first)) return first;
 
  return defaultLocale;
}
 
export function middleware(req: NextRequest) {
  const { pathname } = req.nextUrl;
 
  const pathLocale = pathname.split("/")[1];
  if (pathLocale && isLocale(pathLocale)) return NextResponse.next();
 
  if (pathname === "/" || pathname === "") {
    const locale = getPreferredLocale(req);
    const url = req.nextUrl.clone();
    url.pathname = `/${locale}`;
    return NextResponse.redirect(url);
  }
 
  return NextResponse.next();
}
 
export const config = {
  matcher: ["/((?!_next|api|.*\\..*).*)"],
};

Persisting User Choice#

When a user switches languages, set a cookie explicitly. This prevents flipping back and forth based on browser headers.

Example API route or server action that sets locale=hr for 1 year is enough. If you prefer no cookies, keep language switching purely URL-based, but expect slightly worse UX for repeated visits to /.

⚠️ Warning: Do not vary HTML content by Accept-Language on the same URL. CDNs commonly cache by URL, not by header, so you can serve the wrong language to the next user and confuse Google’s indexing.

# Dynamic Segments: Slugs, IDs, and Translated URLs#

Dynamic segments are where i18n meets content strategy. You have three common options:

StrategyExample URLProsCons
Stable slug across locales/en/blog/nextjs-i18n and /hr/blog/nextjs-i18nSimple mapping, fewer redirectsNot ideal for local-language SEO
Fully translated slugs/en/blog/nextjs-i18n and /hr/blog/nextjs-i18n-app-routerBest local SEO, better CTRRequires slug mapping per locale
ID-based routing/en/blog/12345No slug collisions, easy CMSWorst for SEO and UX unless paired with slug

For marketing pages and blog posts, translated slugs usually win if you can maintain the mapping. For user-generated content, stable slugs or IDs are safer.

Implementing Localized Blog Routes#

A common pattern is to fetch content by locale plus slug:

  • locale = en, slug = nextjs-i18n-app-router
  • locale = hr, slug = nextjs-i18n-app-router

Or if slugs are translated:

  • locale = hr, slug = next-js-i18n-aplikacijski-router

Your data model needs a clear unique key. In CMS setups, that’s typically a document ID with localized fields.

Generating Static Params per Locale#

If you statically generate blog pages, ensure you generate params for every locale.

TypeScript
import { locales } from "@/i18n/config";
 
export async function generateStaticParams() {
  const posts = await fetch("https://example.com/api/posts").then((r) => r.json());
 
  return locales.flatMap((locale) =>
    posts
      .filter((p: any) => p.locales.includes(locale))
      .map((p: any) => ({ locale, slug: p.slug[locale] || p.slug.default }))
  );
}

This avoids the classic bug where English pages build but other locales 404 in production.

# Metadata and SEO: Canonicals, hreflang, and Indexing Signals#

SEO for i18n is mostly about clarity. Search engines need:

  • one canonical per locale page
  • alternate language links via hreflang
  • consistent internal linking within each locale
  • no accidental duplicates created by parameters, cookies, or header-based variations

If you want the broader SEO context in one place, reference SEO for developers alongside this guide.

Canonical URL Rules for Localized Pages#

A good default:

  • /en/blog/post canonical is itself
  • /hr/blog/post canonical is itself
  • never canonicalize all locales to one language homepage

Canonical tags are for duplicate content, not for “primary language preference”. If you canonicalize Croatian to English, you’re telling Google to ignore the Croatian URL.

hreflang Alternates with x-default#

For each page, output alternates for every locale and an x-default that points to a language selector or the default locale route. Many teams set x-default to /en, which is acceptable if English is your true default.

In App Router, implement generateMetadata in each route or in a shared helper.

TypeScript
import type { Metadata } from "next";
import { locales, defaultLocale, type Locale } from "@/i18n/config";
 
const siteUrl = "https://samioda.com";
 
export async function generateMetadata(props: {
  params: Promise<{ locale: Locale; slug: string }>;
}): Promise<Metadata> {
  const { locale, slug } = await props.params;
 
  const canonical = `${siteUrl}/${locale}/blog/${slug}`;
 
  const languages = Object.fromEntries(
    locales.map((l) => [l, `${siteUrl}/${l}/blog/${slug}`])
  );
 
  return {
    alternates: {
      canonical,
      languages: {
        ...languages,
        "x-default": `${siteUrl}/${defaultLocale}/blog/${slug}`,
      },
    },
  };
}

This produces consistent alternates per page and avoids manual mistakes.

Localized Titles and Descriptions#

If your metadata is translated, ensure it comes from the same source as your content and is not missing in some locales. Missing metadata often leads to fallback titles like “Blog” which reduces CTR.

A practical rule:

  • For each locale, require title, description, and og:title equivalents
  • If missing, fail build in CI for marketing content

Sitemap and Robots Considerations#

Make sure your sitemap includes all locale URLs. If you generate sitemaps dynamically, include the locale segment in every entry.

If you exclude non-default locales from sitemap, Google will still find them via links, but indexing is slower and less predictable.

ℹ️ Note: If you redirect / to a locale, keep the redirect stable and avoid changing it frequently. Google treats frequent redirect behavior changes as a signal of instability, which can slow crawling and recrawling.

# Translation Management: JSON, CMS, or Localization Platforms#

Translation is not just “where strings live”. It affects release cadence, who can edit content, and how you prevent broken layouts.

Here are three workable workflows, with a practical decision table.

WorkflowBest forTypical teamProsCons
JSON dictionaries in repoUI strings, small sitesEngineersFast, versioned, easy reviewNon-dev edits are painful, larger diffs
Headless CMS localized fieldsMarketing pages, blogsMarketing plus devEditorial workflow, previews, structured contentNeeds governance, content model design
Localization platform with syncApps with frequent releasesProduct teamsTranslation memory, QA checks, automationExtra cost, integration complexity

If you’re evaluating CMS options for localized content modeling, use this comparison as a baseline: Headless CMS comparison 2026.

Option 1: JSON Dictionaries for UI Strings#

For product UI, JSON dictionaries are often the most reliable. They are versioned, reviewed, and deployed together with code.

A minimal example:

JSON
{
  "nav.home": "Home",
  "nav.blog": "Blog",
  "cta.bookCall": "Book a call"
}

Keep these rules:

  • Use stable keys, not English text as keys
  • Validate missing keys in CI
  • Keep UI text out of CMS if you need strict release control

Option 2: CMS for Marketing Pages and Blog Content#

CMS is the right tool when content changes weekly, not per release. For i18n, model content with:

  • a base document ID
  • localized fields per locale
  • slug per locale if you translate URLs
  • localized SEO fields

Example content shape:

FieldTypeLocalizedNotes
idstringnoStable identifier
slugstringyesUsed for localized routing
titlestringyesDisplay and SEO
descriptionstringyesMeta description
bodyrich textyesMain content
canonicalOverridestringyesRarely needed, but useful

This avoids “multiple documents per language” chaos where editors accidentally unpublish one locale.

Option 3: Localization Platforms for Scale#

If you ship to 5 plus locales or release weekly, localization platforms reduce churn via:

  • translation memory
  • string reuse
  • glossary enforcement
  • automated checks for missing variables and placeholders

The key is to integrate them with CI so missing translations fail before production.

# Caching Pitfalls: CDN, Next Cache, and Locale Variants#

Caching and i18n issues are responsible for many “random language” production incidents.

The Core Rule: Cache Must Vary by URL, Not Headers#

If /pricing renders in English for one user and Croatian for another, but the URL is the same, your CDN may cache the first response and serve it to everyone.

Locale-prefixed URLs avoid this. /en/pricing and /hr/pricing are different cache keys automatically.

Common Caching Mistakes and Fixes#

ProblemSymptomRoot causeFix
Wrong language servedUsers see mixed languagesCache keyed only by path without localeUse locale in URL, avoid header-based rendering
Redirect loop/ keeps redirectingCookie vs URL mismatchURL locale wins, only redirect on non-locale paths
Stale translationsNew copy not visibleISR or fetch cache not invalidatedTag-based revalidation per locale and content type
Duplicate indexingGoogle finds multiple variantsQuery params or trailing slash variantsNormalize URLs, set canonicals, avoid indexing params

Revalidation by Locale#

If you revalidate content, include the locale in your cache tags. Otherwise, revalidating English could accidentally evict Croatian or vice versa depending on your tagging strategy.

A practical naming scheme:

  • post:123:en
  • post:123:hr

Keep tags deterministic and short to avoid operational confusion.

💡 Tip: For multilingual marketing sites, prefer locale-prefixed routes plus static generation for the majority of pages. You get faster TTFB and fewer cache edge cases than fully dynamic, header-driven rendering.

# Canonical URL and Duplicate Content Pitfalls#

i18n increases the number of URLs, so small inconsistencies multiply quickly.

Pitfall 1: Canonical Always Points to Default Locale#

This is common when teams hardcode canonicals in a shared component. The result is that non-default locales may never rank because you told Google they are duplicates.

Fix: compute canonical from params.locale and route params every time.

Pitfall 2: hreflang Missing on Dynamic Pages#

If your blog uses dynamic segments, you may have hreflang on category pages but not on post pages.

Fix: centralize alternates generation and call it from every route that should be indexed.

If English content links to /en/contact, the Croatian version should link to /hr/contact. Otherwise, you leak users across locales and send mixed signals to crawlers.

Fix: implement a locale-aware link helper and enforce it in content tooling. In CMS rich text, store references to internal pages and resolve them per locale.

Pitfall 4: Parameterized URLs Indexed as Separate Pages#

UTM parameters and filters can create duplicate pages across languages.

Fix:

  • keep canonicals clean
  • configure analytics to ignore irrelevant parameters
  • avoid linking to parameterized URLs internally

For broader SEO hygiene that intersects with i18n, align with the practices in Why Next.js for SEO.

# A Practical Content Workflow That Doesn’t Break Builds#

Here’s a workflow that works well for teams with both engineers and marketers:

  1. 1
    UI strings live in JSON dictionaries in the repo.
  2. 2
    Marketing pages and blog content live in a headless CMS with localized fields.
  3. 3
    Slug mapping is enforced in the CMS. If a locale slug is missing, the page is not published for that locale.
  4. 4
    CI checks ensure:
    • every published page has title and description per locale
    • no unsupported locales are referenced
    • sitemap contains all published locale URLs

That prevents the two most expensive failures:

  • broken navigation across locales
  • accidental indexing of incomplete translations

# Key Takeaways#

  • Use locale-prefixed routing in the App Router, because it keeps caching and SEO deterministic and avoids header-based content variation.
  • Implement language detection only for non-locale paths like /, with a clear priority order: URL, cookie, header, default.
  • Generate correct SEO metadata per locale: self-referencing canonicals, full hreflang alternates, and an x-default entry.
  • Choose translation workflows by content type: JSON for UI strings, CMS for editorial content, and localization platforms for high-frequency releases.
  • Treat dynamic segments as a content-model problem: decide early whether slugs are stable, translated, or ID-based, and generate static params for every locale.
  • Prevent production incidents by making caches vary by URL and by tagging revalidation per locale and content entity.

# Conclusion#

A solid Next.js i18n App Router setup is less about translating strings and more about building predictable URLs, stable detection, and SEO signals that scale to dozens or thousands of pages. When you get routing, canonicals, and caching right, you avoid the most common multilingual failures: wrong-language responses, duplicate indexing, and unmaintainable slug logic.

If you want Samioda to implement multilingual routing, SEO-safe metadata, and a content workflow that your team can run without developer bottlenecks, reach out and we’ll scope an i18n architecture that fits your locales, CMS, and release process.

FAQ

Share
A
Adrijan OmićevićSamioda Team
All articles →

Need help with your project?

We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.