Jak Google indeksuje strony wielojęzyczne — sitemap XML i hreflang od podszewki
Wielojęzyczne SEO to jeden z najbardziej niedocenianych aspektów technicznej optymalizacji strony. Wiele serwisów po prostu serwuje treść w różnych językach pod tym samym URL-em (przez Accept-Language), nie zdając sobie sprawy, że Googlebot wysyła nagłówek Accept-Language: en — i nigdy nie zobaczy wersji strony w języku alternatywnym.
Rozwiązaniem jest sitemap XML z tagami xhtml:link rel="alternate" hreflang="pl/en" — mechanizm, który mówi Google: "ten sam artykuł istnieje pod dwoma różnymi URL-ami, w dwóch językach. Oto oba. Nie traktuj ich jako duplicate content — to są warianty językowe." Każdy URL w sitemapie zawiera odnośniki do wszystkich swoich wariantów językowych, tworząc siatkę wzajemnych powiązań.
Dzięki temu hreflang nie wymusza przedrostków w URL — sitemap XML dostarcza Google te same informacje bez zmiany struktury adresów. Dla pełnej poprawności warto jednak dorzucić też <link rel="alternate" hreflang="..."> w <head> każdej strony. Google rekomenduje oba mechanizmy równolegle — sitemap do odkrywania wariantów, HTML <link> jako silniejszy sygnał.
Cyberapis v0.14.0 przebudowuje cały system sitemap od zera. Zamiast polegać na crawlerze (który generował URL-e z localhosta i wciągał strony admina), sitemap jest teraz budowany manualnie z Post::indexable(), DocsPage::indexable() i stron statycznych. Każdy wpis bloga i strona dokumentacji dostaje dwa wpisy url — dla slugu EN i PL — z wzajemnymi hreflang alternatywami. Do tego dochodzi auto-regeneracja przy każdej zmianie w Filament i humans-readable HTML sitemap pod /sitemap.
Dodatkowo system slugów został przepisany: PostController::show() szuka teraz przez slug->en OR slug->pl, a nie tylko slug dla bieżącego locale. To oznacza, że Googlebot (który zawsze wysyła Accept-Language: en) może odkryć i zindeksować polskie URLe.
Nowe funkcjonalności
Przebudowany sitemap XML
Ręczna konstrukcja URL-i zamiast crawlingu — filtruje tylko is_published=true AND is_indexable=true, obejmuje 6 stron statycznych + wszystkie posty bloga + wszystkie strony dokumentacji. XSL stylesheet do czytelnego widoku w przeglądarce.
Wielojęzyczny sitemap z hreflang
Każdy wpis bloga i strona dokumentacji dostaje dwa wpisy url (EN slug + PL slug) z wzajemnymi tagami xhtml:link rel="alternate" hreflang="en/pl". Wyszukiwarki mogą teraz odkrywać i indeksować treści w obu językach.
Auto-regeneracja sitemap
Modele Post i DocsPage wyzwalają sitemap:generate przy eventach saved/deleted — sitemap jest zawsze aktualny w ciągu sekund od zmiany w Filament. Codzienny cron działa jako fallback.
Pole is_indexable
Dodane do tabel posts i docs_pages. Przełączane w Filament. Gdy wyłączone, strona dostaje meta name="robots" content="noindex" i jest wykluczana z sitemap.
Polskie URLe stron prawnych
/regulamin-serwisu i /polityka-prywatnosci z hreflang alternatywami do angielskich /terms i /privacy-policy. Linki w stopce używają lokalnie odpowiednich URL-i.
HTML sitemap dla ludzi
Strona /sitemap renderuje live'ową, zawsze aktualną listę wszystkich indeksowalnych stron pogrupowanych według sekcji (statyczne, blog, dokumentacja).
Zlokalizowana stopka
Klucz tłumaczenia footer.legal zastępuje zahardcodowany nagłówek. Linki prawne dostosowane do języka (PL vs EN URL-e).
Naprawione błędy
Post::toSitemapTag() — martwy kod
Przedwczesny return route('blog.show', $this) generował URLe z ID zamiast slugów (/blog/5 zamiast /blog/the-slug). Blok setLastModificationDate/setPriority nigdy się nie wykonywał. Przepisane na poprawne obiekty Url z hreflang alternatywami.
Localhost w sitemap
Usunięto generację opartą na crawlerze (SitemapGenerator::create()), który pobierał APP_URL z env (często localhost w dev). Wszystkie URLe używają teraz helpera route(), który rozwiązuje się poprawnie per środowisko.
/admin/* w sitemap
Crawler wciągał strony administracyjne. Ręczna konstrukcja URL-i obejmuje tylko publiczne routy.
Zmiany i ulepszenia
Wyszukiwanie slugów cross-locale
PostController::show() i DocsController::show() szukają teraz przez slug->en OR slug->pl zamiast tylko locale sesji. Polskie slugi działają nawet gdy przeglądarka ma Accept-Language: en (np. Googlebot). Dopasowane locale ustawiane przez app()->setLocale(), żeby UI pasował do języka treści.