ISR na żądanie — jak odświeżać tylko te strony, które się zmieniły

Opublikowano
10 kwietnia 2026
Aktualizacja
30 czerwca 2026
Czas czytania
8 min czytania

Czym jest ISR i dlaczego ma znaczenie?

to mechanizm Next.js, który łączy zalety stron statycznych (szybkość, cache CDN) z możliwością aktualizacji treści bez pełnej przebudowy aplikacji.

W tradycyjnym podejściu statycznym (SSG) każda zmiana treści wymaga ponownego budowania całego serwisu, co w przypadku setek czy tysięcy stron oznacza minuty oczekiwania. ISR rozwiązuje ten problem na żądanie, pozwalając odświeżać pojedyncze strony w tle.

Dwa tryby rewalidacji

Rewalidacja czasowa (time-based)

Najprostszy tryb, czyli strona jest serwowana z pamięci podręcznej przez określony czas, po czym Next.js regeneruje ją w tle przy następnym żądaniu.

Code
// app/blog/[slug]/page.tsx
 
// Strona odświeża się co 3600 sekund (1 godzina)
export const revalidate = 3600
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetch(`https://cms.example.com/posts/${slug}`)
  const data = await post.json()
 
  return (
    <article>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.content }} />
    </article>
  )
}

Jak to działa krok po kroku:

  1. Podczas budowania — strona jest generowana i zapisywana w pamięci podręcznej
  2. Przez 3600 sekund — każde żądanie dostaje wersję z pamięci podręcznej (natychmiast)
  3. Po 3600 sekundach — pierwsze żądanie nadal dostaje starą wersję, ale w tle Next.js generuje nową
  4. Następne żądania — dostają już zaktualizowaną wersję

Problem z rewalidacją czasową: nie masz kontroli nad momentem aktualizacji. Jeśli zmienisz treść w CMS, użytkownicy mogą widzieć starą wersję przez cały czas rewalidacji.

Rewalidacja na żądanie

Rewalidacja na żądanie pozwala wymusić regenerację strony w dokładnym momencie, gdy zmienią się dane. Zamiast czekać — natychmiast unieważniasz pamięć podręczną.

Next.js App Router oferuje dwa mechanizmy:

revalidatePath() — unieważnianie po ścieżce

Code
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
 
export async function POST(request: NextRequest) {
  const { path, secret } = await request.json()
 
  // Weryfikacja tokena — bez tego każdy może rewalidować
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Nieprawidłowy token' }, { status: 401 })
  }
 
  try {
    revalidatePath(path)
    return NextResponse.json({ revalidated: true, path })
  } catch (error) {
    return NextResponse.json({ error: 'Rewalidacja nieudana' }, { status: 500 })
  }
}

Użycie:

Code
curl -X POST https://example.com/api/revalidate \
  -H "Content-Type: application/json" \
  -d '{"path": "/blog/moj-post", "secret": "tajny-token"}'

revalidatePath akceptuje różne formaty:

Code
// Rewalidacja konkretnej strony
revalidatePath('/blog/moj-post')
 
// Rewalidacja wszystkich stron w segmencie
revalidatePath('/blog', 'page')
 
// Rewalidacja layoutu i wszystkich stron pod nim
revalidatePath('/blog', 'layout')
 
// Rewalidacja całej aplikacji
revalidatePath('/', 'layout')

revalidateTag() — unieważnianie po tagu

Tagi pozwalają grupować dane i rewalidować wszystkie strony, które z nich korzystają — jednym wywołaniem.

Code
// app/blog/[slug]/page.tsx
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetch(`https://cms.example.com/posts/${slug}`, {
    next: { tags: [`post-${slug}`, 'all-posts'] },
  })
  const data = await post.json()
 
  return (
    <article>
      <h1>{data.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: data.content }} />
    </article>
  )
}
Code
// app/api/revalidate-tag/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
 
