Next.js Sitemap i robots.txt — automatyczna generacja z App Routera

Jak generować sitemap.xml i robots.txt w Next.js App Router? Natywne API konwencji plików vs next-sitemap — dynamiczne sitemaps, lastmod, changefreq i priorytety.

Opublikowano

10 kwietnia 2026 14:30

Czytanie

4 min czytania

Aktualizacja

15 kwietnia 2026 15:01

Dlaczego sitemap i robots.txt są ważne?

Sitemap.xml informuje wyszukiwarki o wszystkich stronach w serwisie — ich lokalizacji, dacie ostatniej modyfikacji i opcjonalnych wskazówkach, a plik Robots.txt kontroluje, które ścieżki crawlery mogą swobodnie odwiedzać. Choć oba pliki, nie gwarantują indeksacji ani jej blokady, są fundamentem technicznego SEO.

Next.js App Router oferuje natywne API (bez zewnętrznych biblioktek) do generowania obu plików.

Natywna sitemap w App Router

Statyczna sitemap

Najprostszym sposobem jest eksport tablicy obiektów z pliku sitemap.ts:

Code
// app/sitemap.ts
import type { MetadataRoute } from 'next'
 
export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://strivelab.pl',
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'monthly',
      priority: 1,
    },
    {
      url: 'https://strivelab.pl/uslugi',
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'monthly',
      priority: 0.9,
    },
    {
      url: 'https://strivelab.pl/blog',
      lastModified: new Date('2025-03-01'),
      changeFrequency: 'weekly',
      priority: 0.8,
    },
    {
      url: 'https://strivelab.pl/kontakt',
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'yearly',
      priority: 0.5,
    },
  ]
}

Next.js generuje /sitemap.xml automatycznie.

Pułapka: lastModified: new Date() dla stron statycznych. Użycie new Date() (aktualny czas buildu) dla stron, które rzadko się zmieniają, spowoduje, że przy każdym deployu Google zobaczy nową datę modyfikacji. Może to zmylić crawlera i zmarnować crawl budget. Dla stron statycznych używaj konkretnej daty lub pomijaj pole lastModified całkowicie. Dla treści z bazy danych pobieraj faktyczne pole updatedAt z rekordu.

changeFrequency i priority Google oficjalnie deklaruje, że oba pola traktuje tylko jako wskazówki i w praktyce je ignoruje. Z kolei inne wyszukiwarki (Bing, Yandex) mogą je uwzględniać, więc warto je wypełniać, ale nie skupiaj się na ich optymalizacji, kosztem innych działań SEO.

Dynamiczna sitemap — wpisy z CMS/bazy

Code
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { db } from '@/lib/db'
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://strivelab.pl'
 
  // Statyczne strony — konkretna data, nie new Date()
  const staticPages: MetadataRoute.Sitemap = [
    {
      url: baseUrl,
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'monthly',
      priority: 1,
    },
    {
      url: `${baseUrl}/uslugi`,
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'monthly',
      priority: 0.9,
    },
    {
      url: `${baseUrl}/kontakt`,
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'yearly',
      priority: 0.5,
    },
  ]
 
  // Dynamiczne strony — posty blogowe
  const posts = await db.post.findMany({
    where: { published: true },
    select: { slug: true, updatedAt: true },
    orderBy: { updatedAt: 'desc' },
  })
 
  const blogPages: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: post.updatedAt, // faktyczna data z bazy — poprawne
    changeFrequency: 'monthly' as const,
    priority: 0.7,
  }))
 
  return [...staticPages, ...blogPages]
}

Wielojęzyczna sitemap z hreflang

Natywne API obsługuje pole alternates, służy do deklarowania wersji językowych strony, co Google wymaga przy implementacji hreflang:

Code
// app/sitemap.ts
import type { MetadataRoute } from 'next'
 
