# 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

```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 (
    <html lang={locale} dir={locale === 'ar' ? 'rtl' : 'ltr'}>
      <body>
        <NextIntlClientProvider messages={messages}>
          {children}
        </NextIntlClientProvider>
      </body>
    </html>
  );
}
```

```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 (
    <main>
      <h1>{t('title')}</h1>
      <p>{t('description')}</p>

      <Link href="/about">{t('aboutLink')}</Link>
    </main>
  );
}
```

---

## 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 => (
        <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>
  );
}
```

```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<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

```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<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

```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 (
    <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

```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<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

```typescript
// 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:

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/)
