SPA-SEO
So machen Sie Single-Page-Anwendungen (React Router, Vue Router, Angular Router) crawlbar und indexierbar – die Probleme mit App-Shell und Soft-404, History API vs. Hash-Routing, pro Route HTML per SSR/Prerendering, pro Route Canonicals und Titel sowie Sitemap-Generierung für clientseitige Routen.
Sprachen
Eine Single-Page-Anwendung lädt ein Dokument und tauscht Ansichten mit JavaScript aus, anstatt eine neue Seite vom Server anzufordern. Viele SPAs implementieren das mit einer 'App-Shell' – der Server liefert echtes HTML für eine URL, eine fast leere Hülle, bis JS jede Route rendert – aber das ist eine häufige Implementierungsentscheidung, keine Regel, der jede SPA folgt; prüfen Sie, was eine direkte Anfrage an jede Route tatsächlich zurückgibt, bevor Sie das annehmen. Die Lösung, wo eine bloße App-Shell-Einrichtung das Problem ist, hat zwei unabhängige Hälften: Adressierbarkeit (History API, nicht Hash/#!-Fragmente, damit jede Ansicht eine echte URL hat – eine Browser-Soft-Navigation über die History API ändert URL und UI, erzeugt aber selbst keine neue Serverantwort) und Inhaltsverfügbarkeit (SSR, Prerendering oder ein Meta-Framework, damit jede dieser URLs bei Anfrage eindeutiges HTML zurückgeben kann). Darüber hinaus überprüfen Sie Titel, Canonical und Robots-Status jeder Route sowohl beim direkten Einstieg als auch nach dem Rendern, behandeln Sie Soft-404s (Router tendieren dazu, einen 200-Status für 'Nicht gefunden'-Ansichten beizubehalten – leiten Sie auf eine URL um, die selbst einen echten Fehlerstatus liefert, oder fügen Sie ein gerendertes noindex hinzu, obwohl ein anfängliches noindex dazu führen kann, dass das Rendern übersprungen wird), und stellen Sie sicher, dass jede Route in Ihrer Sitemap tatsächlich zu echtem, indexierbarem HTML auflöst – es gibt kein spezielles SPA-Sitemap-Format.
TL;DR — Eine Single-Page-Anwendung (SPA) lädt eine Seite vom Server und wechselt dann mithilfe von JavaScript zwischen “Seiten”, ohne die Seite vollständig neu zu laden. Der Haken: Viele SPAs senden unabhängig von der angeforderten URL dieselbe nahezu leere Startseite an Suchmaschinen — das ist ein häufiges Risiko der üblichen SPA-Architektur, keine Eigenschaft jeder SPA. Um das zu beheben, stellen Sie sicher, dass jede Route eine eigene echte URL und eigenes echtes HTML hat — in der Regel durch serverseitiges Rendern oder Vorab-Erzeugen der Seiten.
Was eine SPA ist
Eine Single-Page-Anwendung aktualisiert üblicherweise clientseitige Ansichten und Routen ohne vollständige Dokumentnavigation. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Google kann JavaScript-SPAs rendern, aber crawlbare URLs, Links, Statusbehandlung und gerenderte Inhalte bleiben notwendig. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Eine Single-Page-Anwendung ist eine Website, die als eine einzige HTML-Seite aufgebaut ist. Wenn Sie herumklicken — von der Produktseite zur Über-uns-Seite — tauscht JavaScript den Bildschirminhalt aus, statt vom Server eine komplett neue Seite anzufordern. React (mit React Router), Vue (mit Vue Router) und Angular funktionieren standardmäßig auf diese Weise. Das fühlt sich schnell und app-ähnlich an, weshalb es beliebt ist.
Das Risiko liegt darin, was der Server sendet. Viele SPAs verwenden eine “App-Shell”-Implementierung:
Beim ersten Laden der App (auch durch Google) gibt der Server eine weitgehend leere Hülle zurück,
und der eigentliche Inhalt wird anschließend im Browser aufgebaut. Das ist eine
häufige Art, eine SPA zu erstellen — nicht die Definition einer solchen und nicht etwas, das jede
SPA tut. Wenn eine Website jedoch eine nackte App-Shell ausliefert, ist das Fehlerbild real: Wenn
Google den Server separat nach /about und /products fragt, kann es für beide dieselbe
leere Hülle erhalten, weil nichts an der Route geändert hat, was der Server
sendet. Um zu wissen, ob Ihre App in dieser Situation ist, prüfen Sie, was eine direkte
Anfrage an jede Route tatsächlich zurückgibt — und nehmen Sie es nicht aufgrund der Tatsache an, dass es sich um eine
SPA handelt.
Warum das dem SEO schadet
Suchmaschinen müssen Ihre Inhalte sehen, um sie zu ranken. Bei einer nackten SPA gehen drei Dinge häufig schief:
- Jede URL sieht für den Server gleich aus. Deep Links, Shares und Crawler landen alle auf derselben Hülle.
- 404-Seiten melden trotzdem “200 OK”. Ein JavaScript-Router kann einen “Nicht gefunden”-Bildschirm anzeigen, während die Seite technisch Erfolg meldet, sodass Google leere Seiten indexieren kann.
- Die falschen URLs. Ältere SPAs verwendeten Adressen wie
example.com/#/products. Google kann diese nicht zuverlässig indexieren.
So beheben Sie es (die Kurzfassung)
- Geben Sie jeder Ansicht eine echte URL mithilfe der History-API des Browsers (saubere Pfade wie
/products), nicht#-basierte. - Senden Sie echtes HTML für jede URL. Rendern Sie Seiten serverseitig (SSR) oder erstellen Sie diese im Voraus (Prerendering). Wenn Sie neu starten, erspart Ihnen ein Framework, das dies automatisch erledigt — Next.js, Nuxt, SvelteKit, Angular-SSR — die Mühe.
- Geben Sie jeder Seite einen eigenen Titel und eine eigene Beschreibung, die sich bei Routenwechsel ändern.
- Machen Sie Links zu echten Links (
<a href>), nicht zu klickbaren<div>s.
Möchten Sie die Mechanismen — warum Hash-Routing fehlschlägt, die Falle der “restriktivsten Direktive” bei Kanonischen und wie Sie eine Sitemap für clientseitige Routen generieren? Wechseln Sie zum Erweitert-Tab.
TL;DR — Das zentrale SEO-Risiko einer SPA ist nicht “JavaScript” im Allgemeinen — es ist, dass clientseitiges Routing ändert, was der Benutzer sieht, ohne zu ändern, was der Server zurückgeben würde. Die Lösung hat zwei unabhängige Hälften, die ständig verwechselt werden: Adressierbarkeit (History-API, eindeutige URLs pro Route, keine
#!-Fragmente) und Inhalts- verfügbarkeit (SSR, Prerendering/SSG oder ein Meta-Framework). Nur Ersteres zu tun, ergibt eine ordentliche Sitemap von URLs, die alle dieselbe Hülle rendern. Zusätzlich zu beidem benötigt jede Route ihr eigenes Kanonisch/Titel/Beschreibung im gerenderten DOM, Sie müssen Soft-404s behandeln, die ein200behalten, und jede Route in der Sitemap muss unabhängig zu echtem HTML auflösen.
Was SPA-SEO tatsächlich ist
SPA ist eine Anwendungsarchitektur, kein garantierter SEO-Fehlzustand. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: MDN: Single-page application Server-Rendering, Prerendering oder sorgfältig implementiertes Client-Rendering können Inhalte zugänglich machen, aber keines garantiert Indexierung. Evidence for this claim Primary standard or official documentation supporting the adjacent article claim. Scope: Protocol semantics and Search behavior are kept separate; no indexing, ranking, or migration-timing guarantee is inferred. Confidence: high · Verified: Google: JavaScript SEO basics
Dies ist der Deep Dive speziell zu Single-Page-Anwendungen – Client-seitiges Routing mit React Router, Vue Router oder Angular Router, bei dem der Server nach dem ersten Laden nie wieder eine vollständige Seite sendet. Er steht neben dem breiteren JavaScript-SEO-Leitfaden (der Parität, Lazy-Loading, Infinite Scroll und JS-Rendering allgemein abdeckt) und den framework-spezifischen Artikeln für React, Next.js, Nuxt, Angular, Vue, Svelte und Astro. Hier interessiert mich nur die Routing-Ebene und was sie für Crawling und Indexierung bedeutet.
Warum nackte, client-seitig geroutete SPAs bei SEO scheitern
Eine URL, eine HTML-Antwort – das App-Shell-Problem
Google beschreibt den Fehlermodus präzise: “Some JavaScript sites may use the app
shell model where the initial HTML does not contain the actual content and Google needs
to execute JavaScript before being able to see the actual page content.” (Übersetzung)
„Einige JavaScript-Websites verwenden möglicherweise das App-Shell-Modell, bei dem das
anfängliche HTML nicht den eigentlichen Inhalt enthält und Google JavaScript ausführen
muss, bevor es den tatsächlichen Seiteninhalt sehen kann.“ In einer
nackten SPA ist die “App-Shell” alles, was der Server jemals zurückgibt. Rufen Sie
/products frisch auf und Sie erhalten dasselbe fast leere Dokument, das Sie für
/about erhalten würden. Der Inhalt weicht erst ab, nachdem der Browser Ihr JavaScript
ausführt und Ihr Router entscheidet, was angezeigt wird.
Das ist das ganze Problem in einem Satz: Client-seitiges Routing ändert, was der Benutzer sieht, ohne zu ändern, was der Server zurückgeben würde. Alles Weitere – Soft-404s, doppelter Inhalt, fehlende Titel – ist ein Symptom davon.
Der Server sieht nie, welche “Seite” angefordert wurde (warum Hash-Routing scheitert)
Ältere SPAs laden Ansichten aus URL-Fragmenten – example.com/#/products. Googles
genauer Wortlaut: “A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” (Übersetzung) „Eine SPA kann URL-Fragmente (zum Beispiel
die zuvor genannte Produkt-URL) verwenden, um verschiedene Ansichten zu laden.“ Dies
scheitert aus einem spezifischen, mechanischen Grund:
Browser senden das Fragment (alles nach #) nie im HTTP-Request an den Server. Der
Server kann buchstäblich nicht wissen, welche “Seite” angefordert wurde, also kann er
keinen unterschiedlichen Inhalt oder einen unterschiedlichen Statuscode dafür
zurückgeben. Ein Crawler, der die URL einmal abruft, erhält identisches HTML,
unabhängig vom Fragment.
Das ist auch der Grund, warum Google sein AJAX-Crawling-Schema von 2009 im Oktober
2015 offiziell eingestellt hat: “In short: We are no longer recommending the AJAX crawling proposal we
made back in 2009.” (Übersetzung) „Kurz gesagt: Wir empfehlen den AJAX-Crawling-
Vorschlag, den wir 2009 gemacht haben, nicht mehr.“ Der alte
_escaped_fragment_-Workaround erlaubte es Servern,
Fragment-Routen auf Anfrage vorzurendern – aber er umging das Routing-Problem, anstatt
es zu beheben. Googles eigene Empfehlung, die es ersetzt, ist die History API, die unten
behandelt wird.
Soft-404s – Client-seitige Router behalten für alles ein 200
Dies ist in einer nackten SPA nahezu unvermeidlich. Client-seitige Router behalten
per Design den 200-Status der ursprünglichen Seite für jede virtuelle Navigation –
einschließlich “Nicht gefunden”-Zuständen. Google weist explizit darauf hin: “In a single-page application (SPA), this can be
especially difficult. To prevent error pages from being indexed, you can use one or both
of the following strategies.” (Übersetzung) „In einer Single-Page-Anwendung (SPA)
kann dies besonders schwierig sein. Um zu verhindern, dass Fehlerseiten indexiert werden,
können Sie eine oder beide der folgenden Strategien anwenden.“ Und das Warum: “When a SPA is using client-side JavaScript
to handle errors they often report a 200 HTTP status code instead of the appropriate
status code.” (Übersetzung) „Wenn eine SPA clientseitiges JavaScript zur
Fehlerbehandlung verwendet, meldet sie häufig einen HTTP-Statuscode 200 anstelle des
passenden Statuscodes.“
Das Ergebnis ist, dass leere oder Fehleransichten als dünne 200-Seiten indexiert
werden. Google dokumentiert hier genau zwei Strategien, präzise: Leiten Sie auf eine URL
um (oder machen Sie eine vollständige Anfrage), deren Server einen echten
404/Fehlerstatus zurückgibt, oder fügen Sie dem Fehleransicht mit JavaScript ein
noindex-Tag hinzu. Achten Sie auf die zweite: Wenn dieses noindex vom allerersten
Paint an vorhanden ist, anstatt erst hinzugefügt zu werden, nachdem die App entscheidet,
dass die Route ungültig ist, kann dies dazu führen, dass Google das Rendering der Seite
komplett überspringt – überprüfen Sie also, was eine direkte Anfrage bevor irgendein
JS läuft zurückgibt, nicht nur, was danach im gerenderten DOM erscheint.
Duplikate durch gemeinsame Shell – eine mögliche Ursache, keine automatische Diagnose
Es gibt eine zweite, heimtückischere Fehlerart: Verschiedene Routen werden als Duplikate voneinander indexiert, weil sie auf dasselbe gemeinsame Header-/Navigations-/Footer-Grundgerüst reduziert werden. Gary Illyes beschrieb einen Weg, wie das passiert: “I have a bunch of emails in my inbox where the issue is that the centerpiece took forever to load, so rendering timed out (my most likely explanation) and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (Übersetzung) „Ich habe eine Reihe von E-Mails in meinem Posteingang, bei denen das Problem ist, dass das Herzstück ewig zum Laden brauchte, sodass das Rendering eine Zeitüberschreitung hatte (meine wahrscheinlichste Erklärung) und wir eine Reihe von Seiten hatten, die nur das Grundgerüst enthielten. Mit nur dem Grundgerüst sind diese Seiten Duplikate.“ Sein Fix: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (Übersetzung) „Versuchen Sie, die JS-Aufrufe so umzustrukturieren, dass der Inhalt (einschließlich des marginalen Grundgerüsts) zuerst lädt.“ (Über einen LinkedIn-Beitrag weitergegeben, der einer automatisierten Verifizierung widersteht; als sekundäre Quelle mit hoher Vertrauenswürdigkeit behandeln.)
Beachten Sie, dass Illyes es als „meine wahrscheinlichste Erklärung“ formuliert, nicht als bestätigte Diagnose – das sollte man wörtlich nehmen. „Render-Timeout“ ist nichts, was man allein anhand der Symptome diagnostizieren kann; Sie benötigen Nachweise auf Anfrageebene (was ein direkter Abruf tatsächlich zurückgibt), Nachweise für die gerenderte Ausgabe (ob der zentrale Inhalt nach dem Rendering vorhanden ist) und Search-Console-Nachweise (Duplikat-/Indexierungssignale), bevor Sie das Duplikat-Inhaltsproblem einer Route speziell auf ein Timeout zurückführen, statt auf einen Fehler, eine blockierte Ressource oder eine wirklich identische Shell. Es gibt auch kein festes, veröffentlichtes Timeout, um das herum man planen könnte – Googles eigenes Grundlagendokument sagt, dass eine Seite “may stay on this queue for a few seconds, but it can take longer than that,” (Übersetzung) „möglicherweise einige Sekunden in dieser Warteschlange bleibt, aber es kann länger dauern“, ohne sich auf eine Zahl festzulegen. Planen Sie nicht um eine angenommene Render-Warteschlangendauer herum; stellen Sie stattdessen sicher, dass der primäre Inhalt so früh wie möglich lädt, unabhängig davon, wie lange das Rendering letztendlich dauert.
Der Fix: Echtes HTML pro Route
Der vollständige Fix hat zwei unabhängige Teile, die ständig verwechselt werden:
- URL-Adressierbarkeit – History API, eindeutige URLs pro Route, keine Fragmente.
- Inhaltsverfügbarkeit – SSR, statisches Vorrendern/SSG oder ein Meta-Framework.
Wenn Sie nur #1 tun, erhalten Sie saubere, teilbare URLs, die alle weiterhin dieselbe leere Shell zurückgeben. Für eine Route, die Sie tatsächlich indexiert haben möchten, brauchen Sie beides.
Das ist jedoch kein universelles Gebot, überall SSR einzuführen. SSR und Vorrendern reduzieren, wie stark Sie davon abhängen, dass ein Crawler Ihr JavaScript erfolgreich ausführt – sie werden nicht automatisch Pflicht, sobald eine Website technisch gesehen eine SPA ist. Bevor Sie eine Architektur für eine bestimmte Route wählen, prüfen Sie drei Dinge: ob diese Route überhaupt ranken muss (ein internes Admin-Panel tut das nicht), was eine direkte, JS-freie Anfrage an sie bereits zurückgibt (manche Setups senden bereits sinnvolles HTML) und welche Crawler Sie tatsächlich zufriedenstellen müssen (Google rendert JS ziemlich zuverlässig; Bing und die meisten KI-Crawler weniger – siehe unten). CSR, das diese Prüfungen bereits besteht, muss nicht zu SSR werden, nur weil die Website eine SPA ist.
Serverseitiges Rendering (SSR)
Der Server führt Ihre App für jede Anfrage aus und gibt vollständig geformtes HTML für diese Route zurück, dann „hydratisiert“ der Client es in eine lebende SPA. Dies ist die robusteste Option, weil der Crawler beim ersten Abruf vollständigen Inhalt erhält, ohne dass JS-Ausführung erforderlich ist.
Statisches Vorrendern / SSG
Statt pro Anfrage zu rendern, erstellen Sie das HTML jeder Route im Voraus beim Deployment. Perfekt für Inhalte, die sich nicht pro Benutzer ändern. Aus meinem eigenen JavaScript-SEO-Schreiben: Jede Art von SSR, statischem Rendering und Vorrendern-Setup ist für Suchmaschinen in Ordnung – was Sie vermeiden sollten, ist, Inhalte hinter rein clientseitigem Rendering einzuschließen.
Oder: Nicht selbst zusammenbauen – ein Meta-Framework verwenden
Für einen Neubau ist die ehrliche Empfehlung, kein handgebautes clientseitiges Routing ausschließlich mit React Router oder Vue Router zu verwenden. Ein Meta-Framework – Next.js, Nuxt, Angular mit seinem SSR-Paket, SvelteKit oder Remix – bietet SSR/SSG und routenbasierte Metadaten direkt mit, wodurch diese gesamte Problemkategorie umgangen wird. (Jedes davon hat auf dieser Website einen eigenen ausführlichen Leitfaden.) Seien Sie ehrlich über die Kehrseite: Die Nachrüstung von SSR auf eine bestehende handgebaute SPA ist echte Entwicklungsarbeit – ein Migrationsprojekt, kein Konfigurationsschalter.
Dynamisches Rendering als Übergangslösung, nicht als Ziel
Sie können Crawlern einen separat gerenderten HTML-Snapshot (dynamisches Rendering) ausliefern. Sowohl Google als auch Bing akzeptieren das – Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (Übersetzung) „empfiehlt dynamisches Rendering als hervorragende Alternative für Websites, die stark auf JavaScript angewiesen sind“ (weitergegeben aus Bings Blog von 2018; die beiden kurzen Zitate zu den Fähigkeiten von bingbot unten sind direkt verifiziert, dieses längere ist nicht unabhängig überprüft) – aber Google stellt klar, dass es eine Problemumgehung ist: “Dynamic rendering was a workaround and not a long-term solution… Instead, we recommend that you use server-side rendering, static rendering, or hydration as a solution.” (Übersetzung) „Dynamisches Rendering war eine Problemumgehung und keine langfristige Lösung … Stattdessen empfehlen wir serverseitiges Rendering, statisches Rendering oder Hydration als Lösung.“ (Weitergegeben aus Googles Dokumentation zum dynamischen Rendering; der Wortlaut stimmt mit dem überein, was bereits im umfassenderen JavaScript-SEO-Leitfaden dieser Website zitiert wird.) Es ist eine Brücke, keine Architektur.
History API vs. Hash-Routing (#!)
Fünf Zustände, nicht zwei
Das Testen einer SPA-Route wird verwirrend, weil „funktioniert es“ tatsächlich fünf verschiedene, separat überprüfbare Zustände umfasst:
| Zustand | Was es ist |
|---|---|
| Serverantwort | Die Bytes, die eine frische, JS-freie HTTP-Anfrage an eine URL tatsächlich zurückbekommt. |
| Gerendertes DOM | Was ein Browser (oder der Renderer von Googlebot) aufbaut, nachdem er JavaScript gegen diese Serverantwort ausgeführt hat. |
| Suchverarbeitung | Wie Google die Serverantwort separat crawlt, die Seite später rendert und auf Basis von beidem indexiert. |
| Vollständige Browser-Navigation | Eine tatsächliche neue HTTP-Anfrage an eine URL – der einzige Zustand, der die Serverantwort oder den Statuscode ändern kann. |
| Soft-Navigation im Browser | Ein History-API-Übergang (pushState/replaceState), der die sichtbare URL, den Browserverlauf und die On-Screen-Benutzeroberfläche ändert. |
Der Punkt, den alle verwechseln: Eine Soft-Navigation ändert die URL und die Benutzeroberfläche, erzeugt aber nicht von sich aus eine neue HTTP-Antwort oder einen neuen Status – das passiert nur bei einer vollständigen Navigation (oder einer gleichwertigen direkten Anfrage, wie curl). Das Testen einer Route durch Durchklicken der App von der Startseite aus übt die Soft-Navigation; das Testen durch direkte Anfrage der URL übt die Serverantwort. Beides ist wichtig, und beides kann unterschiedlich ausfallen.
Was Ihnen die History API bietet
Die Empfehlung von Google ist eindeutig: “We recommend using the History API to load different content based on the URL in a SPA.” (Übersetzung) „Wir empfehlen, in einer SPA die History API zu verwenden, um abhängig von der URL unterschiedliche Inhalte zu laden.“ (Im Live-Dokument ist „History API“ ein Link, daher zielt dieser Deep Link auf den Einleitungssatz.) Die History API (pushState/replaceState) ermöglicht es Ihrem Router, die sichtbare URL in einen echten, mit Lesezeichen versehbaren Pfad zu ändern – /products, nicht /#/products – ohne vollständiges Neuladen. Das behebt die Adressierbarkeits-Hälfte: Jede Ansicht hat jetzt eine URL, auf die ein Server anders antworten könnte.
Warum Fragment-/Hash-URLs für den Server – und für Google – unsichtbar sind
Da das Fragment den Server nie erreicht (siehe oben), ist die History API der einzige Weg, jeder Route eine URL zu geben, die der Server tatsächlich ausliefern kann. Google legt in seinem Grundlagendokument eine Untergrenze fest: “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (Übersetzung) „Verwenden Sie keine Fragmente, um unterschiedliche Seiteninhalte zu laden. Das folgende Beispiel ist ungeeignet, weil Googlebot die URLs nicht zuverlässig auflösen kann.“ Dies ist dieselbe Anleitung, die ich auch anderswo geschrieben habe – verwenden Sie normal aussehende URLs wie /products, nicht Hash-URLs wie /#/products, weil Google die Hash-URLs nicht zuverlässig indexieren kann.
Die Einstellung von 2015, kurz
Die Hash-Bang-Ära (#!) endete mit Googles Beitrag von 2015: “Times have changed. Today, as
long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are
generally able to render and understand your web pages like modern browsers.” (Übersetzung)
„Die Zeiten haben sich geändert. Solange Googlebot nicht am Crawlen Ihrer JavaScript-
oder CSS-Dateien gehindert wird, können wir Ihre Webseiten heute im Allgemeinen wie
moderne Browser rendern und verstehen.“ Außerdem heißt es: “you can use the History API
pushState() to ensure accessibility for a wider range of browsers (and our systems).”
(Übersetzung) „Sie können pushState() aus der History API verwenden, um die
Zugänglichkeit für eine größere Bandbreite von Browsern (und unsere Systeme) sicherzustellen.“
Eine Nuance, die man behalten sollte: Google hat alte Hash-Bang-Seiten nicht sofort
deindexiert — “we’ll generally crawl, render, and index the #! URLs” (Übersetzung)
„Wir werden die #!-URLs im Allgemeinen crawlen, rendern und indexieren“ — aber
“we can still try” (Übersetzung) „Wir können es weiterhin versuchen“ bedeutet nicht
“you should still do this” (Übersetzung) „Sie sollten dies weiterhin tun“.
Jede Route unabhängig indexierbar machen
Saubere URLs und echtes HTML sorgen dafür, dass Sie gecrawlt werden. Um korrekt indexiert zu werden, braucht jede Route ihre eigenen Signale.
Canonical-Tags pro Route – und die Falle der „restriktivsten Direktive“
Jede Route braucht ihr eigenes rel=canonical im gerenderten DOM. Die Falle: Wenn ein
Platzhalter-Canonical (oder ein noindex) im rohen HTML-Grundgerüst ausgeliefert wird und JavaScript es später
überschreiben soll, kann es zu einem Konflikt kommen. Google löst Konflikte
zwischen der rohen und der gerenderten Version, indem es das restriktivere Signal übernimmt – wie ich es
schon früher formuliert habe: Google wählt die restriktivsten Aussagen zwischen dem HTML und
der gerenderten Version einer Seite. Ein versehentliches noindex oder ein falsches Canonical im
Grundgerüst kann die gesamte Route stillschweigend unterdrücken, selbst nachdem Ihr JS es „korrigiert“ hat.
Titel und Meta-Descriptions pro Route per JavaScript
Diese per JS zu setzen ist in Ordnung – Google sagt das direkt: “You can use JavaScript to set
or change the meta description as well as the <title> element.” (Übersetzung) „Sie
können JavaScript verwenden, um sowohl die Meta-Description als auch das <title>-Element
festzulegen oder zu ändern.“ Die Anforderung ist,
dass sie im gerenderten DOM landen, das Google auswertet, und nicht nur kurz aufblitzen. Geben Sie jeder
Seite ihren eigenen Titel und ihre eigene Beschreibung, die sich bei einem Routenwechsel aktualisieren.
Echte <a href>-Links zwischen Routen
Machen Sie Ihre Links zwischen Routen zu echten Ankern mit href-Attributen, nicht zu Klick-Handlern auf
<div>s. Google entdeckt URLs, indem es hrefs extrahiert; ein <div onClick>, das über den
Router navigiert, ist für die Link-Extraktion des Crawlers unsichtbar. Und verlassen Sie sich nicht auf
clientseitigen Zustand, um Inhalte über diese Navigationen zu transportieren – der Renderer von Google “does
not retain state across page loads: Local Storage and Session Storage data are cleared
across page loads. HTTP Cookies are cleared across page loads.” (Übersetzung) „behält
keinen Zustand über Seitenladevorgänge hinweg: Daten im Local Storage und Session Storage
werden zwischen Seitenladevorgängen gelöscht. HTTP-Cookies werden zwischen
Seitenladevorgängen gelöscht.“
Sitemap-Generierung für SPA-Routen
Es gibt kein spezielles „SPA-Sitemap“-Format – es ist ein Abrechnungsproblem
Das Sitemap-Protokoll ist für SPAs unverändert. Die eigentliche Arbeit besteht darin, sicherzustellen, dass jede Route, die Sie separat auflisten, echtes, eindeutiges, gerendertes HTML ergibt. Eine Sitemap mit 500 clientseitigen Routen ist wertlos, wenn alle diese Routen dasselbe Grundgerüst zurückgeben. Die Sitemap ist also eine Folge Ihrer Rendering-Strategie, kein Ersatz dafür.
Umgang mit dynamischen/parametrisierten Routen (/product/:id)
Bei Apps mit parametrisierten Routen können Sie die Liste nicht manuell pflegen. Der Sitemap-
Generator muss gegen dieselbe Datenquelle laufen, die auch die App verwendet – ein Build-Skript oder ein
Server-Endpunkt, der jede id aufzählt – damit Sitemap und App nie auseinanderdriften.
Diese aufgezählten URLs sind nur dann eine Auflistung wert, wenn sie per SSR oder Prerendering gerendert wurden.
Die Sitemap mit dem synchron halten, was serverauflösbar ist
Generieren Sie die Sitemap als Teil Ihres Builds neu oder in einem Zeitplan, der an Ihre Inhaltsquelle
gekoppelt ist. Eine Route, die 404 liefert (oder schlimmer, Soft-404 bei 200), aber in Ihrer Sitemap steht, ist ein
Crawl-Budget- und Qualitätssignal, das Sie nicht wollen.
Testen, was Google tatsächlich sieht
Überprüfen Sie nicht nur eine URL stichprobenartig. Der ganze Sinn des SPA-Problems ist, dass sich Routen im Browser unterscheiden können, aber nicht auf der Leitung, also testen Sie mehrere Routen auf der Ebene des rohen HTML:
- Rufen Sie das rohe HTML pro Route ab mit
curl(bei Bedarf mit einem Googlebot-User-Agent) und bestätigen Sie, dass der Inhalt pro URL eindeutig ist, nicht die gemeinsame Hülle. - URL Inspection in der Search Console – vergleichen Sie das gecrawlte/gerenderte HTML für mehrere Routen und bestätigen Sie, dass Titel, Beschreibung und Canonical pro Route vorhanden sind.
- Bestätigen Sie, dass Fehlerrouten das richtige Signal zurückgeben – eine „Nicht gefunden“-Ansicht sollte entweder
auf einen echten Fehlerstatus umleiten oder
noindexim gerenderten DOM tragen.
Häufige Mythen über SPA-SEO
- „Google kann SPAs überhaupt nicht indexieren.“ Veraltet. Google verwendet Evergreen-Chromium und rendert SPA-Inhalte im Allgemeinen. Die eigentlichen Risiken sind spezifisch: Soft-404s, Hash-Routing, Rendering-Timeouts und Nicht-Google-Crawler (Bing weniger zuverlässig, die meisten KI-Crawler gar nicht).
- „Die History-API zu verwenden, behebt SPA-SEO.“ Sie behebt nur die Adressierbarkeit. Wenn der Server weiterhin für jede Route dieselbe Hülle zurückgibt, muss Google weiterhin JS ausführen, um etwas zu sehen.
- „Hash-Routing funktioniert weiterhin als Fallback.“ Seit 2015 veraltet und seitdem davon abgeraten.
- „Client-seitiges Rendering ist eine Ranking-Strafe.“ Es gibt keine direkte CSR-Strafe. Der Schaden ist indirekt – fehlgeschlagenes/verzögertes Rendering, Soft-404s und doppelte Clusterbildung durch Rendering-Timeouts reduzieren, was indexiert wird.
- „Pre-Rendering für Bots ist Cloaking.“ Nicht, wenn der Inhalt dem entspricht, was Benutzer letztendlich sehen – nur das Wann/Wo des Renderings unterscheidet sich, nicht der Inhalt.
- „Sie benötigen einen speziellen SPA-Sitemap-Generator.“ Es gibt kein spezielles Format; die Arbeit besteht darin, jede aufgeführte Route in echtes HTML aufzulösen.
Häufig gestellte Fragen
Kann Google eine Single-Page-Anwendung indexieren? Ja, wenn jede Route in echtes, eindeutiges HTML (per SSR/Prerendering) unter einer echten URL aufgelöst wird. Reine Client-only-SPAs tun das oft nicht.
Brauche ich SSR, oder ist Client-seitiges Rendering jemals in Ordnung? CSR kann für Inhalte funktionieren, die nicht ranken müssen, aber für alles, was Sie zuverlässig indexiert haben möchten, prerendern oder SSR verwenden – verlassen Sie sich nicht darauf, dass der Renderer Ihr JS rechtzeitig ausführt.
Ist Hash-Routing (#!) schlecht für SEO? Ja – das Fragment erreicht den Server nie, daher
kann Google diese URLs nicht zuverlässig auflösen. Verwenden Sie die History-API.
Braucht jede Route ihr eigenes Canonical-Tag? Ja, im gerenderten DOM – und stellen Sie sicher,
dass nichts Restriktiveres (ein versehentliches noindex oder falsches Canonical) in der rohen Hülle ausgeliefert wird.
Ist ein Prerendering-Dienst Cloaking? Nein, vorausgesetzt, der Bots ausgelieferte Inhalt entspricht dem, was Benutzer sehen.
Sollte ich ein Meta-Framework verwenden, anstatt das Routing selbst zu bauen? Für einen neuen Build, normalerweise ja – Next.js/Nuxt/Angular-SSR/SvelteKit bieten Ihnen SSR/SSG und Metadaten pro Route kostenlos.
Warum gibt meine SPA 200 für Seiten zurück, die nicht existieren? Weil der Client-seitige
Router den ursprünglichen 200-Status für virtuelle Navigationen beibehält. Beheben Sie es mit einer JS-Umleitung
zu einem echten Fehlerstatus oder einem gerenderten noindex.
KI-Zusammenfassung
Eine komprimierte Darstellung der erweiterten Version:
- SPA-SEO = speziell clientseitiges Routing (React Router, Vue Router, Angular Router). Eine SPA ist dadurch definiert, dass ein Dokument geladen und Ansichten mit JavaScript ausgetauscht werden – eine „App-Shell“, die für jede URL nahezu leeres HTML sendet, ist eine häufige Implementierung, aber keine Regel, der jede SPA folgt; prüfen Sie, was eine direkte Anfrage tatsächlich zurückgibt, anstatt anzunehmen.
- Das Kernrisiko: Clientseitiges Routing kann ändern, was der Benutzer sieht,
ohne zu ändern, was der Server zurückgeben würde. Bei einer nackten App-Shell-SPA
können direkte Abrufe von
/productsund/aboutbyteidentisches rohes HTML liefern. - Fünf Zustände werden vermischt: Serverantwort, gerendertes DOM, Suchverarbeitung, Browser-Vollseitennavigation und Browser-Soft-Navigation. Ein History-API-Übergang ändert URL und UI, erzeugt aber selbst keine neue Serverantwort oder keinen neuen Status.
- Drei Symptome einer nackten Shell: das App-Shell-Problem, Soft-Fehlerseiten (Router
behalten ein
200für „nicht gefunden“ – Googles zwei dokumentierte Fixes sind die Weiterleitung zu einer URL, die selbst einen echten Fehlerstatus liefert, oder ein per JS hinzugefügtesnoindex; ein anfänglichesnoindexkann dazu führen, dass das Rendern übersprungen wird) und Duplikate der gemeinsamen Shell – wobei „Render-Timeout“ (Illyes) eine mögliche Ursache ist, aber keine automatische Diagnose ohne Anfrage-/Render-/Such- Belege. - Hash-Routing scheitert mechanisch: Das Fragment nach
#erreicht den Server nie, daher kann der Server nicht nach Route unterscheiden. Von Google 2015 als veraltet eingestuft; die History-API bietet Adressierbarkeit, aber keine vollständige Indexierbarkeitslösung. - Der Fix hat zwei unabhängige Hälften, wenn eine nackte Shell das Problem ist: Adressierbarkeit (History-API, eindeutige URLs pro Route) und Inhaltsverfügbarkeit (SSR, Prerendering/SSG oder ein Meta-Framework) – aber SSR ist keine universelle Anforderung; treffen Sie die Entscheidung basierend darauf, ob die Route ranken muss, was sie bereits direkt zurückgibt und welche Crawler sie rendern müssen.
- Pro Route: eigene Canonical-/Title-/Description-Tags im gerenderten DOM;
achten Sie auf die Falle der „restriktivsten Direktive“, bei der ein rohes
HTML-
noindex/Canonical Ihr JS überschreibt. Echte<a href>-Links; verlassen Sie sich nicht auf Client-Zustand (WRS löscht Speicher/Cookies über Ladevorgänge hinweg). - Sitemaps: Eine Route aufzulisten beweist nicht, dass sie auflöst – es gibt kein spezielles Format; jede aufgeführte Route muss unabhängig zu echtem HTML auflösen, und dynamische Routen benötigen einen Generator, der an die Datenquelle der App gebunden ist.
- Dynamisches Rendering ist eine von Google/Bing akzeptierte Übergangslösung, keine langfristige Architektur.
- Testen Sie mehrere Routen auf der Ebene des rohen HTML (direkte Anfragen), nicht nur durch Navigation innerhalb der App.
Offizielle Dokumentation
Primärquellen-Dokumentation der Suchmaschinen.
- Verstehen Sie die JavaScript-SEO-Grundlagen – das App-Shell-Modell, „Verwenden Sie die History-API anstelle von Fragmenten“ und das Festlegen von Titeln/Beschreibungen über JS.
- Beheben Sie suchbezogene JavaScript-Probleme – der Abschnitt zu SPA-Fehlerseiten mit Status 404, „Verwenden Sie keine URL-Fragmente, um unterschiedliche Inhalte zu laden“ und die Empfehlung zur History-API.
- Dynamisches Rendering als Workaround – warum dynamisches Rendering eine Übergangslösung und keine langfristige Lösung ist.
- Einstellung unseres AJAX-Crawling-Schemas (2015) – das formelle Ende des Hash-Bang-Crawlings und die pushState-Empfehlung.
- Erstellen und einreichen einer Sitemap – allgemeines Sitemap-Protokoll (es gibt kein SPA-spezifisches Format).
Bing / Microsoft
- bingbot-Serie: JavaScript, dynamisches Rendering und Cloaking. Oh je! – die JS-Rendering-Fähigkeiten von bingbot und seine Haltung zum dynamischen Rendering.
Zitate aus der Quelle
Offizielle Aussagen von Google und Bing. Jeder Link ist ein Deep Link, der direkt zur zitierten Passage auf der Quellseite springt.
Google – das App-Shell-Problem
- “Some JavaScript sites may use the app shell model where the initial HTML does not contain the actual content and Google needs to execute JavaScript before being able to see the actual page content.” (Übersetzung) „Einige JavaScript-Websites verwenden möglicherweise das App-Shell-Modell, bei dem das anfängliche HTML nicht den tatsächlichen Inhalt enthält und Google JavaScript ausführen muss, bevor es den tatsächlichen Seiteninhalt sehen kann.“ Zum Zitat springen
Google – Hash-/Fragment-Routing und die History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (Übersetzung) „Eine SPA kann URL-Fragmente (zum Beispiel die zuvor genannte Produkt-URL) zum Laden verschiedener Ansichten verwenden.“ Zum Zitat springen
- “We recommend using the History API to load different content based on the URL in a SPA.” (Übersetzung) „Wir empfehlen, in einer SPA die History API zu verwenden, um basierend auf der URL verschiedene Inhalte zu laden.“ Zum Zitat springen (Im Live-Dokument ist „History API“ ein Link, daher zielt dieser Deep Link auf den einleitenden Satzteil; der vollständige Satz ist in der Reihenfolge wörtlich vorhanden.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (Übersetzung) „Verwenden Sie keine Fragmente, um verschiedene Seiteninhalte zu laden. Das folgende Beispiel ist eine schlechte Praxis, da Googlebot die URLs nicht zuverlässig auflösen kann.“ Zum Zitat springen
Google – Soft-Fehlerseiten bei SPAs
- “In a single-page application (SPA), this can be especially difficult. To prevent error pages from being indexed, you can use one or both of the following strategies.” (Übersetzung) „Bei einer Single-Page-Anwendung (SPA) ist das besonders anspruchsvoll. Damit Fehlerseiten nicht in den Index gelangen, können Sie eine der folgenden Maßnahmen oder beide miteinander kombinieren.“ Zum Zitat springen
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (Übersetzung) „Wenn eine SPA clientseitiges JavaScript zur Fehlerbehandlung verwendet, meldet sie häufig einen HTTP-Statuscode 200 anstelle des entsprechenden Statuscodes.“ Zum Zitat springen
Google – Meta-Tags per JavaScript und zustandsloses Rendering
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (Übersetzung) „Sie können JavaScript verwenden, um die Meta-Description sowie das <title>-Element festzulegen oder zu ändern.“ Zum Zitat springen - “WRS does not retain state across page loads: Local Storage and Session Storage data are cleared across page loads. HTTP Cookies are cleared across page loads.” (Übersetzung) „WRS behält keinen Zustand über Seitenladevorgänge hinweg: Local-Storage- und Session-Storage-Daten werden bei Seitenladevorgängen gelöscht. HTTP-Cookies werden bei Seitenladevorgängen gelöscht.“ Zum Zitat springen
Google – Abschaffung des Hash-Bang-Crawlings (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (Übersetzung) „Kurz gesagt: Wir empfehlen den AJAX-Crawling-Vorschlag, den wir 2009 gemacht haben, nicht mehr.“ Zum Zitat springen
- “Times have changed. Today, as long as you’re not blocking Googlebot from crawling your JavaScript or CSS files, we are generally able to render and understand your web pages like modern browsers.” (Übersetzung) „Heute ist die Situation anders: Wenn Sie Googlebot nicht daran hindern, Ihre JavaScript- oder CSS-Dateien zu crawlen, können wir Webseiten im Allgemeinen wie moderne Browser rendern und verstehen.“ Zum Zitat springen
Google – Dynamic Rendering ist ein Workaround (weitergegeben; Formulierung entspricht dem, was bereits in diesem umfassenderen JavaScript-SEO-Leitfaden der Website zitiert wird, nicht in diesem Durchgang unabhängig neu verifiziert)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (Übersetzung) „Dynamic Rendering war ein Workaround und keine langfristige Lösung für Probleme mit JavaScript-generierten Inhalten in Suchmaschinen.“ Zum Zitat springen
Bing – bingbot und JavaScript
- “bingbot is generally able to render JavaScript.” (Übersetzung) „bingbot ist grundsätzlich in der Lage, JavaScript zu rendern.“ — bingbot-Serie, Bing Webmaster Blog. Beitrag lesen
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (Übersetzung) „bingbot unterstützt nicht unbedingt alle JavaScript-Frameworks, die in der neuesten Version Ihres bevorzugten modernen Browsers unterstützt werden.“ — gleicher Beitrag. Beitrag lesen
Gary Illyes, Google – Rendering-Timeouts erzeugen Duplikate (weitergegeben über einen LinkedIn-Beitrag, der einer automatisierten Verifizierung widersteht; als hochzuverlässige Sekundärquelle behandeln)
- “the centerpiece took forever to load, so rendering timed out… and we were left with a bunch of pages that only had the boilerplate. With only the boilerplate, those pages are dups.” (Übersetzung) „Das Herzstück lud ewig, daher lief das Rendering in einen Timeout … und wir blieben mit einer Reihe von Seiten zurück, die nur das Grundgerüst hatten. Mit nur dem Grundgerüst sind diese Seiten Duplikate.“ Beitrag lesen
Welchen Rendering-Pfad sollte ich wählen?
Arbeiten Sie von oben nach unten. Die Frage ist immer: „Bekommt der Crawler echtes HTML für diese Route, ohne mein JavaScript auszuführen?“ – und beginnen Sie damit, zu bestätigen, dass die Route überhaupt indexiert werden muss, und prüfen Sie, was eine direkte, JS-freie Anfrage bereits zurückgibt; nicht jede Route benötigt SSR, und einige geben bereits nutzbares HTML zurück.
1. Starten Sie einen neuen Build?
- Ja → Verwenden Sie ein Meta-Framework (Next.js, Nuxt, Angular mit SSR, SvelteKit, Remix). SSR/SSG und Metadaten pro Route sind integriert. Stoppen Sie hier.
- Nein → Fahren Sie fort.
2. Ändert sich Ihr Inhalt pro Benutzer / pro Anfrage?
- Nein (größtenteils statischer Inhalt) → Prerendering / SSG zur Build-Zeit. Einfachste robuste Lösung – jede Route ist echtes HTML auf der Festplatte.
- Ja → Server-side Rendering (SSR), sodass jede Anfrage routenspezifisches HTML zurückgibt.
3. Können Sie SSR oder Prerendering derzeit nicht umsetzen (altes, handgebautes SPA)?
- Verwenden Sie Dynamic Rendering als temporäre Brücke – liefern Sie Crawlern einen gerenderten Schnappschuss. Planen Sie die Migration zu SSR/SSG; behandeln Sie dies nicht als Endziel.
4. Unabhängig vom gewählten Pfad bestätigen Sie alle folgenden Punkte pro Route:
- Echte URL über die History API (keine
#!-Fragmente). - Eindeutiger Titel + Meta-Description + Canonical im gerenderten DOM.
- Nichts Restriktiveres (
noindex, falsches Canonical) im rohen Grundgerüst eingebettet. - Echte
<a href>-Links zwischen Routen. - Fehlerrouten geben einen echten Fehlerstatus oder ein gerendertes
noindexzurück (keine Soft-404s). - Jede Route in der Sitemap löst unabhängig zu echtem HTML auf.
SPA-SEO-Checkliste
Adressierbarkeit
- Jede Ansicht hat eine echte URL über die History API (
/products), kein Fragment (/#/products). - Links zwischen Routen sind echte
<a href>-Anker, keine<div onClick>-Handler.
Inhaltsverfügbarkeit
- Jede Route gibt echtes, eindeutiges HTML zurück, ohne dass der Crawler Ihr JavaScript ausführt (SSR, Prerendering/SSG oder ein Meta-Framework).
- Routeninhalt lädt, bevor das Rendering-Fenster schließt (nur-Grundgerüst-Duplikate vermeiden).
Indexierbarkeit pro Route
- Einzigartiger
<title>und Meta-Description pro Route, im gerenderten DOM vorhanden. - Einzigartiges
rel=canonicalpro Route im gerenderten DOM. - Kein versehentliches
noindex/Platzhalter-Canonical im rohen Shell, das ein restriktiveres Signal festlegen könnte.
Fehler
- „Nicht gefunden“-Routen leiten auf eine URL um, deren Server einen echten Fehlerstatus zurückgibt oder
ein per JavaScript hinzugefügtes
noindexträgt (keine weichen Fehlerseiten bei200). - Wenn Sie die
noindex-Route verwenden, stellen Sie sicher, dass sie erst hinzugefügt wird, nachdem die App entschieden hat, dass die Route ungültig ist, und nicht schon beim ersten Paint vorhanden ist – ein anfänglichesnoindexkann dazu führen, dass Google das Rendern der Seite überspringt.
Sitemap
- Jede aufgeführte Route löst unabhängig echtes HTML auf.
- Dynamische Routen (
/product/:id) werden aus der Datenquelle der App aufgezählt, nicht manuell gepflegt.
Verifizierung
- Rohes HTML über mehrere Routen geprüft (curl), bestätigt einzigartigen Inhalt pro URL.
- URL Inspection bestätigt gerenderten Inhalt, Titel, Beschreibung und Canonical pro Route.
Die mentalen Modelle
1. Die zwei Hälften, die Sie nicht vermischen dürfen. Adressierbarkeit (History API, einzigartige URLs, keine Fragmente) und Inhaltsverfügbarkeit (SSR, Prerendering oder ein Meta-Framework) sind unabhängig. Saubere URLs ohne HTML pro Route = eine aufgeräumte Sitemap aus leeren Hüllen. Sie brauchen beides.
2. „Was würde der Server frisch zurückgeben?“
Das ganze SPA-Problem ist, dass die Browseransicht von der Serverantwort abweicht. Fragen Sie für
jede Route, was ein curl ohne JS-Ausführung zurückbekommt. Wenn es die Hülle ist, könnte
der Crawler auch nur die Hülle sehen.
3. Der Standard-Statuscode ist 200 – auch bei Fehlern.
Clientseitige Router behalten den ursprünglichen 200. Gehen Sie davon aus, dass jede „Fehler“-Ansicht ein Soft-404 ist,
bis Sie ihnen absichtlich einen echten Fehlerstatus oder ein gerendertes noindex zuweisen.
4. Die Falle der restriktivsten Direktive.
Google gleicht rohe und gerenderte Signale ab und verwendet das restriktivere. Ein noindex
oder falsches Canonical in der Hülle kann Ihren JS-„Fix“ überschreiben. Prüfen Sie das rohe HTML, nicht nur
den gerenderten DOM.
5. Meta-Framework zuerst für neue Builds. Handgerolltes clientseitiges Routing bedeutet, jedes dieser Probleme selbst zu besitzen. Ein Framework zu wählen, das SSR/SSG und Metadaten pro Route bietet, bedeutet, sich gegen sie zu entscheiden.
SPA-SEO-Spickzettel
Routing
| Ansatz | URL-Beispiel | Erreicht den Server? | Google-sicher? |
|---|---|---|---|
| History API | /products | Ja | Ja (empfohlen) |
| Hash-Routing | /#/products | Nein (Fragment wird nie gesendet) | Nein – veraltet 2015 |
Hash-Bang (#!) | /#!/products | Nein | Nein – Legacy-AJAX-Schema, veraltet |
Rendering-Strategien
| Strategie | Crawler erhält echtes HTML ohne JS-Ausführung? | Am besten für |
|---|---|---|
| SSR | Ja | Inhalte pro Benutzer / pro Anfrage |
| Prerendering / SSG | Ja | Größtenteils statische Inhalte |
| Meta-Framework (Next/Nuxt/etc.) | Ja (eingebaut) | Neue Builds |
| Dynamisches Rendering | Ja, nur für Bots | Temporäre Brücke bei Legacy-SPAs |
| Reines CSR (nur Client) | Nein | Nichts, das Sie zuverlässig indexiert haben möchten |
Must-haves pro Route (alle im gerenderten DOM)
- Einzigartige URL (History API) · einzigartiger
<title>· einzigartige Meta-Description · einzigartigesrel=canonical· echte<a href>-Links · Fehlerrouten mit echtem Status odernoindex.
Schnelle Fakten
- Fragment nach
#wird nie an den Server gesendet – deshalb scheitert Hash-Routing. - Clientseitige Router behalten
200für alles, einschließlich „nicht gefunden“ → Soft-404s. - Google gleicht rohe und gerenderte Signale ab und verwendet das restriktivere.
- Es gibt kein spezielles SPA-Sitemap-Format – jede aufgeführte Route muss echtes HTML auflösen.
SPA-SEO-Anti-Patterns
Ein reines CSR-SPA ausliefern und eine vollständige Sitemap einreichen. Die Sitemap listet 500 Routen; alle 500 geben bei einem frischen Abruf dieselbe Hülle zurück. Die Sitemap lässt Inhalte nicht existieren – Rendering tut das.
Die History API hinzufügen und es als erledigt betrachten. Saubere URLs beheben die Adressierbarkeit, nicht den Inhalt. Ohne SSR/Prerendering gibt der Server weiterhin für jede Route die Hülle zurück.
Hash-/Hash-Bang-Routing „als Fallback.“ Das Fragment erreicht den Server nie, daher kann der Server nicht nach Route unterscheiden. Seit 2015 veraltet; kein Fallback, sondern eine Sackgasse.
Navigation mit <div onClick> statt <a href>.
Google extrahiert hrefs, um URLs zu entdecken. Klick-Handler-Navigation ist für die Link-Extraktion unsichtbar, daher werden diese Routen möglicherweise nie gefunden.
Ein Platzhalter-noindex oder Canonical im rohen Grundgerüst, „das JavaScript überschreiben wird.“
Google übernimmt die restriktivere Variante von roh vs. gerendert. Die Direktive des Grundgerüsts kann gewinnen und die Route stillschweigend unterdrücken.
Rückgabe von 200 für „nicht gefunden“-Ansichten.
Soft-404s führen dazu, dass dünne/leere Seiten indexiert werden. Leiten Sie auf einen echten Fehlerstatus um oder fügen Sie ein gerendertes noindex hinzu.
Langsames Laden des Routeninhalts nach dem Grundgerüst. Render-Timeouts hinterlassen nur das gemeinsame Grundgerüst, und jede Route kollabiert zu einem Duplikat jeder anderen (Illyes). Laden Sie zuerst den zentralen Inhalt.
Sich auf Client-State verlassen, um Inhalte über Navigationen hinweg zu tragen. Der Renderer löscht Local/Session Storage und Cookies bei Seitenaufrufen – Inhalte, die nur im Client-State existieren, sind nicht vorhanden, wenn Google die nächste Route rendert.
Häufige SPA-Indexierungsprobleme
Jede Route gibt dasselbe HTML zurück
Symptom: /products und /about haben unterschiedliche Browser-Ansichten, aber identische rohe Antworten. Wahrscheinliche Ursache: History-API-Routing bietet Adressierbarkeit ohne SSR oder Prerendering. Fix: Erzeugen Sie routenspezifisches HTML und bestätigen Sie, dass eine direkte Anfrage an jeden Pfad seine eigene Überschrift, seinen eigenen Fließtext und seine eigenen Metadaten enthält.
Fehlende Routen erscheinen als erfolgreiche Seiten
Symptom: Eine nicht existierende Route zeigt eine „nicht gefunden“-Ansicht, gibt aber 200 zurück. Wahrscheinliche Ursache: Der Client-Router übernimmt den Fehler, nachdem der Server bereits eine erfolgreiche Grundgerüst-Antwort gesendet hat. Fix: Geben Sie den richtigen Serverstatus zurück; falls das noch nicht möglich ist, leiten Sie auf eine URL mit echtem Fehlerstatus um oder rendern Sie noindex. Bestätigen Sie mit einer frischen direkten Anfrage, nicht mit einer In-App-Navigation.
Routen kollabieren nach dem Rendern zu Duplikaten
Symptom: Suchmaschinen gruppieren unterschiedliche URLs oder behalten nur gemeinsame Navigation. Wahrscheinliche Ursache: Routeninhalte laden zu spät und das Rendering erfasst das Grundgerüst. Fix: Priorisieren Sie primären Inhalt in der Serverantwort oder im frühesten Renderpfad. Bestätigen Sie, dass mehrere Routen eindeutige Inhalte zeigen, bevor optionale Skripte fertig sind.
Testen, was der Server tatsächlich pro Route zurückgibt
Die SPA-Falle besteht darin, dass sich Routen im Browser unterscheiden, aber nicht auf der Leitung. Prüfen Sie mehrere Routen auf der Roh-HTML-Ebene, nicht nur eine.
Roh-HTML pro Route abrufen (macOS / Linux)
# Fetch a few routes with a Googlebot UA and compare — they should NOT be identical
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
for path in / /products /about /product/123; do
echo "=== $path ==="
curl -s -A "$UA" "https://example.com$path" | wc -c # byte counts should differ
doneRoh-HTML zweier Routen vergleichen (gleiche Shell?)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
diff <(curl -s -A "$UA" https://example.com/products) \
<(curl -s -A "$UA" https://example.com/about) \
&& echo "IDENTICAL — bare shell, content is client-only" \
|| echo "Different — routes return distinct HTML (good)"Titel und Canonical pro Route in der rohen Antwort prüfen (grep / regex)
UA="Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
curl -s -A "$UA" https://example.com/products \
| grep -Eio '<title>[^<]*</title>|<link[^>]+rel=["'"'"']canonical["'"'"'][^>]*>'HTTP-Status einer „Nicht gefunden“-Route prüfen (Soft-404-Erkennung)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existIn der Browser-DevTools-Konsole — den gerenderten Titel/die gerenderte kanonische URL prüfen
// Run on each route after client-side navigation to confirm JS set them
console.log('title:', document.title);
console.log('description:',
document.querySelector('meta[name="description"]')?.content);
console.log('canonical:',
document.querySelector('link[rel="canonical"]')?.href);Bookmarklet – Klick-Handler-„Links“ markieren, die keine echten Anker sind
javascript:(()=>{const bad=[...document.querySelectorAll('[onclick],div[role="link"]')]
.filter(e=>!e.closest('a[href]'));
bad.forEach(e=>e.style.outline='3px solid red');
alert(bad.length+' non-anchor clickable(s) outlined — these are invisible to link extraction');})();XPath – Fragment-/Hash-Links finden, die ein Crawler nicht auflösen kann (in die DevTools-Konsole einfügen)
$x('//a[starts-with(@href, "#") or contains(@href, "/#/")]')
.map(a => a.getAttribute('href'));
// Any results are hash-routed links; migrate them to History API paths. Tools zur Prüfung einer SPA
- URL Inspection (Google Search Console) – die gecrawlte vs. gerenderte HTML einer Route ansehen und bestätigen, dass die routenspezifischen Titel, Beschreibungen und kanonischen URLs tatsächlich ankommen.
- Rich Results Test / URL Inspection „View crawled page“ – Googles eigene Darstellung einer einzelnen URL, nützlich zur Bestätigung, dass Inhalte das Rendering überleben.
curlmit einem Googlebot-User-Agent – der schnellste Weg, das rohe HTML mehrerer Routen zu vergleichen und eine gemeinsame Shell zu erkennen.- Screaming Frog SEO Spider (JavaScript-Rendering-Modus) – die Website mit und ohne Rendering crawlen, um rohe vs. gerenderte Inhalte und Titel über alle Routen hinweg zu vergleichen.
- Ahrefs Site Audit – deckt Indexierbarkeitsprobleme, fehlende/doppelte Titel und kanonische URLs sowie Soft-404-ähnliche Muster über Routen hinweg auf.
- DebugBear – SPA-orientiertes Performance-Monitoring; Render-Zeiten sind wichtig, weil langsame Routen in Boilerplate-Duplikate auslaufen können.
- Bing Webmaster Tools – URL Inspection – bingbot rendert JS weniger konsistent als Googlebot, also bestätigen Sie Ihre Routen auch auf Bing-Seite.
Beweisen, dass eine SPA-Route unabhängig indexierbar ist
Test: Parität des direkten Einstiegs in HTML
Durchzuführender Test: Repräsentative Routen in einer neuen Sitzung öffnen und dieselben URLs mit curl abrufen. Erwartetes Ergebnis: Jede URL liefert ihren eigenen primären Inhalt und Head-Signale ohne vorherigen App-Zustand. Fehlerinterpretation: Die Route hängt von Client-Navigation oder gespeichertem Zustand ab. Überwachungsfenster: Sofort. Rollback-Auslöser: Eine Route funktioniert nur nach dem Einstieg über die Startseite.
Test: Behandlung von Fehlerstatus
Durchzuführender Test: Eine bekannte ungültige Route direkt anfordern und sowohl Status als auch gerenderte Direktiven prüfen. Erwartetes Ergebnis: Ein echter Fehlerstatus oder der dokumentierte Fallback eines gerenderten noindex/Redirects auf eine Fehlerantwort. Fehlerinterpretation: Die SPA erzeugt Soft-404s. Überwachungsfenster: Sofort. Rollback-Auslöser: Ungültige Pfade werden als indexierbare 200-Seiten ausgeliefert.
Test: Metadaten-Isolation
Durchzuführender Test: Rohen und gerenderten Titel, Robots-Tag und kanonische URL über mindestens drei Routen hinweg vergleichen. Erwartetes Ergebnis: Jede Route hat einen beabsichtigten, intern konsistenten Satz. Fehlerinterpretation: Die gemeinsame Shell leakt Metadaten zwischen Routen oder JS überschreibt sie zu spät. Überwachungsfenster: Sofort lokal und nach dem Recrawl in URL Inspection. Rollback-Auslöser: Jede Route erbt die kanonische URL einer anderen Route oder eine restriktive Shell-Direktive.
Testen Sie sich selbst: SPA-SEO
Fünf kurze Fragen zur Crawlbarkeit und Indexierbarkeit von Single-Page-Anwendungen. Wählen Sie für jede eine Antwort und prüfen Sie dann.
Ressourcen, die Ihre Zeit wert sind
Meine weiterführenden Artikel
- JavaScript-SEO-Probleme und Best Practices — mein vollständiger JS-SEO-Leitfaden, einschließlich der Abschnitte „Verwenden Sie keine Fragmente in URLs“ und „App-Shell/duplizierte Inhalte“, auf denen dieser Artikel aufbaut.
- Der Anfängerleitfaden für technisches SEO — wo Rendering und Crawling im größeren Zusammenhang stehen.
Meine Vorträge
- So funktioniert die Suche (SlideShare) — meine Erläuterung von Crawling, Rendering, Indexierung und Ranking, also der Pipeline, die eine SPA überstehen muss. (Mein üblicher Hinweis gilt: “This is my understanding of systems… not going to be 100% complete or accurate.” (Übersetzung) „Das ist mein Verständnis von Systemen … es wird nicht zu 100 % vollständig oder genau sein.“)
Aus der Branche
- Google — Grundlagen der JavaScript-SEO verstehen — das App-Shell-Modell und die Empfehlung, die History API statt Fragmenten zu verwenden.
- Google — JavaScript-Probleme in der Suche beheben — der Abschnitt zu Soft-404-Seiten bei SPAs und die Empfehlung zur History API.
- Google — Abschaffung des AJAX-Crawling-Schemas (2015) — das formelle Ende des Hash-Bang-Crawlings.
- Bing — bingbot-Serie: JavaScript, dynamisches Rendering und Cloaking — die Haltung von bingbot zum JS-Rendering.
- Ein Prerendering-Dienst für leere HTML-Seiten bei der SPA-Entwicklung ist kein Cloaking (Search Engine Roundtable, 2015) — Berichterstattung darüber, dass Gary Illyes das Pre-Rendering von SPAs nicht als Cloaking betrachtet. (SERs Paraphrase von Illyes, datiert 2015 — sekundär.)
- SEO für Single-Page-Anwendungen (Nuxt SEO) — eine frameworkseitige Betrachtung derselben Probleme.
- Single-Page-Anwendungen für SEO optimieren (DebugBear) — der Rendering-/Performance-Aspekt der Indexierbarkeit von SPAs.
- SPA (Glossar) (MDN) — eine neutrale Definition des Single-Page-Application-Musters.
Videos
- Google Search Central (YouTube) — Martin Splitts JavaScript-SEO-Serie behandelt Rendering, die History API und die hier besprochenen SPA-Fehlerarten. Kanal
Änderungsprotokoll
Aktualisiert am 21. Aug. 2026.
Redaktionelle Zusammenfassung und aufgezeichnete Änderungsdetails.Änderungsdetails
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
Vollständiger Vergleich nicht verfügbar — für diese Version wurde kein früherer Schnappschuss archiviert.
Aktualisiert am 21. Aug. 2026.
Redaktionelle Zusammenfassung und aufgezeichnete Änderungsdetails.Änderungsdetails
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
Vollständiger Vergleich nicht verfügbar — für diese Version wurde kein früherer Schnappschuss archiviert.
Aktualisiert am 3. Aug. 2026.
Redaktionelle Zusammenfassung und aufgezeichnete Änderungsdetails.Änderungsdetails
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
Vollständiger Vergleich nicht verfügbar — für diese Version wurde kein früherer Schnappschuss archiviert.
Aktualisiert am 18. Juli 2026.
Redaktionelle Zusammenfassung und aufgezeichnete Änderungsdetails.Änderungsdetails
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
-
Detaillierte Änderungsangaben sind derzeit auf Englisch verfügbar.
Vollständiger Vergleich nicht verfügbar — für diese Version wurde kein früherer Schnappschuss archiviert.