Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship.
Ich habe den internationalen Launch von TanStack Ship über 12 Locales (en, de, fr, es, pt, ja, zh, ko, it, nl, pl, sv) zwischen Q2 2026 und Q3 2026 ausgeliefert – mit TanStack Router v1.135, benutzerdefinierter hreflang-Generierung, per-Locale Sitemaps und JSON-LD-Schema. Nach 90 Tagen Produktionstraffic habe ich +47 % nicht-englische organische Klicks in der Google Search Console und +38 % nicht-englische Anmeldungen gemessen. Die größte Stolperfalle, die ich erwischt habe, war der Hreflang-Cluster-Drift nach dem Deployment neuer Landingpages – Google Search Console meldete 184 Cluster-Fehler, bevor ich einen CI-Check geschrieben habe. Dieser Artikel ist der Produktionscode, den ich am ersten Tag gebraucht hätte, gepaart mit den 6 Implementierungsschritten, die tatsächlich die Zahlen bewegt haben.
Verifizierte Quellen: Google Search Central — International SEO · Google hreflang specification · schema.org inLanguage · TanStack Router localization · Cloudflare Workers URL rewrites · web.dev i18n · Ahrefs hreflang guide · Open Graph locale · sitemap.org protocol
Letzte Aktualisierung: 2026-09-24 · Changelog
TL;DR: Multilingual SEO für SaaS im Jahr 2026 ist ein Implementierungsproblem, kein Übersetzungsproblem. Ich habe 12 Locales auf TanStack Ship ausgeliefert – mit locale-bewussten TanStack Router-Pfaden, programmatischer hreflang-Generierung, per-Locale Sitemaps und JSON-LD-
inLanguage-Markup. Nach 90 Tagen in Produktion:
- Nicht-englische organische Klicks: +47 % (Google Search Console, 90-Tage-Fenster)
- Nicht-englische Anmeldungen: +38 % (Anmelde-Attribution über
?lang-Query-Parameter)- Indexierte Locale-Seiten: 1.432 über 12 Locales (nach hreflang-Fix)
- hreflang-Cluster-Fehler: 184 → 0 nach CI-Check in der PR-Pipeline
- Zeit pro neuem Locale: ca. 6 Stunden inklusive Übersetzungs-QA
Dieser Beitrag enthält den Produktionscode und die 6-Schritte-Checkliste, die ich verwendet habe. Er ergänzt den Strategiebeitrag Multilingual SEO for SaaS: 8 Strategies to Triple International Traffic – die Strategie steht dort, die Implementierung hier.
Warum Multilingual SEO ein Wachstumshebel für SaaS im Jahr 2026 ist
Englische SERPs im B2B-SaaS-Bereich sind gesättigt. Dieselben fünf Listenartikel dominieren jede Suche nach „best CRM", „best ERP" und „best data warehouse".
Gleichzeitig sind deutsche, französische, spanische, japanische, koreanische und portugiesische SERPs weitgehend offen. Derselbe englische Artikel mit korrektem hreflang rankt in 4–8 Wochen. Die International SEO-Dokumentation von Google Search Central bestätigt, dass der technische Weg der entscheidende Faktor ist.
Die Daten bestätigen das für TanStack Ship. Nach 90 Tagen mit 12 Locales in Produktion sind die nicht-englischen organischen Klicks um 47 % und die nicht-englischen Anmeldungen um 38 % gestiegen. Die Kosten beliefen sich auf einen Übersetzungsvertrag zu $0,10/Wort und ca. 6 Stunden Engineering pro Locale. Es gibt keinen ROI im Paid-Kanal, der das erreicht.
Dieser Beitrag richtet sich an SaaS-Gründer und Engineers, die bereits $5.000–$500.000 MRR erreichen, an die Obergrenze englischsprachiger Märkte stoßen und eine funktionierende technische Implementierung zum Kopieren suchen.
Schritt 1: URL-Struktur zuerst festlegen (Tag 1)
Die URL-Strukturentscheidung ist die teuerste zum Rückgängigmachen. Die falsche Struktur bedeutet ein 6-monatiges Migration mit 301-Ketten. Die drei Optionen:
| Struktur | Beispiel | Vorteile | Nachteile |
|---|---|---|---|
| Unterverzeichnis | /de/pricing | Eine Domain-Autorität, einfache Analytics, unkompliziertes hreflang | Risiko bei Shared Hosting, einzelne Markt-Sanktionen |
| Subdomain | de.beispiel.com | Isoliertes Hosting, getrennte Märkte | Autorität wird aufgeteilt, schwieriger zu konsolidieren |
| ccTLD | beispiel.de | Stärkstes Geo-Targeting-Signal | Teuer, verteilt Autorität über Domains |
Meine Empfehlung für Solo-SaaS: Unterverzeichnis. TanStack Ship verwendet tanstackship.com/de/, /fr/ usw. und konsolidiert alle Autorität auf einer einzigen Domain. Google sagt in der International SEO-Dokumentation explizit, dass die Wahl die Ranking nicht stark beeinflusst – aber Unterverzeichnis ist der Weg des geringsten Widerstands für ein Solo-Team. Wählen Sie das an Tag 1. Eine spätere Änderung ist ein 6-Monats-Projekt.
Schritt 2: Locale-bewusstes Routing mit TanStack Router einrichten
Das lokalisierte Routing von TanStack Router ermöglicht es, das Locale-Segment auf die Route-Tree-Ebene zu heben. Das Muster, das ich ausgeliefert habe, verwendet ein einzelnes root-_locale-pfadloses Layout-Segment, das jeden locale-präfigierten Route umschließt:
// app/router.tsx (tanstack-router@1.135.0)
import { createRouter, createRootRoute, createRoute, createRootRouteWithContext } from '@tanstack/react-router'
export const LOCALES = ['en', 'de', 'fr', 'es', 'pt', 'ja', 'zh', 'ko', 'it', 'nl', 'pl', 'sv'] as const
export type Locale = typeof LOCALES[number]
export const DEFAULT_LOCALE: Locale = 'en'
// Pfadloses Locale-Layout – hält :locale in der URL, aber aus dem Route-Tree heraus
const localeLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
path: '$locale',
parseParams: (params) => ({
// Locale gegen Allowlist validieren – verhindert Pfadmanipulation
locale: LOCALES.includes(params.locale as Locale) ? (params.locale as Locale) : DEFAULT_LOCALE,
}),
stringifyParams: ({ locale }) => ({ locale }),
beforeLoad: async ({ params }) => {
// Übersetzungsbundle serverseitig vorladen
const translations = await loadTranslations(params.locale)
return { locale: params.locale, translations }
},
})
const indexRoute = createRoute({
getParentRoute: () => localeLayoutRoute,
path: '/',
component: HomePage,
})
const pricingRoute = createRoute({
getParentRoute: () => localeLayoutRoute,
path: '/pricing',
component: PricingPage,
})
export const routeTree = rootRoute.addChildren([
localeLayoutRoute.addChildren([indexRoute, pricingRoute, /* … */]),
])
Zwei Muster sind hervorzuheben. parseParams validiert das Locale gegen den Allowlist – ohne dies können Angreifer Pfade im Stil von /../../../etc/passwd generieren. beforeLoad lädt das Übersetzungsbundle serverseitig vor – so liefert TanStack Start den Content bereits beim ersten Render aus.
Für die serverseitige Erkennung auf Cloudflare Workers zeigen die Cloudflare Workers Request-Handling-Docs, wie die Header Accept-Language und CF-IPCountry über die Request.cf-Property ausgelesen werden:
// workers/detect-locale.ts
export function detectLocale(request: Request, urlLocale: string | undefined): Locale {
// Zunächst URL-Präfix prüfen
if (urlLocale && LOCALES.includes(urlLocale as Locale)) return urlLocale as Locale
// Dann Accept-Language-Header
const accept = request.headers.get('Accept-Language') ?? ''
const preferred = accept.split(',')[0]?.split('-')[0]
if (preferred && LOCALES.includes(preferred as Locale)) return preferred as Locale
// Geo-basierter Fallback über Cloudflare
const country = request.cf?.country as string | undefined
const countryLocale = COUNTRY_TO_LOCALE[country]
if (countryLocale) return countryLocale
return DEFAULT_LOCALE
}
Wenn die URL kein Locale-Präfix hat, wählt diese Funktion die beste Entsprechung aus Accept-Language, fällt dann auf CF-IPCountry zurück und schließlich auf DEFAULT_LOCALE. Der /302-Redirect von / nach /:locale wird dann am Edge ausgegeben – weiterlesen für den Redirect-Code.
Schritt 3: Hreflang-Tags programmatisch generieren
Hreflang teilt Google mit: „Diese Seite ist die deutsche Version jener englischen Seite." Wenn das falsch ist, zeigt Google die falsche Sprache den Nutzern – oder ignoriert die internationalen Seiten ganz. Die hreflang-Spezifikation ist eindeutig: Jede Seite in einem Cluster muss auf jede andere Seite im Cluster verweisen, einschließlich sich selbst. x-default muss auf die Sprachauswahl oder die Standard-Locale-Seite zeigen. Das MDN-Referenz zu rel="alternate" deckt die Syntax ab.
Hier ist der Produktionsgenerator, den ich für TanStack Ship ausgeliefert habe:
// lib/hreflang.ts
import { LOCALES, DEFAULT_LOCALE, type Locale } from '@/i18n/config'
export interface HreflangEntry {
hreflang: string // 'en', 'de', 'x-default' oder 'en-GB' für regional
href: string
}
// Generiert alle hreflang-Einträge für einen gegebenen Pfad
export function generateHreflang(
pathname: string,
baseUrl: string,
regionalOverrides?: Partial<Record<Locale, string>>,
): HreflangEntry[] {
const entries: HreflangEntry[] = []
for (const locale of LOCALES) {
const region = regionalOverrides?.[locale]
const code = region ? `${locale}-${region}` : locale
const url = locale === DEFAULT_LOCALE
? `${baseUrl}${pathname}`
: `${baseUrl}/${locale}${pathname}`
entries.push({ hreflang: code, href: url })
}
// x-default muss auf die Sprachauswahl beim Standard-Locale zeigen
entries.push({
hreflang: 'x-default',
href: `${baseUrl}${pathname}`,
})
return entries
}
Zwei Stolperfallen sind zu beachten. Erstens: Verwenden Sie niemals ISO 3166-1-Ländercodes (wie de-DE, en-US), es sei denn, Sie liefern tatsächlich regionsspezifische Inhalte aus. Das Standard-Regional-Fallback (einfach de, en) deckt 95 % der SaaS-Fälle ab. Zweitens: x-default muss auf die Sprachauswahl-Seite oder die Standard-Locale-Seite zeigen, nicht auf Ihr Home-Locale. Ich habe den x-default-Bug beim ersten Deployment von TanStack Ship eingeschifft und Google meldete innerhalb von 48 Stunden 184 Cluster-Fehler.
Im Root-Layout rendern:
// components/HreflangMeta.tsx
export function HreflangMeta({ pathname }: { pathname: string }) {
const baseUrl = 'https://tanstackship.com'
const entries = generateHreflang(pathname, baseUrl)
return (
<>
{entries.map((e) => (
<link key={e.hreflang} rel="alternate" hrefLang={e.hreflang} href={e.href} />
))}
</>
)
}
Schritt 4: Per-Locale Sitemaps mit eingebettetem Hreflang bauen
Sitemaps sind das zweite Signal, das Google verwendet, um Ihre Locale-Seiten zu entdecken. Das sitemap.org-Protokoll unterstützt <xhtml:link>-Elemente mit rel="alternate" und hreflang – damit können Sie den vollständigen hreflang-Cluster in jede sitemap-URL einbetten. Dies ist der einzelne technische SEO-Vorteil mit dem größten Hebel für mehrsprachiges SaaS, weil es das Orphan-Locale-Problem beseitigt (Locales, die existieren, aber die Google nie indexiert).
// workers/sitemap.xml.ts
import { LOCALES, DEFAULT_LOCALE, type Locale } from '@/i18n/config'
import { generateHreflang } from '@/lib/hreflang'
const SITE_URL = 'https://tanstackship.com'
const PAGES = ['/', '/pricing', '/blog', '/features', '/compare', '/docs']
export async function generateSitemap(): Promise<string> {
const urls: string[] = []
for (const page of PAGES) {
// hreflang-Cluster für jede Seite generieren
const hreflangEntries = generateHreflang(page, SITE_URL)
// xhtml:link-Einträge – einer pro alternativer Sprache
const xhtmlLinks = hreflangEntries
.map((e) => ` <xhtml:link rel="alternate" hreflang="${e.hreflang}" href="${e.href}"/>`)
.join('\n')
// Jedes Locale erhält seinen eigenen URL-Eintrag
for (const locale of LOCALES) {
const localeUrl = locale === DEFAULT_LOCALE
? `${SITE_URL}${page}`
: `${SITE_URL}/${locale}${page}`
const alternates = hreflangEntries
.filter((e) => e.hreflang !== 'x-default')
.map((e) => ` <xhtml:link rel="alternate" hreflang="${e.hreflang}" href="${e.href}"/>`)
.join('\n')
urls.push(` <url>
<loc>${localeUrl}</loc>
<lastmod>${new Date().toISOString()}</lastmod>
<changefreq>weekly</changefreq>
${alternates}
</url>`)
}
}
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
${urls.join('\n')}
</urlset>`
}
Dann auf /sitemap.xml bereitstellen und in robots.txt referenzieren. Die xhtml:link-Einträge sind zwingend erforderlich – ohne sie behandelt Google jede Locale-URL als separaten Cluster.
Schritt 5: Locale-Seiten mit JSON-LD inLanguage auszeichnen
Strukturierte Daten helfen Google, die Sprache und regionale Ausrichtung jeder Seite über <html lang> und hreflang hinaus zu verstehen. Die schema.org/inLanguage-Property funktioniert in jedem WebPage- oder Article-Schema:
// components/JsonLd.tsx
import { type Locale } from '@/i18n/config'
interface WebPageSchemaProps {
locale: Locale
pathname: string
title: string
description: string
}
export function WebPageSchema({ locale, pathname, title, description }: WebPageSchemaProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: title,
description,
inLanguage: locale,
url: `https://tanstackship.com/${locale}${pathname}`,
isPartOf: {
'@type': 'WebSite',
name: 'TanStack Ship',
url: 'https://tanstackship.com',
// Array zeigt Google alle Sprachen der Website
inLanguage: LOCALES,
},
}
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
)
}
Zwei Details sind zu beachten. Erstens: Das WebSite.inLanguage-Array teilt Google mit, welche Sprachen die gesamte Website abdeckt – damit kann Google die Sprachauswahl-Seite überspringen und Nutzer direkt zu ihrem bevorzugten Locale leiten. Zweitens: Rendern Sie dieses Skript im HTML der Seite, nicht über clientseitiges JavaScript – Google liest JSON-LD aus dem ersten Response-Payload, nicht nach der Hydration.
Schritt 6: Locale-Switcher und Edge-Redirect hinzufügen
Der Locale-Switcher hat zwei Aufgaben: die URL bei Nutzeraktion ändern und einen 302-Redirect (nicht 301) auslösen, wenn die URL kein Locale-Präfix hat. Das 302 ist entscheidend – ein 301 bewirkt, dass Google /locale-URLs indexiert, selbst wenn Nutzer über unpräfigierte Pfade landen. Ein 302 behält die kanonische Standard-URL bei.
// components/LocaleSwitcher.tsx
export function LocaleSwitcher({ currentLocale }: { currentLocale: Locale }) {
const router = useRouter()
// Lokalen Pfad ohne Locale-Präfix extrahieren
const pathname = useLocation({ select: (l) => l.pathname.replace(/^\/[a-z]{2}(\/|$)/, '$1') })
const switchTo = (newLocale: Locale) => {
const newPath = newLocale === DEFAULT_LOCALE
? pathname
: `/${newLocale}${pathname}`
router.navigate({ to: newPath })
}
return (
<select
value={currentLocale}
onChange={(e) => switchTo(e.target.value as Locale)}
>
{LOCALES.map((l) => (
<option key={l} value={l}>{LOCALE_NAMES[l]}</option>
))}
</select>
)
}
Auf der Cloudflare Workers-Seite werden unpräfigierte Pfade mit einem 302 umgeleitet:
// workers/_middleware.ts
export async function handle(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url)
const hasLocale = LOCALES.some((l) => url.pathname.startsWith(`/${l}/`) || url.pathname === `/${l}`)
const isAsset = url.pathname.startsWith('/_assets/') || url.pathname.startsWith('/api/')
if (!hasLocale && !isAsset && request.method === 'GET') {
const locale = detectLocale(request, undefined)
if (locale !== DEFAULT_LOCALE) {
return Response.redirect(`${url.origin}/${locale}${url.pathname}${url.search}`, 302)
}
}
return env.ASSETS.fetch(request)
}
Das 302 ist die richtige Wahl laut den Cloudflare Workers URL-Rewrite-Docs – Suchmaschinen folgen 302s, indexieren das Redirect-Ziel aber nicht dauerhaft, was die kanonische URL-Strategie intakt hält.
Schritt 7: Den CI-Check hinzufügen, der Hreflang-Drift abfängt
Dies ist der Schritt, den ich zu spät eingebaut habe. Nach dem Deployment einer Reihe neuer Feature-Landingpages ohne Aktualisierung von hreflang meldete die Google Search Console 184 Cluster-Fehler. Die Lösung war ein CI-Check, der den Page-Route-Tree mit den generierten hreflang-Einträgen abgleicht:
// scripts/check-hreflang.ts
import { routeTree } from '../app/router'
import { LOCALES, DEFAULT_LOCALE } from '../i18n/config'
import { generateHreflang } from '../lib/hreflang'
const SITE_URL = 'https://tanstackship.com'
// Alle Routen durchlaufen und hreflang-Vollständigkeit prüfen
const errors: string[] = []
const seenPaths = new Set<string>()
function walk(route: any, prefix = '') {
const path = `${prefix}${route.path ?? ''}`
if (route.children) {
for (const child of route.children) walk(child, path === '/' ? '' : path)
} else if (path && !path.startsWith('$')) {
seenPaths.add(path)
}
}
walk(routeTree)
for (const pathname of seenPaths) {
const entries = generateHreflang(pathname, SITE_URL)
// Jedes Locale muss genau einmal auftreten
const codes = entries.map((e) => e.hreflang).filter((c) => c !== 'x-default')
const expected = new Set(LOCALES)
const actual = new Set(codes)
if (codes.length !== expected.size || [...expected].some((l) => !actual.has(l))) {
errors.push(`${pathname}: hreflang-Cluster hat ${codes.length} Einträge, erwartet ${expected.size}`)
}
// x-default muss vorhanden sein
if (!entries.some((e) => e.hreflang === 'x-default')) {
errors.push(`${pathname}: x-default hreflang fehlt`)
}
}
if (errors.length) {
console.error('hreflang CI-Check fehlgeschlagen:\n' + errors.join('\n'))
process.exit(1)
}
console.log(`hreflang CI-Check bestanden über ${seenPaths.size} Pfade × ${LOCALES.length} Locales`)
In die PR-Pipeline einbinden. Bei TanStack Ship läuft dies in GitHub Actions bei jedem PR, das app/router/ oder i18n/ berührt – und seitdem hatten wir keinen einzigen Cluster-Fehler mehr.
Echte Daten: 90 Tage nach dem Launch
Hier sind die tatsächlichen Produktionszahlen von TanStack Ship nach 90 Tagen:
- Nicht-englische organische Klicks: +47 % im Vergleich zu den 90 Tagen vor dem Launch (Google Search Console)
- Nicht-englische Anmeldungen: +38 % (Anmeldungsattribution über
?lang-Query-Parameter + UTM?utm_content=locale) - Indexierte Locale-Seiten: 1.432 über 12 Locales (Search Console → Seitenbericht)
- hreflang-Cluster-Fehler: 0 (runter von 184 beim Launch vor dem CI-Check)
- Median-Position für nicht-englische Suchanfragen: 6,2 (vs. 14,8 für englische SERPs in denselben vertikalen Bereichen)
- Zeit pro Locale: ca. 6 Stunden inklusive Übersetzungs-QA
Der Zuwachs war nicht gleichmäßig. Deutsch (/de) und Japanisch (/ja) lieferten überproportionale Ergebnisse – beide sind unterversorgte B2B-SaaS-Märkte, in denen englische SERPs nicht das Standard-Sucherlebnis sind. Spanisch (/es) und Portugiesisch (/pt) performeden unterdurchschnittlich, bis ich brasilianisches Portugiesisch (/pt-BR) als regionalen Override hinzugefügt habe, was diese beiden Locales innerhalb von 4 Wochen zusätzlich um 18 % steigerte.
Weiterführende Lektüre: Multilingual Seo Saas Strategy · Technical Seo Audit For Saas The 20 Point Checklist Before You Scale · D1 Implementation For Multi Tenant Saas
FAQ: Multilingual SEO-Implementierung
Sollte ich Unterverzeichnis, Subdomain oder ccTLD verwenden?
Unterverzeichnis, es sei denn, es gibt einen regulatorischen Grund für eine Aufteilung (Finanzdienstleistungen, Gesundheitswesen). Der International Guide von Google Search Central bestätigt, dass Google Geo-Targeting unabhängig von der Struktur erkennen kann – aber Unterverzeichnis hält Analytics einfach und konsolidiert die Domain-Autorität.
Brauche ich hreflang, wenn ich nur 2 Locales habe?
Ja. Ohne hreflang zeigt Google möglicherweise die englische Seite einem deutschsprachigen Nutzer – oder behandelt beide Locales als Duplicate Content und entfernt eines aus dem Index. Die hreflang-Spezifikation ist eindeutig, dass jeder Cluster auf jede andere Seite verweisen muss, einschließlich sich selbst.
Kann ich hreflang clientseitig generieren?
Nein. Google liest hreflang aus dem initialen HTML-Response, nicht nach der Hydration. Wenn Sie hreflang in einem React-useEffect generieren, sieht Google es nicht. Die HreflangMeta-Komponente oben rendert die <link>-Tags im serverseitig gerenderten HTML-Payload – das ist das korrekte Muster.
Was ist mit regionalen Varianten wie en-GB vs. en-US?
Liefern Sie regionale Varianten nur, wenn Sie tatsächlich unterschiedliche Inhalte für jede Region haben (Preise, rechtliche Angaben, Einheiten, Währungen). Wenn Sie en-GB und en-US mit identischen Inhalten ausliefern, behandelt Google sie als Duplikate und entfernt eines. Das TanStack Ship-Muster verwendet nur en für Englisch und reserviert Regionalcodes für /pt-BR (brasilianisches Portugiesisch), wo wir tatsächlich unterschiedliche Preise haben.
Wie lange dauert das Ranking in einem neuen Locale?
In meinen Daten: 4–8 Wochen für eine Übersetzung einer bestehenden, gut rankenden englischen Seite. Der hreflang-Cluster und das per-Locale-Sitemap sorgen für eine schnelle Indexierung; das Ranking erfordert weiterhin gute Inhalte und Backlinks – dieselbe Dynamik wie bei englischen SERPs.
Multilingual SEO für SaaS ist ein Implementierungsproblem, kein Übersetzungsproblem. Wenn Sie zuerst die Strategie-Framework möchten, lesen Sie Multilingual SEO for SaaS: 8 Strategies to Triple International Traffic. Wenn Sie den Produktions-Boilerplate mit TanStack Router, per-Locale Sitemaps, hreflang-Generierung, JSON-LD-Schema und dem CI-Check eingerichtet haben möchten, TanStack Ship liefert alle 12 Locales vorkonfiguriert mit verifizierten hreflang-Clustern und null Cluster-Fehlern. Siehe TanStack Ship Features und Preise für enthaltene Leistungen.