2 min read
SEOInternational SEO & i18n
Internationales SEO mit Next.js. hreflang Tags, URL-Strukturen, Content-Lokalisierung und Multi-Language Routing.
International SEOi18nhreflangMultilingualLocalizationNext.js i18n

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: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <link rel="alternate" hreflang="de" │ │
│ │ href="https://example.com/de/page" /> │ │
│ │ <link rel="alternate" hreflang="en" │ │
│ │ href="https://example.com/en/page" /> │ │
│ │ <link rel="alternate" hreflang="x-default" │ │
│ │ href="https://example.com/page" /> │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Content Strategy: │
│ ├── Translation (Basic) │
│ ├── Localization (Cultural Adaptation) │
│ └── Transcreation (Full Adaptation) │
│ │
└─────────────────────────────────────────────────────────────┘Next.js i18n Setup
// 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
// 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 (
<html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
<body>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}// 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 (
<main>
<h1>{t('title')}</h1>
<p>{t('description')}</p>
<Link href="/about">{t('aboutLink')}</Link>
</main>
);
}hreflang Implementation
// 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 => (
<link
key={link.hrefLang}
rel="alternate"
hrefLang={link.hrefLang}
href={link.href}
/>
))}
</>
);
}
// Usage in layout.tsx
export default function RootLayout({ children }) {
return (
<html>
<head>
<Hreflang />
</head>
<body>{children}</body>
</html>
);
}// 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<Metadata> {
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<string, string> = {};
locales.forEach(loc => {
languages[loc] = `${baseUrl}/${loc}${path}`;
});
languages['x-default'] = `${baseUrl}/de${path}`;
return {
alternates: {
canonical: `${baseUrl}/${locale}${path}`,
languages
}
};
}Translation Management
// 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"
}
}// 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<Locale, string> = {
de: 'Deutsch',
en: 'English',
fr: 'Français',
es: 'Español'
};
export const localeFlags: Record<Locale, string> = {
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
// 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 (
<div className="relative">
<select
value={locale}
onChange={(e) => handleChange(e.target.value as Locale)}
className="appearance-none bg-white border rounded-md px-4 py-2 pr-8"
aria-label="Select language"
>
{locales.map((loc) => (
<option key={loc} value={loc}>
{localeFlags[loc]} {localeNames[loc]}
</option>
))}
</select>
</div>
);
}
// Alternative: Link-based Switcher
export function LanguageSwitcherLinks() {
const locale = useLocale();
const pathname = usePathname();
return (
<nav aria-label="Language selection">
<ul className="flex gap-2">
{locales.map((loc) => (
<li key={loc}>
<Link
href={pathname}
locale={loc}
className={`px-3 py-1 rounded ${
locale === loc
? 'bg-blue-600 text-white'
: 'bg-gray-100 hover:bg-gray-200'
}`}
aria-current={locale === loc ? 'true' : undefined}
>
{localeFlags[loc]} {loc.toUpperCase()}
</Link>
</li>
))}
</ul>
</nav>
);
}Localized Sitemap
// 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<MetadataRoute.Sitemap> {
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<string, string> = {};
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<string, string> = {};
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
// lib/i18n/seo.ts - i18n SEO Helpers
import { Metadata } from 'next';
interface LocalizedSEOConfig {
title: Record<string, string>;
description: Record<string, string>;
keywords: Record<string, string[]>;
ogImage?: Record<string, string>;
}
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<string, string> = {};
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<string, string> = {
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:
- Klare URL-Struktur: Subdirectories für beste Balance
- hreflang Tags: Korrekt auf allen Seiten
- Lokalisierung: Mehr als nur Übersetzung
- Technische Basis: Sitemaps, Canonical, Meta
Internationales SEO öffnet globale Märkte.
Bildprompts
- "World map with language flags, multilingual website concept"
- "hreflang tag implementation diagram, connected language versions"
- "Language switcher UI designs, dropdown and flags"