Przejdź do treści

Dynamiczna sitemap.xml w Astro z API CMS-a

Jak wygenerować sitemap.xml w Astro z Payload lub Sanity? Praktyczny wzorzec z getStaticPaths, lastmod, sitemap-index i przykładami na example.com.

Maciej Sala

Founder StriveLab

12 min czytaniaAktualizacja

Szybka odpowiedź: jak wygenerować sitemap.xml w Astro z CMS?

Najkrótsza odpowiedź brzmi: pobierz z CMS-a wszystkie opublikowane i indeksowalne rekordy, wygeneruj odpowiadające im strony przez getStaticPaths, a @astrojs/sitemap pozwól zbudować XML. Pole updatedAt przekaż do <lastmod>, publikację w CMS-ie połącz webhookiem z nowym buildem, a wynik sprawdź przed wdrożeniem.

W tym kontekście słowo „dynamiczna" opisuje przede wszystkim dynamiczne źródło danych, nie sposób serwowania pliku. Wynikiem może być całkowicie statyczny sitemap-index.xml. To zwykle najlepsze połączenie świeżych danych z CMS-a i odporności pliku dostarczanego z CDN.

Jeśli nowa treść nie jest szybko odkrywana, tracisz jej najbardziej aktualny okres, co przy newsach, landingach kampanii czy ofertach czasowych może oznaczać utracony ruch. Sitemapa nie gwarantuje indeksacji, ale pomaga crawlerowi odkryć kanoniczne adresy i rozpoznać istotne aktualizacje. Jej automatyzacja ma sens od chwili, gdy ręczne utrzymywanie listy URL-i staje się podatne na pomyłki. Nie istnieje przy tym magiczny próg liczby podstron, od którego sitemapa staje się obowiązkowa.

Architektura: build-time jako domyślny wybór

Przepływ danych wygląda tak:

Diagram
Zalecany przepływ: publikacja uruchamia build, a błąd CMS-a nie zastępuje ostatniego poprawnego wdrożenia.

W większości projektów budujemy sitemapę w czasie build, a nie w czasie żądania. To rozróżnienie ma realne konsekwencje dla wydajności i odporności:

  • Generowanie statyczne (, domyślne w Astro): sitemapa powstaje raz podczas builda. To plik XML serwowany z . nie dotyka wtedy bazy ani API CMS-a. Odpowiedź jest szybka i nie zależy od bieżącej dostępności zaplecza.
  • Generowanie na żądanie (): endpoint może pobierać aktualne dane bez nowego deployu, ale niezbuforowana wersja zwiększa obciążenie CMS-a i ryzyko timeoutu. Jeśli ten wariant jest konieczny, generuj migawkę w tle albo stosuj długi cache i mechanizm stale-if-error.

Build-time jest właściwym wyborem, jeśli publikacja, aktualizacja albo usunięcie treści uruchamia nowy deploy. Sam CMS tego nie zapewnia. Potrzebujesz webhooka do platformy hostingowej lub innego mechanizmu wyzwalającego build.

SytuacjaZalecany wariantDlaczego
Astro SSG, treść publikuje się przez webhook@astrojs/sitemap + getStaticPathsNajmniej własnego kodu, automatyczne wykrywanie zbudowanych tras
Projekt SSR, ale mapa może odświeżać się przy deployuStatyczny endpoint z export const prerender = trueStrony mogą być SSR, a XML pozostaje szybki i niezależny od CMS-a
Trasy SSR nie są znane podczas buildacustomPages pobrane z CMS-a albo własny endpointIntegracja nie potrafi automatycznie rozwinąć nieprerenderowanych tras dynamicznych
Bardzo częste zmiany bez deployuBuforowany endpoint lub okresowo generowany plikAktualność bez odpytywania CMS-a przy każdym żądaniu

Jakie URL-e powinny trafić do sitemap.xml w Astro

Sitemapa nie jest listą wszystkich adresów, które technicznie istnieją w aplikacji. To lista URL-i, które chcesz widzieć w wynikach wyszukiwania. Dodawaj więc tylko adresy, które:

  1. Zwracają HTTP 200.
  2. Nie używają .
  3. Są kanoniczną wersją danej treści.
  4. Nie są przekierowaniem, fallbackiem ani stroną błędu.
  5. Mają realną treść i nie są pustym szablonem z CMS-a.
  6. Nie są wariantem śledzącym lub sortującym, takim jak ?utm_source= czy ?sort=.