export default function sitemap(): MetadataRoute.Sitemap {
  const baseUrl = 'https://strivelab.pl'
 
  return [
    {
      url: baseUrl,
      lastModified: new Date('2025-01-15'),
      changeFrequency: 'monthly',
      priority: 1,
      alternates: {
        languages: {
          pl: baseUrl,
          en: `${baseUrl}/en`,
        },
      },
    },
    {
      url: `${baseUrl}/blog/jak-zbudowac-api`,
      lastModified: new Date('2025-02-10'),
      changeFrequency: 'monthly',
      priority: 0.7,
      alternates: {
        languages: {
          pl: `${baseUrl}/blog/jak-zbudowac-api`,
          en: `${baseUrl}/en/blog/how-to-build-an-api`,
        },
      },
    },
  ]
}

Google wymaga, żeby każda wersja językowa wskazywała na pozostałe wersje w alternates (symetrycznie), jednostronne deklaracje hreflang są ignorowane.

Wiele sitemaps — dla dużych serwisów

Sitemap ma limit 50 000 URL-ów lub 50 MB (nieskompresowany), dla dużych serwisów — generuj wiele sitemaps z indeksem:

Code
// app/sitemap.ts
import type { MetadataRoute } from 'next'
import { db } from '@/lib/db'
 
export async function generateSitemaps() {
  const totalPosts = await db.post.count({ where: { published: true } })
  const sitemapsNeeded = Math.ceil(totalPosts / 50000)
 
  return Array.from({ length: sitemapsNeeded }, (_, i) => ({ id: i }))
}
 
export default async function sitemap({
  id,
}: {
  id: number
}): Promise<MetadataRoute.Sitemap> {
  const baseUrl = 'https://strivelab.pl'
  const limit = 50000
  const offset = id * limit
 
  const posts = await db.post.findMany({
    where: { published: true },
    select: { slug: true, updatedAt: true },
    skip: offset,
    take: limit,
  })
 
  return posts.map((post) => ({
    url: `${baseUrl}/blog/${post.slug}`,
    lastModified: post.updatedAt,
  }))
}

Next.js automatycznie generuje indeks sitemaps: /sitemap.xml → zawiera linki do /sitemap/0.xml, /sitemap/1.xml itd.

Natywny robots.txt

Code
// app/robots.ts
import type { MetadataRoute } from 'next'
 
export default function robots(): MetadataRoute.Robots {
  const baseUrl = 'https://strivelab.pl'
 
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/api/', '/admin/', '/dashboard/'],
      },
      {
        userAgent: 'GPTBot',
        disallow: '/', // Prośba o brak crawlowania dla bota respektującego robots.txt
      },
    ],
    sitemap: `${baseUrl}/sitemap.xml`,
  }
}

Wynikowy /robots.txt:

Code
User-agent: *
Allow: /
Disallow: /api/
Disallow: /admin/
Disallow: /dashboard/

User-agent: GPTBot
Disallow: /

Sitemap: https://strivelab.pl/sitemap.xml

next-sitemap — kiedy natywne API nie wystarczy

Biblioteka next-sitemap oferuje dodatkowe funkcje, czyli automatyczne wykrywanie statycznie wygenerowanych stron po buildzie, wielojęzyczne sitemaps z hreflang, server-side sitemap regeneration i konfigurację per-ścieżka.

Code
npm install next-sitemap
Code
// next-sitemap.config.js
module.exports = {
  siteUrl: 'https://strivelab.pl',
  generateRobotsTxt: true,
  changefreq: 'weekly',
  priority: 0.7,
  sitemapSize: 5000,
  exclude: ['/api/*', '/admin/*', '/dashboard/*'],
  robotsTxtOptions: {
    additionalSitemaps: ['https://strivelab.pl/server-sitemap.xml'],
    policies: [
      { userAgent: '*', allow: '/' },
      { userAgent: 'GPTBot', disallow: '/' },
    ],
  },
  transform: async (config, path) => {
    // Custom priorytety per ścieżka
    if (path === '/') return { loc: path, priority: 1.0, changefreq: 'daily' }
    if (path.startsWith('/blog/'))
      return { loc: path, priority: 0.7, changefreq: 'monthly' }
    return {
      loc: path,
      priority: config.priority,
      changefreq: config.changefreq,
    }
  },
}
Code
// package.json
{
  "scripts": {
    "postbuild": "next-sitemap"
  }
}

Natywne API vs next-sitemap — co wybrać?