export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json()
 
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
 
  // W Next.js 16 revalidateTag wymaga drugiego argumentu (profilu).
  // 'max' daje semantykę stale-while-revalidate.
  revalidateTag(tag, 'max')
  return NextResponse.json({ revalidated: true, tag })
}

Przykłady tagowania:

Code
// Tag per zasób — rewalidacja jednego posta
fetch(url, { next: { tags: ['post-nextjs-15'] } })
revalidateTag('post-nextjs-15', 'max')
 
// Tag per typ — rewalidacja wszystkich postów
fetch(url, { next: { tags: ['all-posts'] } })
revalidateTag('all-posts', 'max')
 
// Tag per autor — rewalidacja postów jednego autora
fetch(url, { next: { tags: ['author-maciej'] } })
revalidateTag('author-maciej', 'max')

Integracja z CMS — webhooks

ISR na żądanie ma największy sens wtedy, gdy łączysz go z CMS-em. Po publikacji, edycji albo usunięciu treści CMS wysyła webhook do aplikacji, a endpoint API w Next.js unieważnia tylko te ścieżki lub tagi, których dotyczyła zmiana.

Przykład: webhook z headless CMS

Code
// app/api/cms-webhook/route.ts
import { createHmac } from 'crypto'
import { revalidateTag, revalidatePath } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
 
export async function POST(request: NextRequest) {
  // Weryfikacja podpisu webhooka
  const signature = request.headers.get('x-webhook-signature')
  const body = await request.text()
 
  if (!verifySignature(body, signature)) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 })
  }
 
  const payload = JSON.parse(body)
 
  switch (payload.event) {
    case 'entry.publish':
    case 'entry.update':
      // Rewalidacja konkretnego wpisu
      revalidateTag(`post-${payload.entry.slug}`, 'max')
      // Rewalidacja strony głównej i listy bloga
      revalidatePath('/')
      revalidatePath('/blog')
      break
 
    case 'entry.delete':
      revalidatePath('/blog')
      revalidatePath('/')
      break
 
    case 'media.upload':
      // Nowe media — rewalidacja galerii
      revalidateTag('gallery', 'max')
      break
  }
 
  return NextResponse.json({ revalidated: true })
}
 
function verifySignature(body: string, signature: string | null): boolean {
  if (!signature) return false
  const expected = createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(body)
    .digest('hex')
  return signature === expected
}

Konfiguracja w popularnych CMS-ach

Strapi — utwórz webhook dla publikacji, aktualizacji i usunięcia wpisu. W payloadzie potrzebujesz co najmniej typu zdarzenia, typu treści i identyfikatora albo sluga wpisu, żeby endpoint Next.js wiedział, czy odświeżyć pojedynczą stronę, listę bloga, kategorię czy stronę główną.

Sanity — najlepiej użyć webhooka z filtrem na typ dokumentu, np. tylko post, caseStudy albo page. Dzięki temu edycja autora, assetu albo ustawień globalnych nie musi zawsze czyścić całego cache; możesz osobno obsłużyć wpis, listę wpisów i elementy wspólne, takie jak nawigacja.

Contentful — skonfiguruj webhook dla konkretnych Content Types i eventów publikacji. Dla artykułu zwykle rewalidujesz stronę wpisu oraz listę bloga; dla kategorii albo autora także strony archiwów, które pokazują powiązane wpisy.

WordPress headless — możesz użyć gotowego pluginu od webhooków albo własnej akcji save_post. Własny hook daje największą kontrolę, bo możesz pominąć rewizje, autozapisy i typy wpisów, które nie mają publicznej strony w Next.js:

Code
// functions.php
add_action('save_post', function($post_id) {
    $slug = get_post_field('post_name', $post_id);
    wp_remote_post('https://example.com/api/cms-webhook', [
        'body' => json_encode([
            'event' => 'entry.update',
            'entry' => ['slug' => $slug],
        ]),
        'headers' => [
            'Content-Type' => 'application/json',
            'x-webhook-signature' => hash_hmac('sha256',
                json_encode(['event' => 'entry.update', 'entry' => ['slug' => $slug]]),
                WEBHOOK_SECRET
            ),
        ],
    ]);
});