W praktyce filtruj dane już na etapie pobierania z CMS-a: status === 'published', slug istnieje, canonicalUrl nie wskazuje gdzie indziej, a rekord nie jest wersją roboczą. Jeżeli strona ma robots: noindex, nie powinna pojawić się w sitemapie. Jeżeli URL przekierowuje na inny adres, w sitemapie powinien być adres docelowy. Paginacji nie wykluczaj automatycznie. Adresy typu /blog/strona/2/ mogą być poprawnymi, samodzielnymi stronami, jeśli są kanoniczne i pomagają crawlerowi dotrzeć do starszych treści.

Implementacja sitemap.xml w Astro krok po kroku

Krok 1: Instalacja @astrojs/sitemap

Oficjalna integracja Astro. Instalacja jednym poleceniem:

Code
npx astro add sitemap

Wymaga ustawionego site w konfiguracji. Bez tego integracja nie zbuduje absolutnych URL-i:

Code
// astro.config.mjs
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'
 
export default defineConfig({
  site: 'https://example.com',
  integrations: [sitemap()],
})

Najważniejszym mechanizmem jest @astrojs/sitemap, który wykrywa strony zbudowane podczas builda, włącznie z tymi wygenerowanymi przez getStaticPaths. Nie musisz ręcznie wpisywać ich ścieżek. Jest jeden ważny wyjątek: integracja nie generuje wpisów dla wariantów tras dynamicznych, które istnieją wyłącznie w trybie SSR i nie mają listy ścieżek w czasie builda.

Jeśli łączysz statyczne strony z dynamicznymi trasami SSR, możesz podać te drugie jako customPages:

Code
const SITE = 'https://example.com'
const cmsUrls = posts.map((post) => {
  return new URL(`/blog/${post.slug}/`, SITE).href
})
 
sitemap({
  customPages: cmsUrls,
})

Lista nadal musi pochodzić z pełnego, przefiltrowanego odczytu CMS-a. customPages rozwiązuje problem odkrywania tras przez integrację, ale nie zastępuje walidacji danych.

Krok 2: pobranie wszystkich rekordów, a nie tylko pierwszej strony API

To miejsce, w którym najłatwiej stworzyć poprawny XML z niepełną zawartością. Payload ma paginowane zapytania, a odpowiedź zawiera między innymi totalPages, hasNextPage i nextPage. Nie polegaj na limit=10000: serwer może ograniczyć maksymalny limit, zapytanie może stać się ciężkie, a wraz ze wzrostem bazy założenie w końcu przestanie działać.

Poniżej znajduje się uproszczony loader dla Payload CMS. Filtruje rekordy po stronie API, pobiera je partiami i zatrzymuje build, gdy wykryje brak slugu, nieprawidłową datę albo duplikat:

Code
type CmsPost = {
  slug: string
  title: string
  updatedAt: string
}
 
type PayloadPage = {
  docs: CmsPost[]
  hasNextPage: boolean
  nextPage: number | null
}
 
async function getAllPublishedPosts(CMS_API: string): Promise<CmsPost[]> {
  if (!CMS_API) throw new Error('Brak zmiennej CMS_API_URL')
 
  const posts: CmsPost[] = []
  let page = 1
 
  while (true) {
    const url = new URL(`${CMS_API}/posts`)
    url.searchParams.set('limit', '100')
    url.searchParams.set('page', String(page))
    url.searchParams.set('depth', '0')
    url.searchParams.set('where[_status][equals]', 'published')
    url.searchParams.set('sort', 'slug')
 
    const res = await fetch(url)
    if (!res.ok) throw new Error(`CMS odpowiedział statusem ${res.status}`)
 
    const data = (await res.json()) as PayloadPage
    if (!Array.isArray(data.docs)) {
      throw new Error('CMS zwrócił nieprawidłowy format danych')
    }
 
    for (const post of data.docs) {
      if (!post.slug) throw new Error('Opublikowany wpis nie ma slugu')
      if (Number.isNaN(Date.parse(post.updatedAt))) {
        throw new Error(`Nieprawidłowe updatedAt dla slugu: ${post.slug}`)
      }
      posts.push(post)
    }
 
    if (!data.hasNextPage || data.nextPage === null) break
    page = data.nextPage
  }
 
  const slugs = posts.map((post) => post.slug)
  if (new Set(slugs).size !== slugs.length) {
    throw new Error('CMS zwrócił zduplikowane slugi')
  }
 
  return posts
}