FunkcjaNatywne APInext-sitemap
Automatyczne wykrywanie stronNieTak (po buildzie)
Dynamiczne strony z DBTak (ręczne)Tak (server sitemap)
Hreflang / alternatesTak (pole alternates)Tak (automatyczne)
Wielki serwis (> 50K stron)Tak (generateSitemaps)Tak (sitemapSize)
Konfiguracja per-ścieżkaRęczna (w kodzie)transform function
Dodatkowe zależnościZero1 pakiet
Koszt utrzymaniaMinimalny (część frameworka)Wyższy (zależność do aktualizacji)

Werdykt: Dla większości projektów, zwłaszcza tych z przewidywalną strukturą, natywne API Next.js jest w zupełności wystarczające, prostsze i nie wprowadza dodatkowych zależności. Sięgnij po next-sitemap tylko w wyjątkowych sytuacjach, gdy masz bardzo duży, nietypowy serwis, gdzie automatyczne wykrywanie stron i zaawansowane transformacje oszczędzą Ci mnóstwa pracy manualnej.

Weryfikacja w Google Search Console

Po wdrożeniu sitemap i robots.txt:

  1. Otwórz Google Search Console → Sitemaps
  2. Dodaj URL sitemap: https://strivelab.pl/sitemap.xml
  3. Sprawdź status — Google pokaże liczbę wykrytych i zaindeksowanych stron
  4. Monitoruj zakładkę „Strony" — szukaj błędów indeksowania

Podsumowanie

Dzięki natywnemu API w Next.js App Router, automatyczne generowanie plików sitemap.xml i robots.txt sprowadza się do kilku linii kodu w TypeScript. Wbudowane narzędzia (sitemap.ts, robots.ts) pokrywają 90% typowych scenariuszy bez potrzeby instalowania zewnętrznych bibliotek. Nawet przy ogromnych serwisach z dynamiczną treścią, funkcja generateSitemaps elegancko radzi sobie z automatycznym podziałem mapy na mniejsze pliki.

Cała filozofia sprowadza się do kilku dobrych praktyk. Pamiętaj, aby zawsze używać faktycznej daty modyfikacji w lastModified, poza tym unikaj używania new Date() dla stron statycznych, co zapobiega marnowaniu budżetu na ponowne crawlowanie niezmienionych treści. W robots.txt zablokuj ścieżki, które nie powinny być publiczne (np. /admin, /api), a po wdrożeniu koniecznie sprawdź status sitemapy w Google Search Console.

Najczęściej zadawane pytania

Czy sitemap jest wymagana do indeksowania?

Nie, Google może indeksować strony bez sitemap (przez linki wewnętrzne i zewnętrzne), ale sitemap przyspiesza odkrywanie nowych stron i informuje o zmianach. Jest to szczególnie w dużych serwisach lub przy braku zewnętrznych linków.

Jak często Google czyta sitemap?

Google sprawdza sitemap okresowo i nie ma jakiejś z góry ustalonej częstotliwości. Możesz wymusić ponowne sprawdzenie w Search Console (przycisk „Prześlij ponownie") i czekać na wynik.

Czy changefreq i priority wpływają na pozycje w Google?

Nie, Google wskazuje, że oba pola traktuje tylko jako wskazówki i w praktyce je ignoruje. Warto je wypełniać dla innych wyszukiwarek (Bing, Yandex), które mogą je respektować, ale nie mają żadnego wpływu na wyniki w Google - oczywiście, jeśli przyjmujemy, że wszystko co twierdzi Google odpowiada rzeczywistości.

Czy blokowanie botów AI w robots.txt jest skuteczne?

Działa dla botów, które respektują plik robots.txt. Zarówno GPTBot (OpenAI), jak i Google-Extended (Gemini) deklarują, że to robią. Jednak robots.txt to tylko prośba, a nie techniczna zapora — złośliwy lub źle skonfigurowany crawler może zignorować te dyrektywy. Jest to jednak obecnie standardowa metoda komunikowania swoich preferencji botom AI.

Pracuję z tym zawodowo.

Jeśli chcesz połączyć SEO, analitykę, Google Ads i warstwę techniczną strony w jeden sensowny system wzrostu, skontaktuj się ze mną. Pomagam układać wdrożenia, które nie kończą się na samym tagowaniu, ale wspierają widoczność, pomiar i konwersję.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab