Stawka jest konkretna. Jeśli nowa treść nie trafia do indeksu w kilkanaście minut od publikacji, tracisz ruch w „prime time" — przy newsach, dynamicznych landingach czy ofertach czasowych to wprost utracony przychód. Sitemap to Twój najważniejszy list motywacyjny do Googlebota. Mówi mu: „te adresy istnieją, te są ważne, te właśnie się zmieniły, wróć po nie szybciej". Automatyzacja tego procesu to nie luksus, tylko wymóg każdego projektu, który skaluje content powyżej 10–20 stron.
Architektura rozwiązania
Przepływ danych wygląda tak:
Cała decyzja sprowadza się do jednego: budujemy sitemapę w czasie build, a nie w czasie żądania (SSR). To rozróżnienie ma realne konsekwencje dla SEO i wydajności:
- Generowanie statyczne (, domyślne w Astro) — sitemapa powstaje raz, podczas builda. To zwykły plik XML serwowany z . wchodzi na niego tysiące razy i nigdy nie dotyka Twojej bazy ani API CMS-a. Zero obciążenia, natychmiastowa odpowiedź. To Astro w pełnej krasie: szybkość i lekkość.
- Generowanie dynamiczne () — sitemapa odpytywałaby CMS przy każdym żądaniu Googlebota. Większe obciążenie, wolniejsza odpowiedź, ryzyko timeoutu na dużych zbiorach i niepotrzebny ruch do API.
Skoro sitemapa zmienia się tylko przy publikacji treści (czyli przy kolejnym deployu/buildzie), nie ma powodu generować jej dynamicznie. Build-time to właściwy wybór — dlatego całą logikę opieramy o getStaticPaths i statyczne endpointy.
Implementacja krok po kroku (kod)
Krok 1: Instalacja @astrojs/sitemap
Oficjalna integracja Astro. Instalacja jednym poleceniem:
Wymaga ustawionego site w konfiguracji — bez tego integracja nie zbuduje absolutnych URL-i:
Najważniejszy mechanizm: @astrojs/sitemap automatycznie wykrywa wszystkie strony zbudowane podczas builda — łącznie z tymi wygenerowanymi przez getStaticPaths. Nie musisz ręcznie wpisywać ścieżek do sitemapy. Wystarczy, że Twoje dynamiczne trasy faktycznie powstają na buildzie.
Krok 2: Generowanie podstron z CMS przez getStaticPaths
To jest „ważny szczegół" obsługi tras typu /blog/[slug]. W pliku trasy dynamicznej pobierasz dane z API i zwracasz listę ścieżek. Każda z nich stanie się statyczną stroną — i automatycznie wpadnie do sitemapy.
Sanity zamiast
fetchna REST użyje zapytania GROQ przez klienta@sanity/client, ale zasada jest identyczna: pobierasz listę dokumentów zeslugiupdatedAt, mapujesz na ścieżki.
Po tym kroku @astrojs/sitemap zna już wszystkie adresy /blog/<slug>/. Brakuje mu tylko jednej rzeczy: daty ostatniej modyfikacji.
Krok 3: Mapowanie updatedAt → <lastmod> przez serialize
Integracja nie analizuje kodu źródłowego strony, więc sama nie wie, kiedy dany artykuł był aktualizowany. Tę informację wstrzykujemy w hooku serialize, który jest wołany dla każdego wpisu tuż przed zapisem na dysk. Najczystsze podejście: raz pobrać dane z CMS (top-level await w configu) i zbudować mapę ścieżka → updatedAt.
To wszystko, czego potrzebuje 90% projektów. Jeden build i masz sitemap-index.xml z poprawnymi datami <lastmod> dla każdego artykułu, aktualizowany automatycznie przy każdej publikacji (która i tak wyzwala redeploy).
Wariant „gotowiec": własny endpoint z pełną kontrolą
Czasem chcesz pełnej kontroli nad XML-em — własny podział na pliki, własne reguły, dane prosto z API bez polegania na auto-wykrywaniu stron. Wtedy tworzysz statyczny endpoint. Ten plik możesz wkleić do projektu i podmienić tylko stałe na górze:
W domyślnym trybie statycznym Astro ten endpoint jest renderowany raz, na buildzie — efekt jest taki sam jak przy @astrojs/sitemap: zwykły plik na CDN. (export const prerender = true ma znaczenie tylko wtedy, gdy projekt działa w trybie server/SSR — wymusza tam statyczność tego konkretnego pliku.)
Optymalizacja pod Googlebota
Lastmod is King — ale tylko prawdziwy
<lastmod> to jedyny z trzech opcjonalnych tagów, który Google realnie bierze pod uwagę. Poprawna, świeża data aktualizacji to sygnał: „tu coś się zmieniło, wróć szybciej" — i właśnie ona skraca czas ponownego crawlowania.
Jest jednak haczyk, o którym mało kto pisze: Google ufa lastmod tylko, jeśli jest wiarygodny. Jeśli na każdym buildzie ustawisz lastmod na „teraz" dla wszystkich stron (częsty błąd — branie new Date() zamiast daty z CMS), Google szybko zauważy, że daty kłamią, i zacznie je ignorować. Dlatego w kodzie wyżej lastmod pochodzi wprost z updatedAt w CMS, a nie z czasu builda. To różnica między sygnałem, który działa, a szumem, który Google odfiltrowuje.
Priorytetyzacja — z uczciwym zastrzeżeniem
Pokazałem, jak ustawić priority (strona główna 1.0, artykuły 0.8, kategorie 0.5) w funkcji serialize. Rób to dla porządku i dla innych narzędzi (część wyszukiwarek i crawlerów to czyta), ale bądźmy szczerzy: Google oficjalnie ignoruje priority i changefreq. Potwierdza to nawet dokumentacja @astrojs/sitemap. Nie buduj strategii indeksacji wokół tych tagów — całą realną robotę odwala poprawny lastmod i sama obecność URL-a w sitemapie.
Sitemap Index — co zrobić przy 5000+ stronach
Pojedynczy plik sitemapy ma limity: maksymalnie 50 000 URL-i i 50 MB (Astro domyślnie tnie pliki przy entryLimit: 45000). Przy dużych katalogach produktów czy rozbudowanym blogu i tak dziel sitemapę na mniejsze, tematyczne pliki — nie z konieczności technicznej, ale dlatego, że dużo łatwiej diagnozuje się indeksację w , gdy każdy typ treści ma osobny plik (blog-sitemap.xml, products-sitemap.xml).
W @astrojs/sitemap służy do tego opcja chunks:
Powstaną osobne pliki (sitemap-blog-0.xml, sitemap-produkty-0.xml, plus domyślny koszyk dla reszty), wszystkie spięte w sitemap-index.xml. Gdy ruch z bloga spadnie, od razu widzisz w GSC, czy problem dotyczy bloga, czy produktów.
Monitoring i utrzymanie
Weryfikacja w Google Search Console
Po wdrożeniu zgłoś sitemapę w GSC: raport Mapy witryn → dodaj adres https://strivelab.pl/sitemap-index.xml. Po przetworzeniu zobaczysz status (powinien być „Sukces"), datę ostatniego odczytu i liczbę wykrytych adresów. Jeśli liczba „wykrytych" mocno odbiega od realnej liczby treści — masz sygnał, że coś w generowaniu nie zadziałało. Rozbicie na pliki tematyczne (sekcja wyżej) sprawia, że od razu wiadomo gdzie.
Ostrzeżenie: co, jeśli API CMS-a padnie podczas builda?
To realne ryzyko, które trzeba świadomie obsłużyć. Jeśli fetch do CMS-a zwróci błąd albo pustą listę w trakcie builda, masz dwa scenariusze — i oba bywają groźne:
- build się wywala → nie wdrożysz nic (frustrujące, ale bezpieczne),
- build przechodzi z pustą listą → wdrażasz serwis z pustą sitemapą i bez podstron bloga, co przy regularnym crawlowaniu może doprowadzić do deindeksacji treści.
Dlatego rekomenduję podejście fail-fast dla danych krytycznych: lepiej, żeby build padł głośno, niż żeby po cichu wdrożył uszkodzony serwis.
Wariant pośredni dla bardzo dużych serwisów: zamiast twardego throw możesz wczytać ostatnią dobrą wersję danych z cache (np. plik JSON commitowany na deployu), żeby chwilowa awaria CMS-a nie blokowała wydania. Wybór zależy od tego, co jest gorsze w Twoim projekcie: zablokowany deploy czy ryzyko nieświeżych danych.
Jeśli budujesz rozwiązanie dla biznesu i zależy Ci na solidnej architekturze — nie tylko stronie, ale całym ekosystemie treści na Astro i headless CMS — sprawdź moją matrycę decyzyjną AI albo napisz do mnie w sprawie wdrożenia. A jeśli serwis już działa, ale indeksacja kuleje, od tego jest audyt techniczny SEO.
