Internationalization (i18n) SEO
If your site serves multiple languages or regions, proper internationalization is essential for SEO. This lesson covers hreflang tags, locale routing, and best practices.
Why i18n Matters for SEO
Without proper i18n setup:
- Users may see content in the wrong language
- Search engines may index the wrong version
- You may have duplicate content issues
- Rankings suffer in local markets
URL Strategies
Subdirectory (Recommended)
example.com/en/about
example.com/es/about
example.com/fr/about
Pros: Shared domain authority, simple setup Cons: Less clear geographic targeting
Subdomain
en.example.com/about
es.example.com/about
fr.example.com/about
Pros: Clear separation, can host on different servers Cons: Each subdomain builds authority separately
Country-Code TLD (ccTLD)
example.com/about
example.es/about
example.fr/about
Pros: Strong geographic signal Cons: Expensive, complex to manage, no shared authority
Recommendation: Use subdirectories for most sites.
Hreflang Tags
Hreflang tags tell search engines which language version to show users.
Basic Implementation
// app/[locale]/page.tsx
export const metadata: Metadata = {
alternates: {
canonical: 'https://example.com/en',
languages: {
'en': 'https://example.com/en',
'es': 'https://example.com/es',
'fr': 'https://example.com/fr',
'x-default': 'https://example.com/en',
},
},
}
This generates:
<link rel="alternate" hreflang="en" href="https://example.com/en" />
<link rel="alternate" hreflang="es" href="https://example.com/es" />
<link rel="alternate" hreflang="fr" href="https://example.com/fr" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en" />
Dynamic Hreflang
For dynamic pages with translations:
// app/[locale]/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const { locale, slug } = await params
const post = await getPost(slug, locale)
const translations = await getTranslations(slug)
const languages: Record<string, string> = {
'x-default': `https://example.com/en/blog/${slug}`,
}
translations.forEach((t) => {
languages[t.locale] = `https://example.com/${t.locale}/blog/${t.slug}`
})
return {
title: post.title,
alternates: {
canonical: `https://example.com/${locale}/blog/${slug}`,
languages,
},
}
}
Hreflang Values
Language Only
hreflang="en" // English (any region)
hreflang="es" // Spanish (any region)
hreflang="fr" // French (any region)
Language + Region
hreflang="en-US" // English for United States
hreflang="en-GB" // English for United Kingdom
hreflang="es-MX" // Spanish for Mexico
hreflang="es-ES" // Spanish for Spain
x-default
The fallback for users whose language isn't specified:
hreflang="x-default" // Default version
Internationalized Routing in Next.js
App Router i18n Setup
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
const locales = ['en', 'es', 'fr']
const defaultLocale = 'en'
function getLocale(request: NextRequest) {
// Check cookie, accept-language header, etc.
return defaultLocale
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname
// Check if pathname has locale
const pathnameHasLocale = locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
)
if (pathnameHasLocale) return
// Redirect to locale-prefixed path
const locale = getLocale(request)
return NextResponse.redirect(
new URL(`/${locale}${pathname}`, request.url)
)
}
export const config = {
matcher: ['/((?!api|_next|.*\\..*).*)'],
}
Folder Structure
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── about/
│ │ └── page.tsx
│ └── blog/
│ └── [slug]/
│ └── page.tsx
Content Translation Best Practices
Don't Auto-Translate
Machine translation produces poor results. Use professional translators or native speakers.
Localize, Don't Just Translate
Adapt content for the culture:
English: "Fall Sale - 20% Off"
Spanish (Spain): "Rebajas de Otoño - 20% de descuento"
Spanish (Mexico): "Venta de Otoño - 20% de descuento" // Different terminology
Use Native URLs
Good: /es/contacto
Bad: /es/contact
Separate Content Files
content/
├── en/
│ ├── about.json
│ └── blog/
│ └── seo-guide.json
├── es/
│ ├── about.json
│ └── blog/
│ └── guia-seo.json
Common i18n SEO Mistakes
Automatic Redirects Based on IP
Don't force users to a specific language version:
// Bad: Forces redirect
if (userCountry === 'ES') {
redirect('/es')
}
// Good: Show suggestion banner
<LocaleSuggestionBanner
suggestedLocale="es"
message="Would you like to view this in Spanish?"
/>
Missing x-default
Always include x-default as a fallback.
Inconsistent Hreflang
All pages in the group must link to each other:
// Page A (English) must link to:
- itself (en)
- Spanish version (es)
- French version (fr)
// Page B (Spanish) must link to:
- English version (en)
- itself (es)
- French version (fr)
Summary
In this lesson, you learned:
- URL strategies for internationalization
- Implementing hreflang tags in Next.js
- Language and region codes
- Internationalized routing setup
- Content translation best practices
- Common i18n SEO mistakes to avoid
In the next lesson, we'll cover SEO monitoring and analytics.

