# International SEO & i18n **Meta-Description:** Internationales SEO mit Next.js. hreflang Tags, URL-Strukturen, Content-Lokalisierung und Multi-Language Routing. **Keywords:** International SEO, i18n, hreflang, Multilingual, Localization, Next.js i18n, URL Structure, Language Targeting --- ## Einführung **Internationales SEO** erfordert mehr als nur Übersetzung. Richtige **hreflang-Implementation**, **URL-Strukturen** und **Lokalisierung** sind entscheidend. Dieser Guide zeigt Best Practices für mehrsprachige Websites mit Next.js 15. --- ## International SEO Overview ``` ┌─────────────────────────────────────────────────────────────┐ │ INTERNATIONAL SEO ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ URL Strategies: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 1. Subdirectories (Recommended): │ │ │ │ example.com/de/ │ │ │ │ example.com/en/ │ │ │ │ example.com/fr/ │ │ │ │ ✓ Easy to set up │ │ │ │ ✓ Single domain authority │ │ │ │ ✓ Clear language indication │ │ │ │ │ │ │ │ 2. Subdomains: │ │ │ │ de.example.com │ │ │ │ en.example.com │ │ │ │ ✗ Separate domain authority │ │ │ │ ✓ Easy geo-targeting │ │ │ │ │ │ │ │ 3. ccTLDs (Country-Specific): │ │ │ │ example.de │ │ │ │ example.co.uk │ │ │ │ ✓ Strong geo-signal │ │ │ │ ✗ Higher cost, separate SEO │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ hreflang Implementation: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Content Strategy: │ │ ├── Translation (Basic) │ │ ├── Localization (Cultural Adaptation) │ │ └── Transcreation (Full Adaptation) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Next.js i18n Setup ```typescript // next.config.js - i18n Configuration /** @type {import('next').NextConfig} */ const nextConfig = { // Built-in i18n (für Pages Router, App Router nutzt Middleware) // i18n: { // locales: ['de', 'en', 'fr', 'es'], // defaultLocale: 'de', // localeDetection: true // } }; module.exports = nextConfig; // middleware.ts - App Router i18n import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; import { match } from '@formatjs/intl-localematcher'; import Negotiator from 'negotiator'; const locales = ['de', 'en', 'fr', 'es']; const defaultLocale = 'de'; function getLocale(request: NextRequest): string { // Check cookie first const cookieLocale = request.cookies.get('NEXT_LOCALE')?.value; if (cookieLocale && locales.includes(cookieLocale)) { return cookieLocale; } // Then check Accept-Language header const headers = { 'accept-language': request.headers.get('accept-language') || '' }; const languages = new Negotiator({ headers }).languages(); try { return match(languages, locales, defaultLocale); } catch { return defaultLocale; } } export function middleware(request: NextRequest) { const pathname = request.nextUrl.pathname; // Skip for static files and API routes if ( pathname.startsWith('/_next') || pathname.startsWith('/api') || pathname.startsWith('/static') || pathname.includes('.') // files with extensions ) { return NextResponse.next(); } // Check if pathname has locale const pathnameHasLocale = locales.some( locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}` ); if (pathnameHasLocale) { return NextResponse.next(); } // Redirect to locale const locale = getLocale(request); const newUrl = new URL(`/${locale}${pathname}`, request.url); return NextResponse.redirect(newUrl); } export const config = { matcher: ['/((?!_next|api|static|.*\\..*).*)', '/'] }; ``` --- ## App Router Structure ```typescript // app/[locale]/layout.tsx - Locale Layout import { notFound } from 'next/navigation'; import { NextIntlClientProvider } from 'next-intl'; import { getMessages, getTranslations } from 'next-intl/server'; const locales = ['de', 'en', 'fr', 'es']; export function generateStaticParams() { return locales.map(locale => ({ locale })); } export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Metadata' }); return { title: { template: `%s | ${t('siteName')}`, default: t('siteName') }, description: t('siteDescription'), alternates: { canonical: `https://example.com/${locale}`, languages: { 'de': 'https://example.com/de', 'en': 'https://example.com/en', 'fr': 'https://example.com/fr', 'es': 'https://example.com/es', 'x-default': 'https://example.com' } } }; } export default async function LocaleLayout({ children, params }: { children: React.ReactNode; params: Promise<{ locale: string }>; }) { const { locale } = await params; if (!locales.includes(locale)) { notFound(); } const messages = await getMessages(); return ( {children} ); } ``` ```typescript // app/[locale]/page.tsx - Localized Page import { getTranslations } from 'next-intl/server'; import { Link } from '@/navigation'; interface PageProps { params: Promise<{ locale: string }>; } export default async function HomePage({ params }: PageProps) { const { locale } = await params; const t = await getTranslations('Home'); return (

