SEO aplikacji jednostronicowych (SPA)
Jak sprawić, aby aplikacje jednostronicowe (React Router, Vue Router, Angular Router) były crawlable i indeksowalne — problemy app shell i soft 404, History API kontra routing z haszem, HTML dla każdej trasy przez SSR/prerendering, canonicale i tytuły dla tras oraz generowanie map witryn dla tras po stronie klienta.
Języki
Aplikacja jednostronicowa ładuje jeden dokument i przełącza widoki za pomocą JavaScriptu zamiast żądać nowej strony od serwera. Wiele SPA implementuje to przez app shell — serwer zwraca rzeczywisty HTML dla jednego URL-a albo niemal pustą powłokę, dopóki JS nie wyrenderuje każdej trasy — ale jest to częsty wybór implementacyjny, a nie reguła dla każdego SPA; zanim coś założysz, sprawdź bezpośrednie żądanie każdej trasy. Jeśli problemem jest goła konfiguracja app shell, naprawa ma dwie niezależne połowy: adresowalność (History API zamiast fragmentów hash/#!, aby każdy widok miał prawdziwy URL — miękka nawigacja przeglądarki przez History API zmienia URL i interfejs, ale sama nie tworzy nowej odpowiedzi serwera) oraz dostępność treści (SSR, prerendering albo meta-framework, dzięki któremu każdy URL może zwrócić unikalny HTML). Ponadto sprawdzaj tytuł, canonical i stan robots każdej trasy przy bezpośrednim wejściu i po renderowaniu, obsługuj miękkie błędy (routery często zachowują status 200 dla widoków „nie znaleziono” — przekieruj do URL-a, który sam zwraca błąd, albo dodaj wyrenderowany noindex, choć początkowy noindex może spowodować pominięcie renderowania) i upewnij się, że każda trasa w mapie witryny rozwiązuje się do prawdziwego, indeksowalnego HTML-a — nie ma specjalnego formatu mapy SPA.
TL;DR — Aplikacja jednostronicowa (SPA) ładuje jedną stronę z serwera, a następnie używa JavaScriptu do przełączania „stron” bez pełnego przeładowania. Problem polega na tym, że wiele SPA wysyła niemal pustą stronę startową niezależnie od adresu URL, o który prosi wyszukiwarka — to częste ryzyko typowego sposobu budowania SPA, a nie cecha każdej aplikacji. Aby je usunąć, każda trasa musi mieć własny rzeczywisty adres URL i własny rzeczywisty HTML — zwykle przez renderowanie po stronie serwera albo wcześniejsze wygenerowanie stron.
Czym jest SPA
Aplikacja jednostronicowa zwykle aktualizuje widoki i trasy po stronie klienta bez pełnej nawigacji dokumentu. 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 potrafi renderować aplikacje SPA korzystające z JavaScriptu, ale nadal potrzebne są indeksowalne adresy URL, linki, poprawna obsługa statusów i wyrenderowana treść. 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
Aplikacja single-page application to witryna zbudowana jako jedna strona HTML. Gdy przechodzisz z widoku produktów do widoku „o nas”, JavaScript podmienia zawartość ekranu zamiast prosić serwer o całkiem nową stronę. Tak działają domyślnie React z React Routerem, Vue z Vue Routerem i Angular. Daje to wrażenie szybkości i aplikacyjności, dlatego ten model jest popularny.
Ryzyko dotyczy tego, co wysyła serwer. Wiele SPA korzysta z implementacji app shell: przy pierwszym żądaniu — także od Google — serwer zwraca w większości pustą powłokę, a właściwa treść powstaje dopiero w przeglądarce. To jeden z typowych sposobów budowania SPA, nie definicja SPA i nie zachowanie każdej aplikacji. Gdy witryna faktycznie wysyła pustą powłokę, problem jest realny: jeśli Google osobno poprosi serwer o /about i /products, może dostać dokładnie tę samą pustą powłokę. Sprawdź, co zwraca bezpośrednie żądanie do każdej trasy — nie wnioskuj o tym wyłącznie z faktu, że witryna jest SPA.
Dlaczego szkodzi to SEO
Wyszukiwarki muszą widzieć treść, aby ją pozycjonować. W pustej aplikacji SPA zwykle psują się trzy rzeczy:
- Każdy adres URL wygląda tak samo dla serwera. Głębokie linki, udostępnienia i crawlery trafiają do tej samej powłoki.
- Strony 404 nadal mówią „200 OK”. Router JavaScriptu może pokazać ekran „nie znaleziono”, choć strona technicznie zgłasza sukces, więc Google może indeksować puste strony.
- Niewłaściwe adresy URL. Starsze SPA używały adresów takich jak
example.com/#/products. Google nie potrafi ich niezawodnie indeksować.
Jak to naprawić — skrót
- Nadaj każdemu widokowi prawdziwy adres URL za pomocą History API przeglądarki (czyste ścieżki, takie jak
/products), a nie adresów opartych na#. - Wysyłaj prawdziwy HTML dla każdego adresu URL. Renderuj strony na serwerze (SSR) albo wygeneruj je wcześniej (prerendering). Jeśli zaczynasz od zera, framework, który robi to za Ciebie — Next.js, Nuxt, SvelteKit czy Angular SSR — oszczędzi Ci pracy.
- Nadaj każdej stronie własny tytuł i opis, zmieniane wraz ze zmianą trasy.
- Używaj prawdziwych linków (
<a href>), a nie klikalnych elementów<div>.
Chcesz poznać mechanikę — dlaczego routing oparty na haszach zawodzi, na czym polega pułapka „najbardziej restrykcyjnej dyrektywy” dotycząca canonicali i jak generować mapę witryny dla tras po stronie klienta? Przejdź do karty Advanced.
TL;DR — Główne ryzyko SEO SPA nie polega abstrakcyjnie na „JavaScripcie”, lecz na tym, że routing po stronie klienta zmienia to, co widzi użytkownik, bez zmiany tego, co zwróciłby serwer. Naprawa ma dwie niezależne połowy: adresowalność (History API, unikalne adresy URL dla tras, brak fragmentów
#!) oraz dostępność treści (SSR, prerendering/SSG albo meta-framework). Zrób tylko pierwszą połowę, a otrzymasz uporządkowaną mapę adresów URL, z których każdy renderuje tę samą powłokę. Dodatkowo każda trasa potrzebuje własnego canonicala, tytułu i opisu w wyrenderowanym DOM, obsługi miękkich błędów z kodem200, a każdy adres URL w mapie musi niezależnie zwracać prawdziwy HTML.
Czym naprawdę jest SEO SPA
SPA to architektura aplikacji, a nie gwarantowany stan awarii SEO. 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 Renderowanie serwerowe, prerendering albo starannie wdrożone renderowanie po stronie klienta mogą udostępnić treść, ale żadne z nich nie gwarantuje indeksowania. 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
To szczegółowe omówienie aplikacji jednostronicowych — routingu po stronie klienta z React Routerem, Vue Routerem lub Angular Routerem, gdzie po pierwszym załadowaniu serwer nie wysyła już pełnej strony. Artykuł uzupełnia szerszy przewodnik po JavaScript SEO oraz teksty o React, Next.js, Nuxt, Angularze, Vue, Svelte i Astro. Interesuje mnie tu wyłącznie warstwa routingu i jej wpływ na crawlowanie oraz indeksowanie.
Dlaczego puste SPA z routingiem po stronie klienta zawodzi w SEO
Jeden adres URL, jedna odpowiedź HTML — problem app shell
Google dokładnie opisuje ten tryb awarii: “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.” (tłumaczenie) „Niektóre witryny korzystające z JavaScriptu mogą używać modelu app shell, w którym początkowy HTML nie zawiera właściwej treści, więc Google musi wykonać JavaScript, aby ją zobaczyć.”. W pustym SPA app shell jest wszystkim, co kiedykolwiek zwraca serwer. Pobierz świeżo /products, a dostaniesz ten sam niemal pusty dokument co dla /about. Treść rozdziela się dopiero po uruchomieniu JavaScriptu w przeglądarce i podjęciu decyzji przez router.
To cały problem w jednym zdaniu: routing po stronie klienta zmienia to, co widzi użytkownik, bez zmiany tego, co zwróciłby serwer. Wszystko, co dzieje się później — miękkie błędy, duplikaty treści i brakujące tytuły — jest objawem tego problemu.
Serwer nie widzi, o którą „stronę” poproszono — dlaczego routing z haszem zawodzi
Starsze SPA ładowały widoki z fragmentów adresu URL — example.com/#/products. Google mówi dokładnie: “A SPA may use URL fragments (for example https://example.com/#/products) for
loading different views.” (tłumaczenie) „SPA może używać fragmentów URL-a do ładowania różnych widoków.”. Zawodzi to z mechanicznego powodu: przeglądarka nigdy nie wysyła fragmentu, czyli niczego po znaku #, w żądaniu HTTP do serwera. Serwer nie wie, o którą stronę poproszono, więc nie może zwrócić innej treści ani innego kodu statusu. Crawler, który pobierze adres raz, otrzyma identyczny HTML niezależnie od fragmentu.
Dlatego Google formalnie wycofało schemat crawlowania AJAX z 2009 roku w październiku 2015: “In short: We are no longer recommending the AJAX crawling proposal we
made back in 2009.” (tłumaczenie) „Krótko mówiąc, nie zalecamy już schematu crawlowania AJAX zaproponowanego w 2009 roku.”. Dawne obejście _escaped_fragment_ pozwalało serwerom generować trasy z fragmentami na żądanie, ale omijało problem routingu zamiast go naprawiać. Zastępująca je rekomendacja Google to History API, omówione niżej.
Miękkie błędy — routery po stronie klienta utrzymują status 200 dla wszystkiego
W pustym SPA ten problem jest niemal nieunikniony. Routery po stronie klienta z założenia zachowują pierwotny status 200 dla każdej wirtualnej nawigacji — także dla stanów „nie znaleziono”. Google wyraźnie to sygnalizuje: “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.” (tłumaczenie) „W SPA jest to szczególnie trudne. Aby zapobiec indeksowaniu stron błędów, można zastosować jedną z poniższych strategii lub obie.”. Powód jest prosty: “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.” (tłumaczenie) „SPA obsługujące błędy za pomocą JavaScriptu po stronie klienta często zgłasza kod HTTP 200 zamiast odpowiedniego kodu stanu.”.
W efekcie puste lub błędne widoki są indeksowane jako cienkie strony 200. Google dokumentuje tu dokładnie dwie strategie: przekieruj do adresu URL, którego serwer zwraca prawdziwy status 404/błędu, albo dodaj do widoku błędu tag noindex za pomocą JavaScriptu. Jeśli noindex jest obecny już przy pierwszym renderowaniu, Google może całkowicie pominąć renderowanie. Sprawdź bezpośrednią odpowiedź przed uruchomieniem JS, a nie tylko późniejszy wyrenderowany DOM.
Duplikaty wspólnej powłoki — możliwa przyczyna, nie automatyczna diagnoza
Istnieje drugi, bardziej podstępny tryb awarii: różne trasy są indeksowane jako duplikaty, ponieważ po wyrenderowaniu sprowadzają się do tej samej wspólnej warstwy nagłówka, nawigacji i stopki. Gary Illyes opisał jeden ze sposobów: “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.” (tłumaczenie) „Mam w skrzynce wiele wiadomości, w których główna treść ładowała się tak długo, że renderowanie przekroczyło limit czasu — to moje najbardziej prawdopodobne wyjaśnienie — i pozostało wiele stron zawierających tylko szablon. Z samym szablonem strony te są duplikatami.”. Jego rada brzmi: “Try to restructure the js calls such that the content (including marginal boilerplate) loads first.” (tłumaczenie) „Spróbuj zmienić kolejność wywołań JavaScriptu tak, aby treść, w tym poboczne elementy szablonu, ładowała się jako pierwsza.”. (Przekazane w poście na LinkedIn; traktuj jako źródło wtórne o wysokiej wiarygodności.)
Zwróć uwagę, że Illyes określa to jako “my most likely explanation” (tłumaczenie) „moje najbardziej prawdopodobne wyjaśnienie”, a nie potwierdzoną diagnozę. Same objawy nie wystarczą, by rozpoznać “Render timeout” (tłumaczenie) „przekroczenie limitu czasu renderowania”: potrzebujesz dowodów z bezpośredniego żądania, wyrenderowanego wyniku i Search Console, zanim przypiszesz duplikację tras właśnie limitowi czasu, a nie błędowi, zablokowanemu zasobowi lub identycznej powłoce. Nie istnieje też stały, opublikowany limit, pod który warto projektować. Dokumentacja Google mówi, że strona “may stay on this queue for a few seconds, but it can take longer than that,” (tłumaczenie) „może pozostać w tej kolejce przez kilka sekund, ale może to potrwać dłużej”, nie podając konkretnej wartości. Dlatego główna treść powinna ładować się możliwie wcześnie niezależnie od czasu renderowania.
Naprawa: prawdziwy HTML dla każdej trasy
Pełna naprawa ma dwie niezależne części, które ludzie stale mieszają:
- Adresowalność URL — History API, unikalne adresy URL tras, brak fragmentów.
- Dostępność treści — SSR, statyczny prerendering/SSG albo meta-framework.
Zrób tylko punkt 1, a otrzymasz czyste adresy URL, które nadal zwracają tę samą pustą powłokę. Dla trasy, którą chcesz indeksować, potrzebujesz obu elementów.
Nie jest to jednak uniwersalny nakaz dodawania SSR wszędzie. SSR i prerendering zmniejszają zależność od pomyślnego wykonania JavaScriptu przez crawlera. Przed wyborem architektury sprawdź, czy trasa musi się pozycjonować, co zwraca bezpośrednie żądanie bez JS oraz jakie crawlery musisz obsłużyć. CSR, który przechodzi te testy, nie musi stawać się SSR tylko dlatego, że aplikacja jest SPA.
Renderowanie po stronie serwera (SSR)
Serwer uruchamia aplikację dla każdego żądania i zwraca w pełni utworzony HTML dla danej trasy, po czym klient nawodnia go do działającej aplikacji SPA. To najbardziej niezawodna opcja, bo crawler dostaje pełną treść przy pierwszym pobraniu i nie musi uruchamiać JavaScriptu.
Statyczny prerendering / SSG
Zamiast renderować przy każdym żądaniu, budujesz HTML każdej trasy z wyprzedzeniem podczas wdrożenia. To idealne rozwiązanie dla treści, które nie zmieniają się zależnie od użytkownika. Każdy wariant SSR, renderowania statycznego i prerenderingu będzie dobry dla wyszukiwarek — należy unikać pozostawiania treści za renderowaniem wyłącznie po stronie klienta.
Albo: nie twórz tego ręcznie — użyj meta-frameworka
W nowym projekcie uczciwie rekomenduję, aby nie tworzyć ręcznie routingu po stronie klienta wyłącznie z React Routera ani Vue Routera. Meta-framework — Next.js, Nuxt, Angular z pakietem SSR, SvelteKit czy Remix — daje SSR/SSG i metadane zależne od trasy od razu. Dodanie SSR do istniejącego, ręcznie zbudowanego SPA to prawdziwa praca inżynierska — projekt migracyjny, a nie przełącznik konfiguracji.
Dynamiczne renderowanie jako rozwiązanie tymczasowe, nie cel
Crawlerom można serwować osobno wyrenderowany zrzut HTML (dynamic rendering). Akceptują to Google i Bing — Bing “recommend[s] dynamic rendering as a great alternative for websites relying heavily on JavaScript” (tłumaczenie) „zaleca dynamiczne renderowanie jako dobrą alternatywę dla witryn silnie zależnych od JavaScriptu” (przekazane z bloga Binga z 2018 roku; tego dłuższego cytatu nie sprawdzano ponownie) — ale Google jasno mówi, że to obejście: “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.” (tłumaczenie) „Dynamiczne renderowanie było obejściem, a nie długoterminowym rozwiązaniem. Zamiast niego zalecamy renderowanie po stronie serwera, renderowanie statyczne lub hydrację.” (przekazane z dokumentacji Google o dynamicznym renderowaniu). To most, nie architektura.
History API kontra routing z haszem (#!)
Pięć stanów, nie dwa
Testowanie trasy SPA jest mylące, ponieważ pytanie „czy działa?” obejmuje pięć różnych, niezależnie weryfikowalnych stanów:
| Stan | Czym jest |
|---|---|
| Odpowiedź serwera | Bajty, które faktycznie zwraca świeże żądanie HTTP bez JS do adresu URL. |
| Wyrenderowany DOM | To, co buduje przeglądarka albo renderer Googlebota po wykonaniu JavaScriptu. |
| Przetwarzanie wyszukiwarki | Sposób, w jaki Google crawluje odpowiedź serwera, później renderuje stronę i indeksuje ją na podstawie obu wyników. |
| Pełna nawigacja dokumentu | Nowe żądanie HTTP do adresu URL — jedyny stan, który może zmienić odpowiedź serwera lub kod statusu. |
| Miękka nawigacja | Przejście History API pushState/replaceState, które zmienia adres URL, historię przeglądarki i interfejs. |
Miękka nawigacja zmienia adres URL i interfejs, ale sama nie tworzy nowej odpowiedzi HTTP ani statusu — dzieje się to dopiero przy pełnej nawigacji albo równoważnym żądaniu bezpośrednim, takim jak curl. Oba testy są ważne i mogą dawać różne wyniki.
Co daje History API
Rekomendacja Google jest jednoznaczna: “We recommend using the History API to load
different content based on the URL in a SPA.” (tłumaczenie) „Zalecamy używanie History API do ładowania różnych treści na podstawie URL-a w aplikacji SPA.” (W aktualnej dokumentacji „History API” jest linkiem, dlatego ten deep link wskazuje klauzulę wprowadzającą.). History API pushState/replaceState pozwala routerowi zmienić widoczny adres URL na prawdziwą, możliwą do zapisania ścieżkę — /products, a nie /#/products — bez pełnego przeładowania. Naprawia to połowę dotyczącą adresowalności.
Dlaczego fragmenty i hasze są niewidoczne dla serwera — i Google
Ponieważ fragment nigdy nie dociera do serwera, History API jest sposobem nadania każdej trasie adresu URL, który serwer może obsłużyć. Google zaleca: “don’t use fragments to load different page content. The
following example is a bad practice, because Googlebot can’t reliably resolve the
URLs.” (tłumaczenie) „nie używaj fragmentów do ładowania różnych treści strony; poniższy przykład jest złą praktyką, ponieważ Googlebot nie potrafi niezawodnie rozpoznawać tych URL-i.”. Używaj zwykłych adresów, takich jak /products, a nie adresów z haszem /#/products, bo Google nie potrafi ich niezawodnie indeksować.
Krótko o wycofaniu z 2015 roku
Era hash-bang (#!) skończyła się wpisem Google z 2015 roku: “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,” (tłumaczenie) „Obecne realia są inne: jeśli Googlebot nie ma zablokowanego dostępu do plików JavaScript lub CSS, zazwyczaj potrafimy renderować i rozumieć strony internetowe tak jak współczesne przeglądarki.” oraz “you can use the History API pushState() to ensure accessibility for a wider range of
browsers (and our systems).” (tłumaczenie) „możesz użyć History API pushState(), aby zapewnić dostępność dla większej liczby przeglądarek oraz naszych systemów.”. Google nie usunęło natychmiast starych witryn z hash-bang — “we’ll generally crawl, render, and index the #! URLs” (tłumaczenie) „zazwyczaj będziemy crawlować, renderować i indeksować URL-e z #!.” — ale „nadal możemy próbować” nie znaczy „nadal powinieneś tak robić”.
Spraw, aby każda trasa była niezależnie indeksowalna
Czyste adresy URL i prawdziwy HTML zapewniają crawlowanie. Aby uzyskać poprawne indeksowanie, każda trasa potrzebuje własnych sygnałów.
Tagi canonical dla każdej trasy — pułapka najbardziej restrykcyjnej dyrektywy
Każda trasa potrzebuje własnego rel=canonical w wyrenderowanym DOM. Zastępczy canonical albo noindex może zostać wysłany w surowej powłoce HTML, a JavaScript ma go nadpisać później. Google wybiera bardziej restrykcyjny sygnał. Zbędny noindex albo błędny canonical zaszyty w powłoce może po cichu wyłączyć całą trasę.
Tytuły i opisy meta dla każdej trasy ustawiane przez JavaScript
Ustawianie ich w JS jest poprawne — Google mówi wprost: “You can use JavaScript to set
or change the meta description as well as the <title> element.” (tłumaczenie) „Możesz użyć JavaScriptu do ustawienia lub zmiany opisu meta oraz elementu <title>.”. Warunek jest taki, aby trafiły do wyrenderowanego DOM-u ocenianego przez Google. Każda strona powinna mieć własny tytuł i opis aktualizowany przy zmianie trasy.
Prawdziwe linki <a href> między trasami
Linki między trasami twórz jako prawdziwe kotwice z atrybutami href, a nie obsługiwane kliknięciem elementy <div>. Google odkrywa adresy URL, wyciągając wartości href; nawigacja przez <div onClick> jest niewidoczna dla ekstrakcji linków. Nie przenoś też treści między nawigacjami wyłącznie za pomocą stanu klienta — renderer 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.” (tłumaczenie) „nie zachowuje stanu między wczytaniami stron: dane Local Storage i Session Storage oraz pliki cookie HTTP są czyszczone przy kolejnych wczytaniach.”.
Generowanie mapy witryny dla tras SPA
Nie ma specjalnego formatu mapy SPA — to problem ewidencyjny
Protokół map witryn dla SPA się nie zmienia. Trzeba dopilnować, aby każda wymieniona trasa niezależnie zwracała prawdziwy, unikalny, wyrenderowany HTML. Mapa 500 tras po stronie klienta jest bezwartościowa, jeśli wszystkie zwracają tę samą powłokę.
Obsługa tras dynamicznych i parametryzowanych (/product/:id)
W aplikacji z trasami parametryzowanymi nie da się ręcznie utrzymywać listy. Generator mapy musi korzystać z tego samego źródła danych co aplikacja — ze skryptu budowania albo endpointu serwera wyliczającego każde id — aby mapa i aplikacja nigdy się nie rozjechały. Warto wymieniać te adresy URL dopiero wtedy, gdy są renderowane przez SSR albo prerenderowane.
Utrzymanie zgodności mapy z tym, co może zwrócić serwer
Regeneruj mapę podczas budowania albo według harmonogramu powiązanego ze źródłem treści. Trasa, która zwraca błąd albo miękki błąd z 200, lecz pozostaje w mapie, wysyła niepożądany sygnał dotyczący budżetu crawlowania i jakości.
Testowanie tego, co faktycznie widzi Google
Nie sprawdzaj wyłącznie jednego adresu URL. Istota problemu SPA polega na tym, że trasy mogą różnić się w przeglądarce, ale nie w odpowiedzi sieciowej, dlatego testuj wiele tras na poziomie surowego HTML:
- Pobierz surowy HTML każdej trasy poleceniem
curli potwierdź, że treść jest unikalna dla adresu URL, a nie stanowi wspólnej powłoki. - Inspekcja adresu URL w Search Console — porównaj crawlowany i wyrenderowany HTML kilku tras oraz potwierdź obecność ich tytułu, opisu i canonicala.
- Potwierdź właściwy sygnał dla tras błędów — widok „nie znaleziono” powinien przekierowywać do prawdziwego statusu błędu albo zawierać
noindexw wyrenderowanym DOM.
Typowe mity o SEO SPA
- „Google w ogóle nie indeksuje SPA”. Nieaktualne. Google zwykle renderuje treść SPA. Prawdziwe ryzyka to miękkie błędy, routing z haszem, timeouty renderowania i crawlery inne niż Google.
- „Dodanie History API naprawia SEO SPA”. Naprawia wyłącznie adresowalność. Jeśli serwer nadal zwraca tę samą powłokę dla każdej trasy, Google wciąż musi uruchomić JS.
- „Routing z haszem działa jako plan awaryjny”. Wycofany od 2015 roku.
- „Renderowanie po stronie klienta jest karą rankingową”. Nie ma bezpośredniej kary za CSR; szkoda jest pośrednia — nieudane lub opóźnione renderowanie, miękkie 404 i duplikaty.
- „Prerendering dla botów to cloaking”. Nie, jeśli treść zgadza się z tym, co widzą użytkownicy.
- „Potrzebujesz specjalnego generatora map SPA”. Nie istnieje specjalny format; każda wymieniona trasa musi zwracać prawdziwy HTML.
FAQ
Czy Google może indeksować aplikację jednostronicową? Tak, jeśli każda trasa prowadzi do prawdziwego, unikalnego HTML-a przez SSR lub prerendering pod prawdziwym adresem URL. Puste SPA działające wyłącznie po stronie klienta często tego nie zapewniają.
Czy potrzebuję SSR, czy renderowanie po stronie klienta bywa w porządku? CSR może działać dla treści, które nie muszą się pozycjonować, ale wszystko, co chcesz niezawodnie indeksować, prerenderuj albo renderuj przez SSR.
Czy routing z haszem (#!) jest zły dla SEO? Tak — fragment nigdy nie dociera do serwera, więc Google nie potrafi niezawodnie rozwiązać takich adresów. Użyj History API.
Czy każda trasa potrzebuje własnego canonicala? Tak, w wyrenderowanym DOM-ie — dopilnuj, aby w surowej powłoce nie pojawił się przypadkowy noindex albo błędny canonical.
Czy usługa prerenderingu to cloaking? Nie, pod warunkiem że treść serwowana botom zgadza się z tym, co widzą użytkownicy.
Czy powinienem użyć meta-frameworka zamiast samodzielnie budować routing? Przy nowym projekcie zwykle tak — Next.js, Nuxt, Angular SSR i SvelteKit oferują SSR/SSG oraz metadane tras.
Dlaczego moje SPA zwraca 200 dla nieistniejących stron? Router po stronie klienta zachowuje pierwotny status 200 dla wirtualnej nawigacji. Napraw to przekierowaniem JS do prawdziwego statusu błędu albo wyrenderowanym noindex.
Podsumowanie AI
Skrót wersji Advanced:
- SEO SPA to konkretnie routing po stronie klienta. SPA ładuje jeden dokument i podmienia widoki przez JavaScript; niemal pusty app shell dla każdego adresu URL jest typowym wdrożeniem, a nie regułą. Sprawdź bezpośrednią odpowiedź serwera.
- Główne ryzyko: routing po stronie klienta może zmienić to, co widzi użytkownik, bez zmiany odpowiedzi serwera. Bezpośrednie pobranie
/productsi/aboutmoże zwrócić identyczny HTML. - Pięć mylonych stanów: odpowiedź serwera, wyrenderowany DOM, przetwarzanie wyszukiwarki, pełna nawigacja dokumentu i miękka nawigacja. History API zmienia adres URL i interfejs, ale samo nie tworzy nowej odpowiedzi serwera.
- Objawy pustej powłoki: problem app shell, miękkie błędy (router zachowuje
200i może dodaćnoindex; początkowynoindexmoże pominąć renderowanie) oraz duplikaty wspólnej powłoki. Timeout renderowania jest możliwą przyczyną, nie automatyczną diagnozą. - Routing z haszem zawodzi: fragment po
#nie dociera do serwera; Google wycofało go w 2015 roku. History API zapewnia adresowalność, ale nie pełną indeksowalność. - Naprawa ma dwie połowy: adresowalność oraz dostępność treści przez SSR, prerendering/SSG albo meta-framework. SSR nie jest uniwersalnym wymogiem.
- Dla każdej trasy: własny canonical, tytuł i opis w wyrenderowanym DOM-ie, brak surowego
noindexoraz prawdziwe linki<a href>i brak polegania na stanie klienta. - Mapy witryn: każda trasa musi niezależnie zwracać prawdziwy HTML, a trasy dynamiczne potrzebują generatora powiązanego ze źródłem danych.
- Dynamic rendering jest rozwiązaniem tymczasowym, nie architekturą docelową.
- Testuj wiele tras na poziomie surowego HTML.
Oficjalna dokumentacja
Dokumentacja źródłowa wyszukiwarek.
- Podstawy SEO JavaScript — model app shell, „używaj History API zamiast fragmentów” oraz ustawianie tytułów i opisów przez JS.
- Rozwiązywanie problemów wyszukiwania związanych z JavaScript — sekcja o soft 404 w SPA, zakaz używania fragmentów do ładowania różnej treści oraz zalecenie History API.
- Dynamic rendering jako obejście — wyjaśnienie, dlaczego dynamic rendering jest rozwiązaniem przejściowym, a nie długoterminowym.
- Wycofanie schematu crawlowania AJAX (2015) — formalne zakończenie crawlowania hash-bang i zalecenie pushState.
- Tworzenie i przesyłanie mapy witryny — ogólny protokół map witryn (nie istnieje format właściwy dla SPA).
Bing / Microsoft
- Seria bingbot: JavaScript, dynamic rendering i cloaking — możliwości renderowania JS przez bingbot i stanowisko wobec dynamic rendering.
Cytaty ze źródeł
Oficjalne wypowiedzi Google i Binga. Każdy link prowadzi bezpośrednio do cytowanego fragmentu na stronie źródłowej.
Google — problem app shell
- “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.” (tłumaczenie) „Niektóre witryny korzystające z JavaScriptu mogą używać modelu app shell, w którym początkowy HTML nie zawiera właściwej treści, więc Google musi wykonać JavaScript, aby ją zobaczyć.” Przejdź do cytatu
Google — routing z haszami/fragmentami i History API
- “A SPA may use URL fragments (for example https://example.com/#/products) for loading different views.” (tłumaczenie) „SPA może używać fragmentów URL-a do ładowania różnych widoków.” Przejdź do cytatu
- “We recommend using the History API to load different content based on the URL in a SPA.” (tłumaczenie) „Zalecamy używanie History API do ładowania różnych treści na podstawie URL-a w aplikacji SPA.” Przejdź do cytatu (W aktualnej dokumentacji „History API” jest linkiem, dlatego ten deep link wskazuje klauzulę wprowadzającą; całe zdanie występuje w kolejności dosłownej.)
- “don’t use fragments to load different page content. The following example is a bad practice, because Googlebot can’t reliably resolve the URLs.” (tłumaczenie) „Nie używaj fragmentów do ładowania różnych treści strony; poniższy przykład jest złą praktyką, ponieważ Googlebot nie potrafi niezawodnie rozpoznawać tych URL-i.” Przejdź do cytatu
Google — miękkie błędy w SPA
- “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.” (tłumaczenie) „W aplikacji jednostronicowej może to być szczególnie trudne. Aby strony błędów nie zostały zindeksowane, można zastosować jedną lub obie z poniższych strategii.” Przejdź do cytatu
- “When a SPA is using client-side JavaScript to handle errors they often report a
200HTTP status code instead of the appropriate status code.” (tłumaczenie) „Gdy SPA obsługuje błędy za pomocą JavaScriptu po stronie klienta, często zgłasza kod HTTP 200 zamiast właściwego kodu stanu.” Przejdź do cytatu
Google — metatagi przez JavaScript i bezstanowe renderowanie
- “You can use JavaScript to set or change the meta description as well as the
<title>element.” (tłumaczenie) „Możesz użyć JavaScriptu do ustawienia lub zmiany opisu meta oraz elementu <title>.” Przejdź do cytatu - “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.” (tłumaczenie) „WRS nie zachowuje stanu między wczytaniami stron: dane Local Storage i Session Storage oraz pliki cookie HTTP są czyszczone przy kolejnych wczytaniach.” Przejdź do cytatu
Google — wycofanie crawlowania hash-bang (2015)
- “In short: We are no longer recommending the AJAX crawling proposal we made back in 2009.” (tłumaczenie) „Krótko mówiąc, nie zalecamy już schematu crawlowania AJAX zaproponowanego w 2009 roku.” Przejdź do cytatu
- “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.” (tłumaczenie) „Czasy się zmieniły. Jeśli nie blokujesz Googlebotowi dostępu do plików JavaScript lub CSS, zazwyczaj potrafimy renderować i rozumieć strony tak jak nowoczesne przeglądarki.” Przejdź do cytatu
Google — dynamic rendering jako obejście (przekazane; brzmienie odpowiada cytatowi z szerszego przewodnika po SEO JavaScript, ale nie zostało ponownie niezależnie zweryfikowane)
- “Dynamic rendering was a workaround and not a long-term solution for problems with JavaScript-generated content in search engines.” (tłumaczenie) „Dynamiczne renderowanie było obejściem, a nie długoterminowym rozwiązaniem problemów z treścią generowaną przez JavaScript w wyszukiwarkach.” Przejdź do cytatu
Bing — bingbot i JavaScript
- “bingbot is generally able to render JavaScript.” (tłumaczenie) „Bingbot zazwyczaj potrafi renderować JavaScript.” — seria bingbot, Bing Webmaster Blog. Przeczytaj wpis
- “bingbot does not necessarily support all the same JavaScript frameworks that are supported in the latest version of your favorite modern browser.” (tłumaczenie) „Bingbot nie musi obsługiwać wszystkich frameworków JavaScript dostępnych w najnowszej wersji nowoczesnej przeglądarki.” — ten sam wpis. Przeczytaj wpis
Gary Illyes, Google — timeouty renderowania tworzą duplikaty (przekazane przez wpis na LinkedIn, który utrudnia automatyczną weryfikację; traktuj jako wtórne źródło o wysokiej wiarygodności)
- “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.” (tłumaczenie) „Główna treść ładowała się tak długo, że renderowanie przekroczyło limit czasu, a pozostało wiele stron zawierających tylko szablon; z samym szablonem strony te są duplikatami.” Przeczytaj wpis
Którą ścieżkę renderowania wybrać?
Pracuj od góry do dołu. Pytanie zawsze brzmi: „czy crawler otrzymuje prawdziwy HTML dla tej trasy bez wykonywania mojego JS?”. Najpierw potwierdź, że trasa rzeczywiście musi być indeksowana, i sprawdź, co zwraca bezpośrednie żądanie bez JS; nie każda trasa potrzebuje SSR, a niektóre już zwracają użyteczny HTML.
1. Zaczynasz nowy projekt?
- Tak → Użyj meta-frameworka (Next.js, Nuxt, Angular z SSR, SvelteKit, Remix). SSR/SSG i metadane dla tras są wbudowane. Koniec.
- Nie → Przejdź dalej.
2. Czy treść zmienia się zależnie od użytkownika lub żądania?
- Nie (głównie treść statyczna) → Prerender / SSG podczas budowania. Najprostsza solidna naprawa — każda trasa jest prawdziwym HTML-em na dysku.
- Tak → Renderowanie po stronie serwera (SSR), aby każde żądanie zwracało HTML właściwy dla trasy.
3. Nie możesz teraz użyć SSR ani prerenderingu (starsze, ręcznie zbudowane SPA)?
- Użyj dynamic rendering jako tymczasowego mostu — podawaj crawlerom wyrenderowany snapshot. Zaplanuj migrację do SSR/SSG; nie traktuj tego jako celu.
4. Niezależnie od wybranej ścieżki potwierdź dla każdej trasy:
- Prawdziwy URL przez History API (bez fragmentów
#!). - Unikalny tytuł, meta description i canonical w wyrenderowanym DOM-ie.
- W surowej powłoce nie ma bardziej restrykcyjnego sygnału (
noindex, błędny canonical). - Prawdziwe linki
<a href>między trasami. - Trasy błędów zwracają prawdziwy status błędu albo wyrenderowany
noindex(bez miękkich błędów). - Każda trasa z mapy witryny niezależnie rozwiązuje się do prawdziwego HTML-a.
Lista kontrolna SEO SPA
Adresowalność
- Każdy widok ma prawdziwy URL przez History API (
/products), a nie fragment (/#/products). - Linki między trasami są prawdziwymi kotwicami
<a href>, a nie procedurami obsługi<div onClick>.
Dostępność treści
- Każda trasa zwraca prawdziwy, unikalny HTML bez wykonywania JS przez crawlera (SSR, prerendering/SSG albo meta-framework).
- Treść trasy ładuje się, zanim zamknie się okno renderowania (unikaj duplikatów zawierających wyłącznie boilerplate).
Indeksowalność każdej trasy
- Unikalny
<title>i meta description dla każdej trasy, obecne w wyrenderowanym DOM-ie. - Unikalny
rel=canonicaldla każdej trasy w wyrenderowanym DOM-ie. - W surowej powłoce nie ma przypadkowego
noindexani zastępczego canonicala, który mógłby utrwalić bardziej restrykcyjny sygnał.
Błędy
- Trasy „nie znaleziono” przekierowują do adresu, którego serwer zwraca prawdziwy status błędu, albo mają
noindexdodany przez JavaScript (bez miękkich błędów przy200). - Jeśli używasz trasy z
noindex, potwierdź, że jest dodawany po uznaniu trasy za nieprawidłową, a nie obecny już przy pierwszym renderowaniu — początkowynoindexmoże spowodować pominięcie renderowania przez Google.
Mapa witryny
- Każda wymieniona trasa niezależnie rozwiązuje się do prawdziwego HTML-a.
- Trasy dynamiczne (
/product/:id) pochodzą z danych aplikacji, a nie z ręcznie utrzymywanej listy.
Weryfikacja
- Surowy HTML sprawdzony dla wielu tras (curl), z potwierdzeniem unikalnej treści dla każdego URL-a.
- URL Inspection potwierdza wyrenderowaną treść, tytuł, opis i canonical dla każdej trasy.
Modele mentalne
1. Dwóch połówek nie wolno utożsamiać. Adresowalność (History API, unikalne URL-e, brak fragmentów) i dostępność treści (SSR, prerendering albo meta-framework) są niezależne. Czyste URL-e bez HTML-a dla każdej trasy dają schludną mapę witryny pełną pustych powłok. Potrzebujesz obu elementów.
2. „Co serwer zwróciłby świeżo?”
Cały problem SPA polega na rozjeździe widoku przeglądarki z odpowiedzią serwera. Dla każdej trasy zapytaj, co zwróci curl bez wykonywania JS. Jeśli otrzyma powłokę, crawler może zobaczyć również powłokę.
3. Domyślny kod statusu to 200 — także dla błędów.
Routery po stronie klienta zachowują pierwotne 200. Traktuj każdy widok błędu jako soft 404, dopóki celowo nie zwrócisz prawdziwego statusu błędu albo wyrenderowanego noindex.
4. Pułapka najbardziej restrykcyjnej dyrektywy.
Google uzgadnia surową i wyrenderowaną wersję, wybierając bardziej restrykcyjny sygnał. noindex albo błędny canonical w powłoce może nadpisać JS-ową „naprawę”. Audytuj surowy HTML, nie tylko wyrenderowany DOM.
5. Meta-framework najpierw w nowych projektach. Ręczne budowanie routingu po stronie klienta oznacza przejęcie odpowiedzialności za każdy z tych problemów. Wybór frameworka z SSR/SSG i metadanymi dla tras oznacza, że nie musisz ich mieć.
Ściąga SEO SPA
Routing po stronie klienta
| Podejście | Przykład URL | Dociera do serwera? | Bezpieczne dla Google? |
|---|---|---|---|
| History API | /products | Tak | Tak (zalecane) |
| Routing z haszem | /#/products | Nie (fragment nigdy nie jest wysyłany) | Nie — wycofane w 2015 r. |
Hash-bang (#!) | /#!/products | Nie | Nie — starszy schemat AJAX, wycofany |
Strategie renderowania
| Strategia | Czy crawler otrzymuje prawdziwy HTML bez uruchamiania JS? | Najlepsze zastosowanie |
|---|---|---|
| SSR | Tak | Treść zależna od użytkownika lub żądania |
| Prerendering / SSG | Tak | Głównie statyczna treść |
| Meta-framework (Next/Nuxt itd.) | Tak (wbudowane) | Nowe projekty |
| Dynamic rendering | Tak, tylko dla botów | Tymczasowy most w starszych SPA |
| Bare CSR (tylko klient) | Nie | Nic, co chcesz niezawodnie indeksować |
Wymagania każdej trasy (wszystkie w wyrenderowanym DOM-ie)
- Unikalny URL (History API) · unikalny
<title>· unikalny meta description · unikalnyrel=canonical· prawdziwe linki<a href>· trasy błędów z prawdziwym statusem albonoindex.
Szybkie fakty
- Fragment po
#nigdy nie jest wysyłany do serwera — dlatego routing z haszem zawodzi. - Routery po stronie klienta zachowują
200dla wszystkiego, także dla „nie znaleziono” → miękki błąd. - Google uzgadnia surową i wyrenderowaną wersję, wybierając sygnał bardziej restrykcyjny.
- Nie istnieje specjalny format mapy witryny SPA — każda wymieniona trasa musi rozwiązywać się do prawdziwego HTML-a.
Antywzorce SEO SPA
Wysyłanie bare CSR SPA i przesyłanie pełnej mapy witryny. Mapa witryny zawiera 500 tras, a wszystkie 500 zwracają tę samą powłokę przy świeżym pobraniu. Mapa witryny nie sprawia, że treść istnieje — robi to renderowanie.
Dodanie History API i uznanie sprawy za zakończoną. Czyste URL-e naprawiają adresowalność, nie treść. Bez SSR lub prerenderingu serwer nadal zwraca powłokę dla każdej trasy.
Routing z haszem/hash-bang „jako rozwiązanie awaryjne”. Fragment nigdy nie dociera do serwera, więc serwer nie może różnicować tras. Wycofane od 2015 roku; to nie plan awaryjny, lecz ślepy zaułek.
Nawigowanie przez <div onClick> zamiast <a href>.
Google wyodrębnia href, aby odkrywać URL-e. Nawigacja obsługiwana kliknięciem jest niewidoczna dla ekstrakcji linków, więc te trasy mogą nigdy nie zostać znalezione.
Zastępczy noindex lub canonical w surowej powłoce „który JS nadpisze”.
Google wybiera bardziej restrykcyjny sygnał spośród surowej i wyrenderowanej wersji. Dyrektywa z powłoki może wygrać i po cichu wyciszyć trasę.
Zwracanie 200 dla widoków „nie znaleziono”.
Miękkie błędy mogą doprowadzić do indeksowania cienkich lub pustych stron. Przekieruj do prawdziwego statusu błędu albo dodaj wyrenderowany noindex.
Powolne ładowanie treści trasy, po boilerplate. Timeouty renderowania pozostawiają tylko wspólną powłokę, a każda trasa staje się duplikatem każdej innej (Illyes). Najpierw ładuj główną treść.
Poleganie na stanie klienta, który ma przenosić treść między nawigacjami. Renderer czyści Local/Session Storage i cookies przy kolejnych ładowaniach stron — treść istniejąca wyłącznie w stanie klienta nie będzie dostępna, gdy Google wyrenderuje następną trasę.
Typowe problemy z indeksowaniem SPA
Każda trasa zwraca ten sam HTML
Objaw: /products i /about mają różne widoki w przeglądarce, ale identyczne surowe odpowiedzi. Prawdopodobna przyczyna: routing History API zapewnia adresowalność bez SSR lub prerenderingu. Naprawa: generuj HTML właściwy dla trasy i potwierdź, że bezpośrednie żądanie każdej ścieżki zawiera jej własny nagłówek, tekst główny i metadane.
Brakujące trasy wyglądają jak strony zakończone sukcesem
Objaw: nieistniejąca trasa pokazuje widok „nie znaleziono”, zwracając 200. Prawdopodobna przyczyna: router klienta przejmuje błąd po tym, jak serwer wysłał poprawną powłokę. Naprawa: zwracaj właściwy status serwera; jeśli nie da się tego jeszcze wdrożyć, przekieruj do URL-a z prawdziwym statusem błędu albo wyrenderuj noindex. Potwierdź to świeżym bezpośrednim żądaniem, nie nawigacją wewnątrz aplikacji.
Trasy po renderowaniu zapadają się w duplikaty
Objaw: wyszukiwarki grupują odrębne URL-e albo zachowują tylko wspólną nawigację. Prawdopodobna przyczyna: treść trasy ładuje się zbyt późno i renderowanie przechwytuje boilerplate. Naprawa: nadaj głównej treści priorytet w odpowiedzi serwera albo najwcześniejszej ścieżce renderowania. Potwierdź, że kilka tras pokazuje unikalną treść, zanim zakończą się opcjonalne skrypty.
Testowanie tego, co serwer faktycznie zwraca dla każdej trasy
Pułapka SPA polega na tym, że trasy różnią się w przeglądarce, ale nie w warstwie sieciowej. Sprawdź wiele tras na poziomie surowego HTML, nie jedną.
Pobieranie surowego HTML dla każdej trasy (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
donePorównanie surowego HTML dwóch tras (czy to ta sama powłoka?)
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)"Sprawdzenie tytułu i canonicala dla trasy w surowej odpowiedzi (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["'"'"'][^>]*>'Sprawdzenie statusu HTTP trasy „nie znaleziono” (detektor soft 404)
# A missing route should NOT return 200
curl -s -o /dev/null -w "%{http_code}\n" https://example.com/this-route-does-not-existW konsoli DevTools w przeglądarce — sprawdzenie tytułu/canonicala w renderze
// 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 — oznaczanie „linków” z obsługą kliknięcia, które nie są prawdziwymi kotwicami
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 — znajdowanie linków fragment/hash, których crawler nie może rozwiązać (wklej do konsoli DevTools)
$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. Narzędzia do audytu SPA
- URL Inspection (Google Search Console) — pokaż crawlowany i wyrenderowany HTML dla trasy oraz potwierdź, że tytuł, opis i canonical właściwe dla trasy rzeczywiście się pojawiają.
- Rich Results Test / URL Inspection „View crawled page” — własny render Google pojedynczego URL-a, przydatny do potwierdzenia, że treść przetrwa renderowanie.
curlz user-agentem Googlebota — najszybszy sposób porównania surowego HTML wielu tras i wykrycia wspólnej powłoki.- Screaming Frog SEO Spider (tryb renderowania JavaScript) — crawluj witrynę z renderowaniem i bez niego, aby porównać surową i wyrenderowaną treść oraz tytuły na każdej trasie.
- Audyt witryny Ahrefs — pokazuje problemy z indeksowalnością, brakujące lub zduplikowane tytuły i canonicale oraz wzorce przypominające soft 404.
- DebugBear — monitoring wydajności zorientowany na SPA; czas renderowania ma znaczenie, bo wolne trasy mogą kończyć się timeoutem i duplikatami boilerplate.
- Bing Webmaster Tools — URL Inspection — bingbot renderuje JS mniej konsekwentnie niż Googlebot, więc potwierdź trasy także po stronie Binga.
Dowód niezależnej indeksowalności trasy SPA
Test parzystości HTML przy bezpośrednim wejściu
Test do wykonania: Otwórz reprezentatywne trasy w nowej sesji i pobierz te same URL-e przez curl. Oczekiwany wynik: Każdy URL zwraca własną główną treść i sygnały w sekcji head bez wcześniejszego stanu aplikacji. Interpretacja niepowodzenia: Trasa zależy od nawigacji klienta albo zapisanego stanu. Okno monitorowania: Natychmiast. Wyzwalacz wycofania: Trasa działa tylko po wejściu przez stronę główną.
Test obsługi statusu błędu
Test do wykonania: Zażądaj bezpośrednio znanej nieprawidłowej trasy i sprawdź zarówno status, jak i wyrenderowane dyrektywy. Oczekiwany wynik: Prawdziwy status błędu albo udokumentowane obejście: wyrenderowany noindex lub przekierowanie do odpowiedzi błędu. Interpretacja niepowodzenia: SPA generuje miękki błąd. Okno monitorowania: Natychmiast. Wyzwalacz wycofania: Nieprawidłowe ścieżki są publikowane jako indeksowalne strony 200.
Test izolacji metadanych
Test do wykonania: Porównaj surowy i wyrenderowany tytuł, tag robots oraz canonical dla co najmniej trzech tras. Oczekiwany wynik: Każda trasa ma jeden zamierzony, wewnętrznie spójny zestaw. Interpretacja niepowodzenia: Wspólna powłoka ujawnia metadane innej trasy albo JS nadpisuje je zbyt późno. Okno monitorowania: Natychmiast lokalnie i ponownie po crawlowaniu w URL Inspection. Wyzwalacz wycofania: Dowolna trasa dziedziczy canonical innej trasy albo restrykcyjną dyrektywę powłoki.
Sprawdź się: SEO SPA
Pięć krótkich pytań o to, jak uczynić aplikacje jednostronicowe crawlable i indeksowalne. Wybierz odpowiedź przy każdym pytaniu, a następnie sprawdź wynik.
Zasoby warte uwagi
Moje powiązane teksty
- Problemy SEO JavaScript i najlepsze praktyki — pełny przewodnik po JS-SEO, w tym sekcje o zakazie fragmentów w URL-ach oraz o app shell i duplikatach treści, na których opiera się ten artykuł.
- Przewodnik dla początkujących po technicznym SEO — miejsce renderowania i crawlowania w szerszym obrazie.
Moje wystąpienia
- Jak działa wyszukiwarka (SlideShare) — mój przewodnik po crawlowaniu, renderowaniu, indeksowaniu i rankingowaniu, czyli potoku, który SPA musi przejść. (Obowiązuje moje standardowe zastrzeżenie: „To moje rozumienie systemów… nie będzie w 100% kompletne ani dokładne”).
Z branży
- Google — Podstawy SEO JavaScript — model app shell i „używaj History API zamiast fragmentów”.
- Google — Rozwiązywanie problemów wyszukiwania związanych z JavaScript — sekcja o soft 404 w SPA i zalecenie History API.
- Google — Wycofanie schematu crawlowania AJAX (2015) — formalne zakończenie crawlowania hash-bang.
- Bing — Seria bingbot: JavaScript, dynamic rendering i cloaking — stanowisko Binga wobec renderowania JS.
- Użycie usługi pre-render dla pustych stron HTML SPA nie jest cloakingiem (Search Engine Roundtable, 2015) — omówienie wypowiedzi Gary’ego Illyesa o tym, że prerendering SPA nie jest cloakingiem. (Parafraza SER wypowiedzi Illyesa z 2015 roku — źródło wtórne.)
- SEO dla aplikacji jednostronicowych (Nuxt SEO) — przewodnik po tych samych problemach z perspektywy frameworka.
- Jak optymalizować aplikacje jednostronicowe pod SEO (DebugBear) — perspektywa renderowania i wydajności na indeksowalność SPA.
- SPA (glosariusz) (MDN) — neutralna definicja wzorca aplikacji jednostronicowej.
Filmy
- Google Search Central (YouTube) — seria Martina Splitta o SEO JavaScript obejmuje renderowanie, History API i tryby awarii SPA omawiane w tym artykule. Kanał
Dziennik zmian
Zaktualizowano 20 sie 2026.
Podsumowanie redakcyjne i zapisane szczegóły zmian.Szczegóły zmian
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
Pełne porównanie jest niedostępne — dla tej wersji nie zarchiwizowano wcześniejszej migawki.
Zaktualizowano 20 sie 2026.
Podsumowanie redakcyjne i zapisane szczegóły zmian.Szczegóły zmian
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
Pełne porównanie jest niedostępne — dla tej wersji nie zarchiwizowano wcześniejszej migawki.
Zaktualizowano 20 sie 2026.
Podsumowanie redakcyjne i zapisane szczegóły zmian.Szczegóły zmian
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
Pełne porównanie jest niedostępne — dla tej wersji nie zarchiwizowano wcześniejszej migawki.
Zaktualizowano 3 sie 2026.
Podsumowanie redakcyjne i zapisane szczegóły zmian.Szczegóły zmian
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
Pełne porównanie jest niedostępne — dla tej wersji nie zarchiwizowano wcześniejszej migawki.
Zaktualizowano 18 lip 2026.
Podsumowanie redakcyjne i zapisane szczegóły zmian.Szczegóły zmian
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
-
Szczegółowe uwagi dotyczące zmian są obecnie dostępne po angielsku.
Pełne porównanie jest niedostępne — dla tej wersji nie zarchiwizowano wcześniejszej migawki.