Aplikacja jednostronicowa (SPA, single page application) to strona, która pobiera z serwera jeden dokument HTML i pakiet skryptów, a kolejne podstrony buduje w przeglądarce bez przeładowania. Tak działa większość witryn na React, Vue i Angularze. Użytkownik dostaje płynną nawigację, ale Googlebot pobiera prawie pusty HTML i treść widzi dopiero po wykonaniu skryptu. Pozycjonowanie SPA sprowadza się do czterech rzeczy: każdy widok ma osobny adres, prawdziwy kod odpowiedzi, własny tytuł i treść w HTML.
Czym aplikacja jednostronicowa różni się od zwykłej strony?
Zwykła strona wielostronicowa, na przykład na WordPressie, oddaje z serwera osobny dokument HTML na każdą podstronę, a aplikacja jednostronicowa tylko jeden dokument i skrypt, który rysuje wszystkie podstrony już w przeglądarce. W źródle takiej strony zamiast treści jest zwykle pusty element, na przykład <div id="root"></div>, i odwołanie do pliku JavaScript. Dopiero skrypt pobiera dane z serwera i wstawia do tego elementu nagłówki, opisy i listę produktów.
Ten sposób budowania stron nazywa się renderowaniem po stronie klienta (CSR). Po kliknięciu w menu przeglądarka nie pobiera nowej strony, tylko podmienia widok i zmienia adres w pasku. Strona zbudowana na WordPressie z klasycznym motywem albo na generatorze stron statycznych nie jest SPA, nawet jeśli korzysta z wielu skryptów JavaScript do animacji czy formularzy.
| Cecha | Aplikacja jednostronicowa (SPA) | Strona wielostronicowa (MPA) |
|---|---|---|
| Przejście między podstronami | skrypt podmienia widok bez przeładowania | przeglądarka pobiera nowy dokument HTML |
| Co jest w HTML zaraz po pobraniu | szkielet i pusty kontener, treść dopisuje skrypt | pełna treść podstrony |
| Adresy podstron | istnieją tylko, jeśli aplikacja je obsłuży (History API) | osobny adres na każdą podstronę z natury |
| Odpowiedź serwera na nieistniejący adres | zwykle kod 200 i ten sam index.html | kod 404 |
| Typowe zastosowanie | panele, konfiguratory, aplikacje po zalogowaniu | strony firmowe, blogi, sklepy |
Dlaczego aplikacja jednostronicowa jest trudniejsza do pozycjonowania?
Aplikacja jednostronicowa jest trudniejsza do pozycjonowania, bo treść, którą Google ma zaindeksować, powstaje dopiero po wykonaniu skryptu, a serwer nie wie, które adresy w niej istnieją. Z tego wynikają cztery typowe problemy:
- Pusty HTML. Googlebot najpierw pobiera surowy HTML, a skrypty wykonuje w osobnej kolejce renderowania. Wszystko, co dopisuje skrypt, wchodzi do indeksu dopiero po tym kroku, co opisaliśmy we wpisie Czy Google widzi treść ładowaną przez JavaScript?.
- Brak osobnych adresów. Aplikacja, która zmienia widoki pod jednym adresem albo rozróżnia je tylko fragmentem po znaku
#(/#/oferta), z punktu widzenia Google ma jedną stronę. Google pisze wprost, że fragmentów po#nie odczytuje wiarygodnie, a schemat indeksowania takich adresów (AJAX crawling) wycofał w 2015 roku. - Kod 200 na wszystko. Serwer SPA oddaje ten sam
index.htmlna każdy adres, więc komunikat „nie znaleziono” wyświetla skrypt, a odpowiedź i tak ma kod 200. Google klasyfikuje taką stronę jako soft 404 i może indeksować adresy, które nie istnieją. - Jeden tytuł dla wszystkich widoków. Jeśli skrypt nie podmienia tytułu i meta description przy przejściu do kolejnego widoku, każda podstrona ma w Google ten sam nagłówek.
Jest jeszcze piąty problem – poza Google, ale równie ważny dla widoczności firmy. Jak pokazało badanie Vercela z grudnia 2024 roku, roboty ChatGPT, Claude i Perplexity pobierają pliki JavaScript, ale ich nie wykonują, więc SPA bez treści w HTML jest dla nich pustą stroną. Gemini korzysta z infrastruktury Googlebota i renderuje strony w pełni.
Masz stronę albo sklep na React, Vue lub Angularze i nie wiesz, co z niej widzi Google?
Napisz do nas, sprawdzimy to i podpowiemy, co zrobić.
Jak ustawić adresy w aplikacji jednostronicowej, żeby Google je widział?
Każdy widok w aplikacji jednostronicowej powinien mieć własny adres w ścieżce, na przykład /oferta/sklep-internetowy/, obsługiwany przez History API, a nie adres z fragmentem po znaku #. History API to mechanizm przeglądarki, w którym funkcja pushState() zmienia adres w pasku i wpis w historii bez przeładowania strony, a przycisk „wstecz” wraca do poprzedniego widoku. Google zaleca ten sposób routingu w dokumentacji Search Central.
Sam adres nie wystarczy – trzeba zadbać jeszcze o trzy sprawy:
- Linki jako zwykłe odnośniki. Menu i linki w treści mają być elementami
<a href="/oferta/sklep-internetowy/">, a nie przyciskami ze zdarzeniem kliknięcia. Google zbiera adresy z atrybutuhref; link, który działa tylko po wykonaniu skryptu, nie prowadzi robota dalej. - Serwer odpowiada na każdy adres widoku. Gdy użytkownik odświeży
/oferta/sklep-internetowy/albo wejdzie na ten adres z Google, serwer musi oddać aplikację, a nie własny błąd 404. Standardowe rozwiązanie z dokumentacji Vue Router to reguła przekierowująca wszystkie ścieżki naindex.html: w nginxtry_files $uri $uri/ /index.html;, w ApacheRewriteRule . index.html [L]. Ta reguła ma skutek uboczny, który opisujemy w następnej sekcji. - Adresy widoków w mapie witryny i z canonicalem. Wszystkie widoki, które mają być w Google, wpisz do mapy witryny XML, a w każdym widoku ustaw tag canonical na jego własny adres, żeby parametry i warianty nie tworzyły duplikatów.
Jak obsłużyć błąd 404 w aplikacji jednostronicowej?
Strona „nie znaleziono” w aplikacji jednostronicowej musi zwracać prawdziwy kod 404 albo mieć tag noindex, bo reguła przekierowująca wszystko na index.html sprawia, że serwer odpowiada kodem 200 na każdy, także nieistniejący, adres. Dokumentacja Vue Router mówi to wprost: po włączeniu takiej reguły serwer przestaje zgłaszać błędy 404, a obsługę brakujących adresów trzeba dopisać w aplikacji.
Skutek braku tej obsługi widać w Search Console: literówki w adresach, stare linki i usunięte produkty trafiają do raportu jako soft 404, a część z nich Google indeksuje jako pełnoprawne strony. Google w dokumentacji o JavaScripcie podaje dwie drogi:
- Przekierowanie skryptem na adres, dla którego serwer zwraca prawdziwy kod 404, na przykład
/404/wyłączony z reguły przekierowania. - Tag noindex dodany skryptem do widoku „nie znaleziono”:
<meta name="robots" content="noindex">. O tym, jak ten tag działa, piszemy we wpisie Co to jest tag noindex?
Gdy aplikacja renderuje widoki na serwerze, problem znika u źródła: serwer dopasowuje adres do listy widoków, a gdy takiego nie znajdzie, odpowiada kodem 404, zanim jakikolwiek skrypt się uruchomi. Kod odpowiedzi sprawdzisz w przeglądarce: narzędzia deweloperskie, karta Sieć, pierwszy wiersz po odświeżeniu strony.
Jak ustawić tytuł i meta description osobno dla każdego widoku?
Skrypt ma zmieniać tytuł dokumentu i meta description przy każdej zmianie widoku – Google czyta jedno i drugie także wtedy, gdy ustawia je JavaScript, co potwierdza w dokumentacji Search Central. W praktyce każdy framework ma do tego gotowe narzędzie: React od wersji 19 pozwala wstawić <title> i <meta> bezpośrednio w komponencie widoku. W Vue odpowiada za to biblioteka Unhead z funkcją useHead, a w Angularze usługi Title i Meta. Tą samą drogą ustawia się tag canonical i dane strukturalne JSON-LD.
Są przy tym dwie pułapki. Po pierwsze, jeśli w surowym HTML jest tag noindex, Google może pominąć renderowanie, więc skrypt, który ma ten tag zdjąć, nigdy się nie wykona; noindex na stałe w szablonie aplikacji wyłącza z indeksu wszystkie widoki. Po drugie, roboty portali społecznościowych i komunikatorów czytają surowy HTML bez skryptów, więc tagi Open Graph ustawione tylko przez JavaScript nie działają przy udostępnianiu linku.
Czy Googlebot wyrenderuje aplikację jednostronicową?
Tak, Googlebot wykonuje JavaScript w przeglądarce headless Chromium i indeksuje wyrenderowany HTML, ale tylko wtedy, gdy spełnione są warunki opisane w jego dokumentacji:
- adres zwraca kod 200, bo tylko takie strony trafiają do kolejki renderowania;
- pliki JavaScript, CSS i dane pobierane przez skrypt nie są zablokowane w robots.txt;
- treść pojawia się bez kliknięcia i przewijania, bo Googlebot nie wykonuje działań użytkownika; dotyczy to także elementów doładowywanych leniwie i list z nieskończonym przewijaniem;
- aplikacja nie zależy od danych w localStorage, sessionStorage ani ciasteczkach, bo Googlebot czyści je przy każdym załadowaniu strony;
- aplikacja nie pobiera treści przez WebSockets ani WebRTC i nie czeka na zgodę użytkownika, na przykład na lokalizację, bo Googlebot odrzuca takie prośby.
Nawet gdy wszystkie warunki są spełnione, między pobraniem HTML a wykonaniem skryptu strona czeka w kolejce renderowania. To, co Google faktycznie zobaczył, sprawdzisz w Search Console w narzędziu Sprawdzanie adresu URL: przycisk „Wyświetl przetestowaną stronę” pokazuje wyrenderowany HTML i błędy konsoli. Kroki tego testu opisaliśmy we wpisie o treści ładowanej przez JavaScript.
Jak dać Google treść w HTML od razu, bez czekania na renderowanie?
Treść trafia do HTML od razu, gdy widoki są renderowane po stronie serwera (SSR), generowane jako statyczne pliki przy budowaniu (SSG) albo wstępnie wyrenderowane dla wybranych adresów (prerendering). W każdym z tych trybów Googlebot i inne roboty dostają pełną stronę już w pierwszej fazie, a skrypt dopiero potem przejmuje nawigację. Google pisze w dokumentacji, że renderowanie po stronie serwera lub prerendering „nadal jest świetnym pomysłem”, bo przyspiesza stronę i ułatwia pracę robotom.
Frameworki mają to wbudowane: Next.js dla Reacta, Nuxt dla Vue, Angular z pakietem @angular/ssr (komenda ng add @angular/ssr), a Astro generuje statyczny HTML domyślnie. Dynamic rendering, czyli podawanie robotom osobnej, wyrenderowanej kopii strony, Google nazywa dziś obejściem, a nie rozwiązaniem, i zaleca zamiast niego SSR, renderowanie statyczne lub hydratację.
Reguła praktyczna z naszych wdrożeń: widoki publiczne (oferta, produkty, blog, kontakt) mają mieć treść w HTML, a panel dostępny po zalogowaniu może zostać czystą aplikacją jednostronicową, bo Google i tak się do niego nie zaloguje. Przy wyborze technologii dla nowej strony firmowej zapytaj wykonawcę wprost, w którym trybie będą renderowane strony publiczne; przy stronach internetowych, które budujemy, to jeden z pierwszych punktów briefu.
Jak sprawdzić aplikację jednostronicową przed startem?
Pięć testów poniżej zrobisz w przeglądarce bez programisty: każdy trwa minutę, a razem odpowiadają na pytanie, czy Google zobaczy stronę tak jak Ty.
Pokaż źródło strony
Kliknij prawym przyciskiem na stronie i wybierz „Pokaż źródło”. Jeśli widzisz nagłówki i opisy, treść jest w HTML; jeśli tylko pusty kontener i pliki .js, treść buduje skrypt.
Wyłącz JavaScript i odśwież
Zrobisz to w narzędziach deweloperskich Chrome (klawisz F12, menu „Więcej narzędzi”, „Ustawienia”, „Wyłącz JavaScript”). Na ekranie zostaje dokładnie tyle, ile dostają roboty, które nie wykonują skryptów.
Odśwież stronę na podstronie
Wejdź w dowolny widok z menu i naciśnij F5. Jeśli pojawi się błąd serwera zamiast strony, brakuje reguły oddającej aplikację na każdy adres.
Wpisz nieistniejący adres
Dopisz do adresu strony końcówkę /taki-adres-nie-istnieje/ i sprawdź kod odpowiedzi w karcie Sieć. Kod 200 przy komunikacie „nie znaleziono” to soft 404 do naprawy.
Sprawdź adres w Search Console
W narzędziu Sprawdzanie adresu URL kliknij „Sprawdź URL wersji opublikowanej”, potem „Wyświetl przetestowaną stronę” i wyszukaj w karcie HTML zdanie z treści widoku. Po kilku tygodniach od startu przejrzyj też raport „Strony”: soft 404 i „Strona zeskanowana, ale jeszcze niezindeksowana” to najczęstsze sygnały problemów z SPA.
Te pięć punktów to stały element audytu technicznego w pozycjonowaniu stron zbudowanych na frameworkach JavaScript.
Jakie pojęcia wiążą się z pozycjonowaniem aplikacji jednostronicowej?
- Renderowanie po stronie klienta (CSR): sposób budowania strony, w którym HTML z treścią powstaje w przeglądarce ze skryptu; podstawa każdej SPA.
- Renderowanie po stronie serwera (SSR) i generowanie statyczne (SSG): sposoby, w których HTML z treścią powstaje na serwerze przy każdym żądaniu albo raz przy budowaniu strony.
- History API: mechanizm przeglądarki do zmiany adresu i historii bez przeładowania strony; Google zaleca go do routingu w SPA.
- Soft 404: strona, która odpowiada kodem 200, ale nie ma treści albo pokazuje komunikat o błędzie; typowy skutek reguły przekierowującej wszystko na
index.html. - Lazy loading: doładowywanie obrazów i treści dopiero wtedy, gdy są potrzebne; w SPA treść ma się pojawiać bez działania użytkownika, inaczej Google jej nie zobaczy.
- Crawlowanie a indeksowanie: pobranie strony przez robota to jeszcze nie indeksacja; przy SPA między jednym a drugim jest jeszcze renderowanie, o czym piszemy we wpisie Crawlowanie a indeksowanie: czym się różnią?
- Crawl budget: liczba adresów i plików, które Googlebot pobiera z witryny; skrypty i dane SPA też się w nim mieszczą, co opisaliśmy we wpisie Co to jest crawl budget?
Planujesz stronę lub sklep na React, Vue albo Angularze?
Napisz do nas, sprawdzimy założenia wykonawcy pod kątem Google, zanim ruszy budowa.
FAQ
Nie. O tym, czy strona jest SPA, decyduje sposób renderowania, a nie biblioteka. Next.js, Nuxt i Angular z pakietem SSR generują HTML z treścią na serwerze i dopiero potem uruchamiają skrypt w przeglądarce, więc dla Google wyglądają jak zwykłe strony.
Nie. Schemat AJAX crawling, w którym Google zamieniał adresy z #! na wersję do pobrania, został wycofany w 2015 roku. Dziś Google zaleca routing przez History API i osobne adresy w ścieżce dla każdego widoku.
Google opisuje dynamic rendering, czyli podawanie robotom wyrenderowanej kopii strony, jako obejście, a nie rozwiązanie długoterminowe, bo wymaga osobnej infrastruktury i drugiego wariantu strony do utrzymania. Zamiast tego Google zaleca renderowanie po stronie serwera, renderowanie statyczne i hydratację.
Nie. Googlebot nie loguje się do aplikacji, więc widoki za logowaniem i tak nie trafią do indeksu. Wymagania z tego wpisu dotyczą części publicznej: strony głównej, oferty, produktów, bloga i kontaktu.
Tak, w wariancie headless: WordPress zostaje panelem do treści, a stronę dla użytkownika buduje osobna aplikacja na React lub Next.js. Wtedy obowiązują wszystkie zasady z tego wpisu. Klasyczny WordPress z motywem oddaje gotowy HTML na każdą podstronę i nie jest SPA.
Źródła
- Google Search Central, „Understand JavaScript SEO basics”: developers.google.com
- Google Search Central, „Fix search-related JavaScript problems”: developers.google.com
- Google Search Central, „Dynamic rendering as a workaround”: developers.google.com
- Google Search Central, „Lazy-loading content”: developers.google.com
- MDN Web Docs, „History API”: developer.mozilla.org
- Vue Router, „Different History Modes”: router.vuejs.org
- Angular, „Server-side rendering”: angular.dev
- Vercel, „The rise of the AI crawler”: vercel.com