W Payload CMS z włączonymi wersjami roboczymi pole publikacji nazywa się _status. Jeśli w Twojej kolekcji istnieje własne pole status, dostosuj filtr do schematu. W produkcji pobieraj tylko pola potrzebne do danego etapu. Lista do sitemapy zwykle potrzebuje slug, updatedAt, statusu indeksacji i ewentualnego canonicalUrl. Pełną treść artykułu możesz pobrać osobno przy renderowaniu strony. W Sanity odpowiednikiem dla dużych zbiorów jest paginacja kursorem po stabilnym i jednoznacznym polu, na przykład _id, zamiast coraz większych zakresów tablicy.

Krok 3: generowanie podstron 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.

Code
// src/pages/blog/[slug].astro
---
import { getAllPublishedPosts } from '../../lib/cms/posts'
 
export async function getStaticPaths() {
  const docs = await getAllPublishedPosts(import.meta.env.CMS_API_URL)
 
  return docs
    .map((post) => ({
      params: { slug: post.slug },
      props: { post },
    }))
}
 
const { post } = Astro.props
---
<article>
  <h1>{post.title}</h1>
  <!-- ... -->
</article>

W przykładzie funkcję getAllPublishedPosts warto umieścić we wspólnym module. Jeżeli karta artykułu potrzebuje całej treści, loader powinien zwracać ją w props albo strona może wykonać osobne zapytanie po slugu.

Po tym kroku @astrojs/sitemap zna już wszystkie adresy /blog/<slug>/. Brakuje mu tylko jednej rzeczy: daty ostatniej modyfikacji.

Krok 4: mapowanie updatedAt na <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, przerwać build przy błędzie i zbudować mapę ścieżka → updatedAt.

Code
// astro.config.mjs
import { defineConfig } from 'astro/config'
import sitemap from '@astrojs/sitemap'
import { loadEnv } from 'vite'
import { getAllPublishedPosts } from './src/lib/cms/posts'
 
export default defineConfig(({ mode }) => {
  const SITE = 'https://example.com'
  const { CMS_API_URL } = loadEnv(mode, process.cwd(), '')
  const lastmodMapPromise = getAllPublishedPosts(CMS_API_URL).then(
    (docs) =>
      new Map(
        docs.map((post) => [
          new URL(`/blog/${post.slug}/`, SITE).pathname,
          new Date(post.updatedAt).toISOString(),
        ]),
      ),
  )
 
  return {
    site: SITE,
    integrations: [
      sitemap({
        async serialize(item) {
          const lastmodMap = await lastmodMapPromise
          const path = new URL(item.url).pathname
          const lastmod = lastmodMap.get(path)
          if (lastmod) item.lastmod = lastmod
          return item
        },
      }),
    ],
  }
})

To wszystko, czego potrzebuje większość projektów. Jeden build i masz sitemap-index.xml z poprawnymi datami <lastmod> dla każdego opublikowanego artykułu. Pamiętaj, że konfiguracja sitemapy i getStaticPaths mogą pobrać dane osobno. Ogranicz zakres pól, użyj cache klienta CMS albo przygotuj jedną migawkę danych dla builda, jeśli podwójny odczyt jest kosztowny.

Krok 5: webhook z CMS-a uruchamiający build

Statyczna sitemapa jest aktualna tylko tak długo, jak aktualny jest deploy. Skonfiguruj webhook dla zdarzeń publikacji, aktualizacji, usunięcia i cofnięcia publikacji. Powinien wywoływać build hook hostingu. Dzięki temu ta sama operacja aktualizuje stronę, linkowanie wewnętrzne i sitemapę.

Zabezpiecz webhook sekretem i ogranicz liczbę buildów. Gdy redaktor zapisuje dokument kilka razy w ciągu minuty, debounce lub kolejka mogą połączyć serię zdarzeń w jedno wdrożenie. Po deployu wykonaj test dymny: sprawdź, czy nowy URL występuje w sitemapie i zwraca 200.