{t('title')}

{t('description')}

{t('aboutLink')}
); } ``` --- ## hreflang Implementation ```typescript // lib/i18n/hreflang.ts - hreflang Generator interface HreflangConfig { locales: string[]; defaultLocale: string; baseUrl: string; } interface HreflangLink { hrefLang: string; href: string; } export function generateHreflangLinks( path: string, config: HreflangConfig ): HreflangLink[] { const { locales, defaultLocale, baseUrl } = config; const links: HreflangLink[] = locales.map(locale => ({ hrefLang: locale, href: `${baseUrl}/${locale}${path}` })); // x-default für Sprachauswahl / Default links.push({ hrefLang: 'x-default', href: `${baseUrl}/${defaultLocale}${path}` }); return links; } // components/Hreflang.tsx - Hreflang Component import { headers } from 'next/headers'; const config: HreflangConfig = { locales: ['de', 'en', 'fr', 'es'], defaultLocale: 'de', baseUrl: 'https://example.com' }; export async function Hreflang() { const headersList = await headers(); const pathname = headersList.get('x-pathname') || '/'; // Remove locale from pathname const path = pathname.replace(/^\/(de|en|fr|es)/, ''); const links = generateHreflangLinks(path, config); return ( <> {links.map(link => ( ))} ); } // Usage in layout.tsx export default function RootLayout({ children }) { return ( {children} ); } ``` ```typescript // app/[locale]/[...slug]/page.tsx - Dynamic hreflang import { Metadata } from 'next'; interface PageProps { params: Promise<{ locale: string; slug: string[] }>; } export async function generateMetadata({ params }: PageProps): Promise { const { locale, slug } = await params; const path = `/${slug.join('/')}`; const locales = ['de', 'en', 'fr', 'es']; const baseUrl = 'https://example.com'; // Generate language alternates const languages: Record = {}; locales.forEach(loc => { languages[loc] = `${baseUrl}/${loc}${path}`; }); languages['x-default'] = `${baseUrl}/de${path}`; return { alternates: { canonical: `${baseUrl}/${locale}${path}`, languages } }; } ``` --- ## Translation Management ```typescript // messages/de.json { "Metadata": { "siteName": "Meine Website", "siteDescription": "Die beste Website für..." }, "Home": { "title": "Willkommen", "description": "Entdecken Sie unsere Produkte und Dienstleistungen.", "aboutLink": "Über uns" }, "Navigation": { "home": "Startseite", "about": "Über uns", "products": "Produkte", "contact": "Kontakt" }, "Common": { "readMore": "Mehr lesen", "learnMore": "Mehr erfahren", "submit": "Absenden", "cancel": "Abbrechen" } } // messages/en.json { "Metadata": { "siteName": "My Website", "siteDescription": "The best website for..." }, "Home": { "title": "Welcome", "description": "Discover our products and services.", "aboutLink": "About us" }, "Navigation": { "home": "Home", "about": "About", "products": "Products", "contact": "Contact" }, "Common": { "readMore": "Read more", "learnMore": "Learn more", "submit": "Submit", "cancel": "Cancel" } } ``` ```typescript // lib/i18n/index.ts - Translation Utilities import { getRequestConfig } from 'next-intl/server'; export default getRequestConfig(async ({ locale }) => ({ messages: (await import(`@/messages/${locale}.json`)).default })); // i18n.ts - Configuration export const locales = ['de', 'en', 'fr', 'es'] as const; export type Locale = (typeof locales)[number]; export const localeNames: Record = { de: 'Deutsch', en: 'English', fr: 'Français', es: 'Español' }; export const localeFlags: Record = { de: '🇩🇪', en: '🇬🇧', fr: '🇫🇷', es: '🇪🇸' }; export const defaultLocale: Locale = 'de'; // Check if locale is valid export function isValidLocale(locale: string): locale is Locale { return locales.includes(locale as Locale); } ``` --- ## Language Switcher ```tsx // components/LanguageSwitcher.tsx 'use client'; import { useLocale } from 'next-intl'; import { usePathname, useRouter } from '@/navigation'; import { locales, localeNames, localeFlags, Locale } from '@/lib/i18n'; export function LanguageSwitcher() { const locale = useLocale(); const pathname = usePathname(); const router = useRouter(); const handleChange = (newLocale: Locale) => { // Set cookie for persistence document.cookie = `NEXT_LOCALE=${newLocale}; path=/; max-age=31536000`; // Navigate to new locale router.replace(pathname, { locale: newLocale }); }; return (
); } // Alternative: Link-based Switcher export function LanguageSwitcherLinks() { const locale = useLocale(); const pathname = usePathname(); return ( ); } ``` --- ## Localized Sitemap ```typescript // app/sitemap.ts - Multilingual Sitemap import { MetadataRoute } from 'next'; import { locales, defaultLocale } from '@/lib/i18n'; import { getAllPages } from '@/lib/content'; export default async function sitemap(): Promise { const baseUrl = 'https://example.com'; const pages = await getAllPages(); const sitemap: MetadataRoute.Sitemap = []; // Static pages for each locale const staticPaths = ['', '/about', '/contact', '/products']; for (const path of staticPaths) { for (const locale of locales) { const alternates: Record = {}; locales.forEach(loc => { alternates[loc] = `${baseUrl}/${loc}${path}`; }); alternates['x-default'] = `${baseUrl}/${defaultLocale}${path}`; sitemap.push({ url: `${baseUrl}/${locale}${path}`, lastModified: new Date(), changeFrequency: path === '' ? 'daily' : 'weekly', priority: path === '' ? 1.0 : 0.8, alternates: { languages: alternates } }); } } // Dynamic pages (blog posts, products) for (const page of pages) { for (const locale of locales) { // Check if translation exists const hasTranslation = page.translations?.includes(locale); if (!hasTranslation && locale !== defaultLocale) continue; const alternates: Record = {}; const availableLocales = page.translations || [defaultLocale]; availableLocales.forEach(loc => { alternates[loc] = `${baseUrl}/${loc}/${page.slug}`; }); alternates['x-default'] = `${baseUrl}/${defaultLocale}/${page.slug}`; sitemap.push({ url: `${baseUrl}/${locale}/${page.slug}`, lastModified: new Date(page.updatedAt), changeFrequency: 'weekly', priority: 0.7, alternates: { languages: alternates } }); } } return sitemap; } ``` --- ## SEO Best Practices ```typescript // lib/i18n/seo.ts - i18n SEO Helpers import { Metadata } from 'next'; interface LocalizedSEOConfig { title: Record; description: Record; keywords: Record; ogImage?: Record; } export function generateLocalizedMetadata( config: LocalizedSEOConfig, locale: string, path: string ): Metadata { const baseUrl = 'https://example.com'; const locales = ['de', 'en', 'fr', 'es']; // Language alternates const languages: Record = {}; locales.forEach(loc => { languages[loc] = `${baseUrl}/${loc}${path}`; }); languages['x-default'] = `${baseUrl}/de${path}`; return { title: config.title[locale], description: config.description[locale], keywords: config.keywords[locale], alternates: { canonical: `${baseUrl}/${locale}${path}`, languages }, openGraph: { title: config.title[locale], description: config.description[locale], url: `${baseUrl}/${locale}${path}`, siteName: locale === 'de' ? 'Meine Website' : 'My Website', locale: getOGLocale(locale), alternateLocales: locales .filter(l => l !== locale) .map(getOGLocale), images: config.ogImage?.[locale] ? [config.ogImage[locale]] : undefined } }; } function getOGLocale(locale: string): string { const map: Record = { de: 'de_DE', en: 'en_US', fr: 'fr_FR', es: 'es_ES' }; return map[locale] || 'en_US'; } ``` --- ## Checklist | Aspect | Implementation | Priority | |--------|---------------|----------| | **URL Structure** | Subdirectories (/de/, /en/) | Critical | | **hreflang** | All pages, x-default | Critical | | **Canonical** | Per locale | Critical | | **Sitemap** | Multilingual with alternates | Important | | **Language Detection** | Accept-Language header | Important | | **Switcher** | Visible, accessible | Important | | **Content** | Fully translated, not auto | Important | | **Local Keywords** | Research per market | Important | --- ## Fazit International SEO erfordert: 1. **Klare URL-Struktur**: Subdirectories für beste Balance 2. **hreflang Tags**: Korrekt auf allen Seiten 3. **Lokalisierung**: Mehr als nur Übersetzung 4. **Technische Basis**: Sitemaps, Canonical, Meta Internationales SEO öffnet globale Märkte. --- ## Bildprompts 1. "World map with language flags, multilingual website concept" 2. "hreflang tag implementation diagram, connected language versions" 3. "Language switcher UI designs, dropdown and flags" --- ## Quellen - [Google i18n SEO Guide](https://developers.google.com/search/docs/specialty/international) - [Next.js Internationalization](https://nextjs.org/docs/app/building-your-application/routing/internationalization) - [next-intl Documentation](https://next-intl-docs.vercel.app/) - [Ahrefs International SEO](https://ahrefs.com/blog/international-seo/)