Server Actions + rewalidacja

W projektach z backendem w tym samym repozytorium (bez zewnętrznego CMS) Server Actions to naturalny sposób na mutowanie danych i natychmiastową rewalidację:

Code
// app/admin/posts/actions.ts
'use server'
 
import { revalidatePath, revalidateTag } from 'next/cache'
import { db } from '@/lib/db'
 
export async function updatePost(formData: FormData) {
  const slug = formData.get('slug') as string
  const title = formData.get('title') as string
  const content = formData.get('content') as string
 
  await db.post.update({
    where: { slug },
    data: { title, content, updatedAt: new Date() },
  })
 
  // Rewalidacja strony posta i listy bloga
  revalidateTag(`post-${slug}`, 'max')
  revalidatePath('/blog')
}
Code
// app/admin/posts/[slug]/edit.tsx
import { updatePost } from '../actions'
 
export default function EditPost({ post }: { post: Post }) {
  return (
    <form action={updatePost}>
      <input type="hidden" name="slug" value={post.slug} />
      <input name="title" defaultValue={post.title} />
      <textarea name="content" defaultValue={post.content} />
      <button type="submit">Zapisz i opublikuj</button>
    </form>
  )
}

Strategia tagowania dla dużych serwisów

Dla serwisów z setkami stron przyjmij stały schemat tagowania:

Code
// lib/cache-tags.ts
export const cacheTags = {
  // Pojedyncze zasoby
  post: (slug: string) => `post-${slug}`,
  product: (id: string) => `product-${id}`,
  category: (slug: string) => `category-${slug}`,
 
  // Kolekcje
  allPosts: 'all-posts',
  allProducts: 'all-products',
 
  // Globalne
  navigation: 'navigation',
  footer: 'footer',
  settings: 'site-settings',
} as const
Code
// Użycie w fetchach
const posts = await fetch('https://api.example.com/posts', {
  next: { tags: [cacheTags.allPosts] },
})
 
const post = await fetch(`https://api.example.com/posts/${slug}`, {
  next: { tags: [cacheTags.post(slug), cacheTags.allPosts] },
})
 
const nav = await fetch('https://api.example.com/navigation', {
  next: { tags: [cacheTags.navigation] },
})

A co z Astro?

Tu trzeba być precyzyjnym, ponieważ Astro nie ma odpowiednika revalidatePath() i revalidateTag() na poziomie frameworka. Nie oznacza to, że Astro nie nadaje się do treści aktualizowanych z CMS-a, ale oznacza tylko, że problem rozwiązuje innym modelem.

W Next.js ISR jest częścią mechaniki frameworka i jego pamięci podręcznej, natomiast w Astro decyzję podejmujesz na poziomie sposobu renderowania oraz hostingu:

PotrzebaNext.jsAstro
Blog lub strona firmowa, aktualizacja kilka razy dziennieISR albo zwykłe budowanieSSG + webhook uruchamiający wdrożenie
Duży katalog, zmienia się pojedynczy produktrevalidatePath() / revalidateTag()SSR na żądanie + cache CDN albo platformowa invalidacja URL-i
Cena lub stan magazynowy musi być świeżyISR z krótkim fallbackiem albo SSRprerender = false, endpoint API albo Server Island
Treść statyczna, tylko jeden fragment dynamicznyCache Components / PPRServer Islands
Tysiące stron programmatic SEOISR + tagizależy od częstotliwości zmian: SSG albo SSR z cache CDN

Model 1: Astro SSG i webhook do wdrożenia

Najprostszy model Astro to statyczne budowanie, czyli strona generuje HTML podczas wdrożenia, a potem CDN serwuje gotowe pliki. Jeśli edytor zmieni artykuł w CMS-ie, webhook uruchamia nowe budowanie.