Wariant „gotowiec": własny endpoint z pełną kontrolą

Czasami potrzebujesz pełnej kontroli nad XML-em: własnego podziału na pliki, osobnych reguł albo danych niezależnych od automatycznego wykrywania stron. W takiej sytuacji utwórz statyczny endpoint. Poniższy przykład korzysta z paginowanego loadera z wcześniejszej sekcji:

Code
// src/pages/blog-sitemap.xml.ts
import type { APIRoute } from 'astro'
import { getAllPublishedPosts } from '../lib/cms/posts'
 
const SITE = 'https://example.com'
 
// W trybie SSR wymuszamy statyczne generowanie tego pliku,
// żeby Googlebot nie odpytywał CMS-a przy każdym wejściu.
export const prerender = true
 
// Escape znaków, które rozbiłyby XML, na przykład & w adresie.
const esc = (s: string) =>
  s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
 
const encodePath = (slug: string) => {
  return slug.split('/').map(encodeURIComponent).join('/')
}
 
export const GET: APIRoute = async () => {
  const posts = await getAllPublishedPosts(import.meta.env.CMS_API_URL)
 
  const urls = posts
    .map((post) => {
      const slug = encodePath(post.slug)
      const loc = esc(`${SITE}/blog/${slug}/`)
      const lastmod = new Date(post.updatedAt).toISOString()
      return `  <url>
    <loc>${loc}</loc>
    <lastmod>${lastmod}</lastmod>
  </url>`
    })
    .join('\n')
 
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`
 
  return new Response(xml, {
    headers: { 'Content-Type': 'application/xml; charset=utf-8' },
  })
}

W domyślnym trybie statycznym Astro ten endpoint jest renderowany raz, podczas builda. Efekt jest taki sam jak przy @astrojs/sitemap, czyli zwykły plik na CDN. export const prerender = true ma znaczenie w projekcie z renderowaniem serwerowym, ponieważ wymusza statyczność tego konkretnego pliku.

Ten przykład tworzy wyłącznie blog-sitemap.xml. Nadal potrzebujesz mapy dla pozostałych sekcji i indeksu wskazującego wszystkie pliki albo musisz zgłosić poszczególne mapy osobno. Jeśli równolegle używasz @astrojs/sitemap, wyklucz własne endpointy XML z automatycznie generowanej mapy, aby sitemapa nie wskazywała samej siebie:

Code
sitemap({
  filter: (page) => !new URL(page).pathname.endsWith('.xml'),
})

Własny pojedynczy endpoint nadaje się tylko wtedy, gdy mieści się w limicie 50 000 URL-i i 50 MB bez kompresji. Powyżej tego progu generator musi utworzyć kilka plików <urlset> oraz indeks <sitemapindex>. Integracja robi ten podział za Ciebie, dlatego własny XML warto wybierać tylko wtedy, gdy dodatkowa kontrola rzeczywiście uzasadnia kod i testy.

Sitemap.xml a : co naprawdę czyta Google?

<lastmod> musi oznaczać istotną zmianę

<lastmod> to opcjonalny tag, który Google może wziąć pod uwagę, ale tylko wtedy, gdy jest spójny i możliwy do zweryfikowania. Poprawna data aktualizacji mówi: „tu zmieniła się istotna treść, warto wrócić". Nie powinna oznaczać każdego builda, deployu, zmiany stopki albo aktualizacji informacji o prawach autorskich.

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, Google szybko zauważy, że daty nie odpowiadają zmianom treści, i zacznie je ignorować. Częstym błędem jest użycie new Date() zamiast daty z CMS-a. Dlatego w kodzie wyżej lastmod pochodzi wprost z updatedAt, a nie z czasu builda. To różnica między użytecznym sygnałem a szumem.

priority i changefreq nie są strategią indeksacji

Możesz ustawić priority i changefreq dla porządku albo dla narzędzi, które je analizują, ale Google oficjalnie ignoruje oba tagi. Nie buduj strategii indeksacji wokół wartości 0.8 czy weekly. Większe znaczenie mają: kanoniczny URL, poprawny status HTTP, brak noindex, linkowanie wewnętrzne i wiarygodny lastmod.

Sitemap Index: co zrobić przy tysiącach stron?

Pojedynczy plik sitemapy ma limity: maksymalnie 50 000 URL-i i 50 MB bez kompresji. @astrojs/sitemap domyślnie dzieli wynik przy entryLimit: 45000, zostawiając bezpieczny margines. Przy dużych katalogach produktów czy rozbudowanym blogu warto dodatkowo dzielić sitemapę na mniejsze pliki tematyczne. Nie tylko z konieczności technicznej, ale także dlatego, że łatwiej diagnozuje się indeksację w , gdy każdy typ treści ma osobny plik.

W @astrojs/sitemap służy do tego opcja chunks:

Code
sitemap({
  entryLimit: 10000,
  chunks: {
    blog: (item) => {
      if (new URL(item.url).pathname.startsWith('/blog/')) return item
    },
    produkty: (item) => {
      if (new URL(item.url).pathname.startsWith('/produkty/')) return item
    },
  },
})

Powstaną osobne pliki (sitemap-blog-0.xml, sitemap-produkty-0.xml oraz domyślny zbiór dla reszty), wszystkie spięte w sitemap-index.xml. Gdy liczba indeksowanych wpisów blogowych spadnie, szybciej rozpoznasz w GSC, której sekcji dotyczy problem.

Opcja chunks jest dostępna w @astrojs/sitemap od wersji 3.7.0. Jeśli korzystasz ze starszej wersji, zaktualizuj integrację albo zastosuj kilka własnych endpointów i osobny indeks sitemap.

Wielojęzyczne adresy i hreflang

Jeżeli serwis ma równoważne wersje językowe, alternatywy możesz opisać w sitemapie. Integracja Astro obsługuje konfigurację i18n, ale adresy muszą odpowiadać realnym, kanonicznym stronom. Nie dodawaj automatycznie tłumaczenia, które nie istnieje albo przekierowuje na język domyślny. To samo powiązanie powinno być wzajemne: polska wersja wskazuje angielską, a angielska polską.

Monitoring i utrzymanie sitemap.xml w Astro

Weryfikacja w Google Search Console

Po wdrożeniu zgłoś sitemapę w GSC. Wejdź w raport Mapy witryn → dodaj adres https://example.com/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 wtedy wyraźny sygnał, że coś jest nie tak. Rozbicie na pliki tematyczne (sekcja wyżej) sprawia, że od razu wiadomo gdzie.

Dodaj też sitemapę do robots.txt, żeby crawler znalazł ją bez ręcznego zgłoszenia w panelu:

Code
User-agent: *
Allow: /
 
Sitemap: https://example.com/sitemap-index.xml

Zgłoszenie sitemapy pomaga wyszukiwarkom w odkrywaniu nowych adresów, ale nie gwarantuje natychmiastowego crawlowania ani indeksacji i dlatego każda nowa podstrona wciąż potrzebuje solidnych linków wewnętrznych. Pamiętaj, że mapa witryny jest cennym uzupełnieniem architektury informacji, ale jej nie zastępuje.

Kontrola jakości po każdym buildzie

Sam status 200 dla pliku XML to za mało. Do CI warto dodać test, który po astro build sprawdzi:

  1. Czy wszystkie mapy są poprawnym XML-em i nie przekraczają limitów.
  2. Czy każdy <loc> jest absolutnym adresem HTTPS we właściwej domenie.
  3. Czy adresy są unikalne i nie zawierają parametrów śledzących.
  4. Czy liczba URL-i nie spadła nagle względem poprzedniego wdrożenia.
  5. Czy próbka stron zwraca 200, nie ma noindex i wskazuje samą siebie jako adres kanoniczny.
  6. Czy daty <lastmod> są prawidłowe i nie leżą przypadkowo w przyszłości.

Próg liczby adresów powinien uwzględniać naturalną rotację i usuwanie treści. W przypadku serwisu posiadającego 12 000 produktów znacznie lepiej sprawdzi się alarm ustawiony na spadek liczby URL-i o 20 procent. Poza tym warto również monitorować i zapisywać historyczne statystyki liczby adresów dla każdego pliku tematycznego z osobna – taka prosta seria czasowa pozwala błyskawicznie wychwycić nagłe błędy w filtrowaniu, paginacji lub działaniu webhooków.

Ostrzeżenie: co, jeśli API CMS-a padnie podczas builda?

To realne ryzyko, które trzeba obsłużyć w sposób przemyślany. Jeśli fetch do CMS-a zwróci błąd albo niepełną listę w trakcie builda, możliwe są dwa scenariusze:

  • build kończy się błędem i ostatnie poprawne wdrożenie pozostaje aktywne,
  • build przechodzi z niepełną listą i może wdrożyć pustą sitemapę oraz brak statycznych podstron bloga.

Sama pusta sitemapa nie wydaje Google polecenia usunięcia stron z indeksu. Zagrożeniem jest wdrożenie stron zwracających 404 albo długotrwała utrata sygnałów odkrywania. Dlatego rekomenduję podejście fail-fast dla danych krytycznych. Niech build zatrzyma się z czytelnym błędem, zamiast po cichu zastępować poprawny serwis niepełną wersją.

Code
async function getPosts(CMS_API: string) {
  try {
    const docs = await getAllPublishedPosts(CMS_API)
    const minimumExpected = Number(process.env.MIN_PUBLISHED_POSTS ?? 1)
 
    if (docs.length < minimumExpected) {
      throw new Error(
        `CMS zwrócił ${docs.length} wpisów, oczekiwano co najmniej ${minimumExpected}`,
      )
    }
    return docs
  } catch (err) {
    // Fail-fast: zachowaj ostatnie poprawne wdrożenie.
    console.error('[sitemap] Błąd pobierania danych z CMS:', err)
    throw err
  }
}

Wariant pośredni dla dużych serwisów to wczytanie ostatniej poprawnej migawki danych z kontrolowanego magazynu, aby chwilowa awaria CMS-a nie blokowała wydania. Każda migawka musi być odpowiednio wersjonowana, posiadać określoną datę ważności oraz przechodzić te same testy co standardowe dane. Stosowanie cichego fallbacku do przypadkowego, lokalnego pliku to prosta droga do tego, by chwilowy problem sieciowy przerodził się w wielotygodniową, niezauważoną nieaktualność serwisu.

W sytuacji, kiedy 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, napisz do mnie w sprawie wdrożenia. A jeśli serwis już działa, ale indeksacja kuleje, zacznij od audytu technicznego SEO.

Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Jak zrobić dynamiczną sitemapę w Astro z headless CMS?

Najprościej: zainstaluj @astrojs/sitemap, generuj podstrony przez getStaticPaths (pobierając listę dokumentów z API CMS-a), a w opcji serialize ustaw lastmod na podstawie pola updatedAt. Integracja wykryje zbudowane strony i wygeneruje statyczny sitemap-index.xml. Sitemapa pomaga wyszukiwarkom odkrywać URL-e, ale nie gwarantuje indeksacji.

Czy sitemapa w Astro jest generowana dynamicznie przy każdym wejściu Googlebota?

Nie, jeśli budujesz ją na etapie build (domyślny tryb statyczny Astro). To wtedy zwykły plik XML serwowany z CDN. Googlebot nie odpytuje Twojej bazy ani CMS-a. Sitemapa aktualizuje się przy kolejnym deployu, dlatego publikacja treści w CMS-ie powinna uruchamiać build przez webhook. W projekcie SSR można też wystawić endpoint generowany na żądanie, ale powinien być buforowany i odporny na awarię CMS-a.

Jak ustawić lastmod w sitemapie Astro na podstawie daty z CMS?

W funkcji serialize integracji @astrojs/sitemap przypisz item.lastmod z pola updatedAt dokumentu (sformatowanego jako ISO string). Nie używaj czasu builda jako daty wszystkich stron. Google może ignorować lastmod, jeśli nie odpowiada on rzeczywistym, istotnym zmianom treści.

Czy priority i changefreq w sitemapie mają sens dla SEO?

Dla Google nie. Oficjalnie ignoruje oba tagi. Możesz je ustawiać dla porządku i dla innych narzędzi, ale realny wpływ na odkrywanie i ponowne crawlowanie mają przede wszystkim poprawne, kanoniczne URL-e oraz wiarygodny lastmod.

Co zrobić, gdy mam więcej niż 50 000 stron?

Pojedyncza sitemapa ma limit 50 000 URL-i i 50 MB. Astro automatycznie dzieli pliki (entryLimit, domyślnie 45000) i spina je w sitemap-index.xml. Dodatkowo użyj opcji chunks, by podzielić sitemapę tematycznie (blog, produkty). To ułatwia diagnostykę indeksacji w Search Console.

Co się stanie, jeśli API CMS-a padnie podczas builda?

Sama pusta sitemapa nie usuwa stron z indeksu. Problem jest poważniejszy, gdy ten sam build nie wygeneruje podstron i wdroży URL-e zwracające 404. Rekomendowane jest podejście fail-fast: sprawdź odpowiedź, format oraz rozsądną minimalną liczbę rekordów i przerwij build (throw) przy anomalii. Alternatywą jest ostatnia poprawna migawka danych z cache.

Czy to samo podejście zadziała z Sanity zamiast Payload?

Tak. Zmienia się sposób pobierania danych: zamiast REST API Payload użyjesz zapytania GROQ przez @sanity/client. Mapowanie na ścieżki, lastmod i podział sitemapy pozostają takie same. Przy tysiącach dokumentów nie pobieraj jednak całego zbioru jednym rosnącym zakresem. W Sanity zastosuj paginację opartą na stabilnym kursorze, na przykład _id.

Czy sitemapa gwarantuje indeksację wszystkich stron?

Nie. Sitemapa jest sygnałem odkrywania URL-i, a nie gwarancją indeksacji. Google nadal ocenia jakość strony, kanoniczność, duplikację, dostępność dla crawlera, linkowanie wewnętrzne i dyrektywy typu noindex. Dlatego do sitemapy wrzucaj tylko adresy, które mają zwracać 200, być kanoniczne i nadawać się do indeksowania.

Czy @astrojs/sitemap obejmuje dynamiczne strony renderowane w SSR?

Nie automatycznie. Integracja zna statyczne trasy i warianty wygenerowane przez getStaticPaths, ale nie potrafi odgadnąć parametrów tras renderowanych wyłącznie na żądanie. Takie adresy dodaj przez customPages na podstawie pełnej listy z CMS-a albo wygeneruj własny endpoint XML.

Jak szybko zaktualizować statyczną sitemapę po publikacji w CMS?

Połącz zdarzenia publikacji, aktualizacji, usunięcia i cofnięcia publikacji z build hookiem hostingu. Webhook powinien uruchomić nowy build Astro, a test po wdrożeniu powinien potwierdzić obecność URL-a w sitemapie oraz odpowiedź 200 strony.

O autorze

Maciej Sala

Maciej Sala — konsultant technologiczny produktów cyfrowych i web developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną
LH.pl – Hosting Mango

Biblioteka wiedzy na temat Astro

Czytaj dalej

Zobacz więcej wpisów
Astro i Headless CMS: Integracja Sanity, Storyblok, Strapi

Markdown w plikach projektu działa sprawnie, dopóki treść piszą osoby, które swobodnie pracują w Git. W sytuacji, kiedy klient chce sam zmienić cennik, a redaktor poprawia nagłówek w piątek po południu, repozytorium przestaje być wygodnym CMS-em. Wtedy najlepszy będzie headless CMS, czyli panel dla redakcji i API dla Astro.

Maciej Sala

Maciej Sala

Founder StriveLab

Przewodnik po projektach w Astro od architektury po utrzymanie

Strona internetowa może mieć świetny wynik PageSpeed, a mimo to wciąż mieć różne problemy, poczynając od blokowania publikacji artykułów, a kończąc na gubieniu zapytania z formularza. Stworzyłem ten przewodnik, by sprawnie prowadził przez wybór technologii, migrację, model treści, rendering, SEO i testy, aż po wdrożenie i utrzymanie. Szczegółowe rozwiązania znajdziesz w materiałach przypisanych do poszczególnych etapów.

Maciej Sala

Maciej Sala

Founder StriveLab

Integracja Astro i Payload CMS: szybka strona pod kontrolą

Wyobraź sobie stronę internetową, której koszt utrzymania nie rośnie wraz z liczbą odwiedzin. Otrzymasz taki efekt, łącząc lekki frontend w Astro z własnym CMS-em w postaci Payload, odizolowanym od ruchu użytkowników. To architektura, która daje wysoką wydajność i pełną kontrolę nad danymi, jednocześnie trzymając koszty pod kontrolą.

Maciej Sala

Maciej Sala

Founder StriveLab

LH.pl – Cloud Server 1C4G