Jak Googlebot naprawdę czyta stronę Next.js
Najpierw przyglądnijmy się, jak Google przetwarza stronę opartą na JavaScripcie. Proces przebiega w . W pierwszej pobiera surowy HTML zwrócony przez serwer i od razu wyciąga z niego treść, linki oraz metadane. Wszystko, co znajdzie na tym etapie, może trafić do indeksu błyskawicznie i bez czekania na render JS.
W drugiej fali robot wraca, uruchamia JavaScript i analizuje wyrenderowany DOM. Ten etap może trochę potrwać, nawet do kilku godzin, czasem kilku dni. Wszystko zależy od kolejki renderowania. Dlatego treść obecna w początkowym HTML indeksuje się najszybciej i najpewniej, a treść pojawiająca się dopiero po JS zależy od drugiej fali. W aplikacji renderowanej tylko po stronie klienta () robot w pierwszym podejściu widzi pustą skorupę.
Next.js z założenia rozwiązuje ten problem, ponieważ i dostarczają gotowy HTML z treścią już w pierwszej fali. Najmocniejsza zaleta tego podejścia opiera się jednak na jednym, ważnym założeniu: HTML z serwera i render klienta są ze sobą zgodne. Hydration mismatch jest niebezpieczny, ponieważ łamie to założenie i może poważnie zaszkodzić indeksacji i stabilności w wynikach wyszukiwania.
Co się dzieje, gdy serwer i klient się nie zgadzają
React podczas oczekuje, że drzewo, które zbuduje na kliencie, będzie identyczne z tym, które otrzymał w serwerowym HTML. Gdy napotka różnicę, w zależności od jej skali albo naprawia pojedyncze miejsce, albo — przy poważniejszym rozjeździe — odrzuca serwerowy HTML i renderuje dany fragment od nowa po stronie klienta. I tu zaczyna się problem dla wyszukiwarki.
Wyobraźmy sobie kartę produktu, gdzie serwer renderuje status „niedostępny" na podstawie danych z momentu budowania strony, a klient po hydratacji odpytuje świeże API i podmienia go na „dostępny". Googlebot w pierwszej fali zaindeksuje wersję serwerową. Jeśli render klienta konsekwentnie pokazuje co innego, robot trafia na niespójność między zaindeksowanym HTML a tym, co widzi po renderze — a w skrajnym przypadku może to zostać odczytane jako . Udokumentowano przypadki, w których właśnie taki rozjazd kończył się wypadnięciem strony z indeksu.
Ten sam mechanizm uderza w dane strukturalne, ponieważ jeśli z ceną, dostępnością czy oceną renderuje się inaczej na serwerze, a inaczej po hydratacji, Google może uznać za niezgodne z widoczną treścią i przestać przyznawać . W efekcie tracisz gwiazdki, cenę czy informację o dostępności w wyszukiwarce. Strona technicznie nadal się indeksuje, ale Google przestaje "ufać" danym strukturalnym, co pozbawia Cię kluczowej przewagi wizualnej nad konkurencją. Warto też pamiętać, że hydration mismatch bywa objawem szerszego problemu: jeśli podmiana treści przesuwa layout, odbija się to na — ale to już osobny, wydajnościowy wątek, który rozkładamy na czynniki w artykule o wzorcach niszczących INP w React.
Skąd biorą się rozjazdy w Next.js
Niemal każdy hydration mismatch sprowadza się do kodu, który na serwerze i na kliencie daje inny rezultat. Trzy źródła odpowiadają za większość przypadków.
Pierwsze to odwołania do API przeglądarki, których na serwerze po prostu nie ma. Komponent sięgający po window, document albo localStorage na serwerze dostaje wartość domyślną lub błąd, a na kliencie prawdziwe dane. Efektem jest rozjazd drzew.
Drugie źródło to wartości zmienne w czasie i losowość — Date.now(), new Date(), Math.random() czy generowane identyfikatory. Serwer policzy je w momencie renderu, a klient w momencie hydratacji. Efekt? Niemal nigdy nie wyjdzie to samo.
Trzecie to treść zależna od stanu, który istnieje tylko po stronie klienta. Mogą to być preferencje z ciasteczek odczytywane dopiero w przeglądarce, dane z localStorage albo warunki oparte na szerokości ekranu. Wspólny mianownik wszystkich trzech jest ten sam, czyli pierwszy render klienta musi być identyczny z HTML serwera, a tutaj nie jest.
Trzeba przy tym zaznaczyć jedną rzecz: nie każde „Hydration failed" ma związek z treścią ważną dla SEO. Część rozjazdów bierze się z niepoprawnie zagnieżdżonego HTML — klasyk to <p> wewnątrz <p> albo <div> w <p>, które przeglądarka po cichu naprawia inaczej niż serwer. Akurat ten przypadek dał mi się we znaki przy pierwszym projekcie w Next.js: błąd wracał uparcie, a okazało się, że źródłem był, za każadym razem, źle zagnieżdżony tag - nie żadna logika danych. Inną częstą przyczyną są rozszerzenia przeglądarki, które dorzucają atrybuty do DOM, zanim React zdąży się zhydratyzować.
Te wszystkie przypadki trzeba naprawić, ponieważ psują render, ale nie zmieniają treści, którą indeksuje Google. Z perspektywy SEO groźne są wyłącznie te rozjazdy, które podmieniają widoczną treść — i właśnie na nich skup się w pierwszej kolejności.
Jak to naprawić, nie psując SEO
Najczystsza zasada brzmi: kod zależny od środowiska klienta powinien wykonać się dopiero po hydratacji, a nie w trakcie pierwszego renderu. W praktyce oznacza to przeniesienie takiej logiki do useEffect, który React uruchamia wyłącznie na kliencie, już po dopasowaniu drzewa do serwerowego HTML.
Tutaj serwer i pierwszy render klienta pokazują light, więc hydratacja przebiega bezproblemowo. Dopiero po niej useEffect odczytuje prawdziwą preferencję i aktualizuje widok. Z perspektywy SEO to bezpieczne, bo treść używana do indeksacji — jeśli taka jest w tym miejscu — pozostaje stabilna, a po hydratacji zmienia się jedynie element czysto interfejsowy.
Gdy cały fragment ma się pojawić dopiero na kliencie, ten sam mechanizm wygodnie zamknąć we flagę montowania. Komponent renderuje wtedy na serwerze i w pierwszym przebiegu klienta dokładnie to samo — nic albo neutralny placeholder — a właściwą treść pokazuje dopiero po hydratacji:
A co z danymi spoza Reacta, takimi jak localStorage, matchMedia czy status sieci? W React 18 i nowszych wersjach najlepiej użyć do tego hooka useSyncExternalStore. Dzięki niemu możemy z góry powiedzieć, co ma wyrenderować serwer, więc jego praca jest przewidywalna. W przeglądarce komponent sam nasłuchuje na właściwą wartość, bez potrzeby pisania skomplikowanej logiki w useEffect.
Są też komponenty, które z natury nie mają sensu na serwerze. Może to być na przykład interaktywna mapa, wykres albo cokolwiek, co opiera się o obiekt window. W takim wypadku najprostszym i najczystszym rozwiązaniem jest całkowite wyłączenie dla nich renderowania po stronie serwera (SSR) za pomocą next/dynamic:
Pamiętaj tylko, że taki komponent znika z początkowego HTML, więc trzymaj w nim wyłącznie rzeczy, które nie są ważne dla indeksacji. Mogą to być interaktywne widżety, a nie treść, którą ma zobaczyć Googlebot.
W sytuacjach, w których fragment naprawdę musi różnić się między serwerem a klientem i nie da się tego uniknąć, React daje punktowe rozwiązanie:
suppressHydrationWarning mówi Reactowi, żeby zaakceptował różnicę w tym jednym węźle. To narzędzie do stosowania wyłącznie tam, gdzie rozbieżność jest nieunikniona i dotyczy treści, na której wyszukiwarce nie zależy, jak zegar czy znacznik czasu. Nigdy nie używaj go, żeby „uciszyć" rozjazd na treści, która ma się indeksować.
Dlaczego dynamic rendering to już ślepa uliczka
Kiedyś popularnym obejściem problemów JavaScriptu i SEO było , czyli wykrywanie bota i podawanie mu osobnej, prerenderowanej wersji strony. Google traktuje to dziś jako rozwiązanie tymczasowe, które zostało wycofane z rekomendacji. Powód jest prosty: utrzymywanie dwóch osobnych ścieżek renderowania samo w sobie rodzi ryzyko rozjazdu treści — czyli dokładnie tego problemu, który próbujemy wyeliminować.
Aktualne podejście w Next.js i React jest prostsze oraz spójniejsze. SSR albo SSG z poprawną hydratacją dostarcza tę samą, kompletną treść użytkownikowi i wyszukiwarce, bez rozdzielania ścieżek. W praktyce oznacza to, że treść ważna dla SEO ma znaleźć się w początkowym HTML, a hydratacja nie może jej podmieniać. Przy większym projekcie contentowym sprawdź też, kiedy Next.js naprawdę daje przewagę SEO nad zwykłym Reactem — dobrze ustawiona hydratacja jest częścią tej przewagi, a nie czymś osobnym.