To nie jest częściowa przebudowa (partial rebuild) w sensie Next.js ISR. To pełne wdrożenie, ale przy blogu, dokumentacji, stronie usługowej albo landing page'ach często jest to wystarczające i prostsze operacyjnie. Nie utrzymujesz tagów rewalidacji, endpointów API do rewalidacji ani serwera regenerującego strony.

Przykład dynamicznej strony w Astro SSG:

Code
---
// src/pages/blog/[slug].astro
import { getPost, getPosts } from '../../lib/cms'
 
export async function getStaticPaths() {
  const posts = await getPosts()
 
  return posts.map((post) => ({
    params: { slug: post.slug },
  }))
}
 
const { slug } = Astro.params
const post = await getPost(slug!)
---
 
<article>
  <h1>{post.title}</h1>
  <Fragment set:html={post.content} />
</article>

Webhook z CMS-a nie uderza wtedy w /api/revalidate, tylko w hook wdrożeniowy platformy hostingowej. W praktyce wygląda to tak:

Code
CMS publish/update/delete
  -> webhook
  -> hook wdrożeniowy hostingu
  -> astro build
  -> nowa wersja statycznych plików na CDN

Ten model jest bardzo dobry, jeśli budowanie trwa kilkadziesiąt sekund albo kilka minut, a treść nie musi pojawić się w sekundę po kliknięciu „publish".

Model 2: Astro i renderowanie na żądanie

Jeśli dana trasa musi pobierać świeże dane przy żądaniu, Astro może renderować ją na żądanie. Projekt potrzebuje adaptera SSR, a trasa musi nie być prerenderowana:

Code
---
// src/pages/products/[slug].astro
export const prerender = false
 
const { slug } = Astro.params
const response = await fetch(`https://cms.example.com/products/${slug}`)
const product = await response.json()
---
 
<article>
  <h1>{product.name}</h1>
  <p>{product.price} PLN</p>
</article>

To jest inny kompromis niż ISR. Nie przebudowujesz statycznej strony w tle, ale renderujesz odpowiedź w warstwie runtime. Dzięki temu dane mogą być świeże, ale płacisz kosztem żądania serwerowego oraz zależności od adaptera: Node, Vercel, Netlify, Cloudflare, Deno albo innej platformy wspierającej SSR.

Model 3: Astro SSR + cache CDN

Najbliżej modelu ISR jesteś wtedy, gdy Astro renderuje stronę na żądanie, a odpowiedź zapisujesz w cache CDN przez nagłówki HTTP. Astro nie musi mieć własnego revalidateTag(), jeśli CDN potrafi serwować starą odpowiedź i odświeżać ją w tle.

Code
---
// src/pages/products/[slug].astro
export const prerender = false
 
Astro.response.headers.set(
  'Cache-Control',
  'public, s-maxage=300, stale-while-revalidate=3600'
)
 
const { slug } = Astro.params
const product = await fetch(`https://cms.example.com/products/${slug}`).then((res) =>
  res.json()
)
---
 
<article>
  <h1>{product.name}</h1>
  <p>{product.price} PLN</p>
</article>

W tym wariancie świeżość zależy od hostingu i CDN. Jedna platforma pozwoli ręcznie wyczyścić konkretny URL, inna będzie honorować stale-while-revalidate, a jeszcze inna wymaga własnego endpointu API lub integracji z API pamięci podręcznej. Dlatego w Astro nie mówisz „robię ISR", tylko: robię renderowanie na żądanie i definiuję politykę pamięci podręcznej na poziomie infrastruktury.

Model 4: Server Islands zamiast przebudowy całej strony

Bardzo często nie trzeba odświeżać całej strony, ponieważ jeśli statyczny artykuł ma tylko jeden dynamiczny fragment (cenę, dostępność, rekomendacje albo licznik) Astro Server Islands pozwalają zostawić resztę jako statyczny HTML, a dynamiczny fragment wyrenderować osobno.

Code
---
// src/pages/products/[slug].astro
import ProductAvailability from '../../components/ProductAvailability.astro'
 
const { slug } = Astro.params
const product = await getStaticProduct(slug!)
---
 
<article>
  <h1>{product.name}</h1>
  <p>{product.description}</p>
 
  <ProductAvailability productId={product.id} server:defer>
    <span slot="fallback">Sprawdzam dostępność...</span>
  </ProductAvailability>
</article>

To nie jest ISR, ale rozwiązuje bardzo podobny problem, jakim jest przebudowywanie całej strony tylko dlatego, że jeden fragment musi być świeży. Dla Astro to bardziej naturalny wzorzec niż przenoszenie całej trasy w dynamiczną warstwę runtime.

Kiedy wybrać Next.js ISR, a kiedy Astro?

Jeśli potrzebujesz natywnej rewalidacji pojedynczych ścieżek i grup danych, Next.js wygrywa. revalidatePath(), revalidateTag() i Server Actions tworzą spójny model dla dużych katalogów, paneli administracyjnych, e-commerce i aplikacji, w których publikacja lub mutacja danych ma natychmiast odświeżać właściwe URL-e.

Astro jest idealne, gdy większość strony jest treścią i nie potrzebuje warstwy runtime. Blog, dokumentacja, baza wiedzy, strona usługowa albo landing page zwykle lepiej znoszą prosty model: statyczne budowanie, webhook po publikacji i bardzo lekki HTML. Gdy potrzebujesz świeżych fragmentów, sięgasz po renderowanie na żądanie, cache CDN albo selektywnie użyte Server Islands.

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

Często zadawane pytania

Czy ISR działa na każdym hostingu?

Pełny ISR z rewalidacją na żądanie wymaga platformy obsługującej Next.js w trybie serwerowym — Vercel, Netlify, AWS Amplify, Coolify, samodzielny serwer Node.js. Statyczny eksport (output: export) nie wspiera ISR, bo nie ma serwera regenerującego strony.

Regeneracja odbywa się w tle — trwa tyle, ile pobranie danych i wyrenderowanie strony (zwykle 0,5–5 s). Użytkownik, który wywołał rewalidację, widzi jeszcze starą wersję; następne żądanie dostaje już nową.

Tak, ale ostrożnie. revalidateTag(all-posts, max) unieważni pamięć podręczną wszystkich stron z tym tagiem. Regeneracja następuje leniwie — każda strona budowana jest na nowo dopiero przy pierwszym żądaniu po unieważnieniu, więc nie ma jednoczesnego skoku obciążenia.

Technicznie nie, ale to krytyczne dla bezpieczeństwa. Bez tokena (albo podpisu webhooka) każdy mógłby wymusić regenerację Twoich stron — przy masowych żądaniach to wektor ataku obciążającego serwer. Zawsze weryfikuj sekret albo podpis HMAC.

revalidateTag wymaga teraz drugiego argumentu — profilu rewalidacji (np. max dla stale-while-revalidate). Forma jednoargumentowa jest przestarzała i daje błąd TypeScript. Dodatkowo doszedł updateTag() — wariant dla Server Actions z semantyką read-your-writes (natychmiastowy odczyt własnego zapisu).

Nie wprost. Astro nie ma frameworkowych funkcji revalidatePath() i revalidateTag(). Ten sam problem rozwiązuje inaczej: statyczne strony odświeżasz przez webhook uruchamiający wdrożenie, wybrane trasy możesz renderować na żądanie przez adapter i prerender = false, a przy SSR możesz oprzeć świeżość o Cache-Control oraz cache CDN danej platformy.

Astro jest lepsze, gdy większość serwisu to treść, która może być statyczna: blog, dokumentacja, strona firmowa, landing page'e. Wtedy webhook z CMS może po prostu uruchamiać przebudowę i wdrożenie, a użytkownicy dostają bardzo lekki HTML. Next.js ISR wygrywa przy dużych katalogach, cenach, stanach magazynowych i treściach, które muszą aktualizować pojedyncze URL-e bez pełnego budowania.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend 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 side-projecty.

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

Skontaktuj się ze mną

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów