Przejdź do treści

WPGraphQL i Next.js: Poradnik Headless WordPress

Połącz WordPressa z Next.js App Router przez API. Poznaj bezpieczną konfigurację produkcyjną bez problemów z ISR, trybem podglądu i webhookami.

Maciej Sala

Founder StriveLab

7 min czytaniaOpublikowano 11 kwietnia 2026 (Aktualizacja 17 lipca 2026)

WPGraphQL i Next.js App Router: zakres implementacji headless WordPress

Jeśli decyzja ze wstępu jeszcze nie zapadła, zacznij od Headless WordPress — kiedy ma sens, a kiedy nie. Artykuł jest technicznym tutorialem wdrożeniowym, który mówi jak połączyć WordPress, i Next.js w działającą architekturę. Headless WordPress to WordPress używany wyłącznie jako , z Next.js jako frontendem.

WordPress udostępnia treści przez ( lub GraphQL), a Next.js pobiera je, renderuje i serwuje użytkownikowi. W efekcie znany edytor WordPress dla klienta łączy się z wydajnością i Next.js na frontendzie.

Kiedy headless WordPress z Next.js ma sens?

  • Istniejący WordPress z setkami artykułów. Klient nie chce migrować treści, ale chce szybszy frontend.

  • Zespół redakcyjny zna WordPress i nie chce uczyć się Sanity/Strapi.

  • Zaawansowane pola ACF (Advanced Custom Fields). Pola własne, elastyczna zawartość (flexible content) oraz grupy pól.

  • WooCommerce. Koszyk i checkout w wariancie headless z Next.js, ale zarządzanie produktami w WordPressie.

Kiedy zamiast WordPressa wybrać dedykowany ?

  • Nowy projekt od zera. Sanity/Strapi mają lepsze doświadczenie deweloperskie i nie ciągniesz ze sobą PHP.

  • Nie potrzebujesz ekosystemu wtyczek WP. Mniej zależności oznacza mniej problemów.

  • Jeden deweloper. Utrzymanie WordPress i Next.js to dwa systemy do zarządzania.

Konfiguracja WPGraphQL w WordPressie i Next.js

Code
# W WordPress — zainstaluj wtyczki:
# 1. WPGraphQL (wp-graphql)
# 2. WPGraphQL for ACF (opcjonalnie, jeśli używasz ACF)

Po instalacji WPGraphQL: endpoint dostępny na https://twoj-wordpress.pl/graphql.

Klient GraphQL w Next.js App Router

Code
// lib/wordpress.ts
import 'server-only'
 
const WORDPRESS_API_URL = process.env.WORDPRESS_API_URL!
 
interface GraphQLResponse<T> {
  data?: T
  errors?: { message: string; path?: (string | number)[] }[]
}
 
export async function fetchGraphQL<T>(
  query: string,
  variables?: Record<string, unknown>,
  options: { tags?: string[]; revalidate?: number; authToken?: string } = {},
): Promise<T> {
  const res = await fetch(WORDPRESS_API_URL, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.authToken && {
        Authorization: `Bearer ${options.authToken}`,
      }),
    },
    body: JSON.stringify({ query, variables }),
    next: {
      revalidate: options.revalidate ?? 3600,
      tags: options.tags,
    },
  })
 
  if (!res.ok) {
    throw new Error(`WordPress GraphQL HTTP ${res.status}`)
  }
 
  const json = (await res.json()) as GraphQLResponse<T>
 
  // GraphQL może zwrócić HTTP 200 razem z errors i częściowym data.
  if (json.errors?.length || json.data === undefined) {
    throw new Error(
      json.errors?.map((error) => error.message).join(', ') ||
        'WordPress GraphQL response has no data',
    )
  }
 
  return json.data
}

Unikaj dołączania tokenów Bearer bez wyraźnej potrzeby. Dane publiczne pobieraj anonimowo, a autoryzację stosuj wyłącznie w trybie podglądu. Pamiętaj, że WPGraphQL nie obsługuje natywnie, więc wymaga to dodatkowych wtyczek, takich jak WPGraphQL JWT Authentication lub WPGraphQL Headless Login. W komunikacji serwer-serwer możesz rozważyć użycie WordPress Application Passwords z uwierzytelnianiem Basic Auth, pamiętając o specyficznych ograniczeniach tego rozwiązania. Klucze dostępowe przechowuj wyłącznie w module server-only, dbaj o ich regularną rotację i nadawaj przypisanemu kontu minimalne uprawnienia, tylko takie, które są niezbędne do odczytu szkiców.

Typ generyczny T nie sprawdza odpowiedzi w czasie działania aplikacji. W większym projekcie generuj typy na podstawie schematu i dokumentów GraphQL (GraphQL Code Generator) oraz waliduj krytyczne granice, szczególnie gdy aktualizacja wtyczki może zmienić schemat. W CI uruchamiaj kontrolę dokumentów względem schematu ze środowiska staging przed wdrożeniem WordPressa.

Zapytania GraphQL do treści WordPressa

Code
// lib/queries/posts.ts
import { fetchGraphQL } from '../wordpress'
import { cache } from 'react'
 
interface WPPost {
  id: string
  title: string
  slug: string
  excerpt: string
  date: string
  modified: string
  content: string
  featuredImage: {
    node: {
      sourceUrl: string
      altText: string
      mediaDetails: { width: number | null; height: number | null } | null
    }
  } | null
  categories: {
    nodes: { name: string; slug: string }[]
  }
}
 
export async function getPosts(first = 20): Promise<WPPost[]> {
  const data = await fetchGraphQL<{ posts: { nodes: WPPost[] } }>(
    `
    query GetPosts($first: Int!) {
      posts(first: $first, where: { status: PUBLISH }) {
        nodes {
          id
          title
          slug
          excerpt
          date
          modified
          featuredImage {
            node {
              sourceUrl
              altText
              mediaDetails {
                width
                height
              }
            }
          }
          categories {
            nodes {
              name
              slug
            }
          }
        }
      }
    }
  `,
    { first },
    { tags: ['wp-posts'] },
  )
 
  return data.posts.nodes
}
 
const getPostBySlugUncached = async (slug: string): Promise<WPPost | null> => {
  const data = await fetchGraphQL<{ post: WPPost | null }>(
    `
    query GetPost($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        id
        title
        slug
        excerpt
        content
        date
        modified
        featuredImage {
          node {
            sourceUrl
            altText
            mediaDetails {
              width
              height
            }
          }
        }
        categories {
          nodes {
            name
            slug
          }
        }
      }
    }
  `,
    { slug },
    { tags: ['wp-posts', `wp-post-${slug}`] },
  )
 
  return data.post
}
 
// Deduplikacja tego samego odczytu między generateMetadata i stroną.
export const getPostBySlug = cache(getPostBySlugUncached)
 
export async function getAllPostSlugs(): Promise<string[]> {
  const slugs: string[] = []
  let after: string | null = null
 
  do {
    const data: {
      posts: {
        nodes: { slug: string }[]
        pageInfo: { hasNextPage: boolean; endCursor: string | null }
      }
    } = await fetchGraphQL(
      `
      query GetAllSlugs($after: String) {
        posts(first: 100, after: $after, where: { status: PUBLISH }) {
          nodes { slug }
          pageInfo {
            hasNextPage
            endCursor
          }
        }
      }
    `,
      { after },
      { tags: ['wp-posts'] },
    )
 
    slugs.push(...data.posts.nodes.map((post) => post.slug))
    after = data.posts.pageInfo.hasNextPage
      ? data.posts.pageInfo.endCursor
      : null
  } while (after)
 
  return slugs
}

WPGraphQL używa paginacji Relay opartej na kursorach. first: 1000 nie jest bezpiecznym skrótem: instalacja może ograniczyć maksymalny rozmiar strony, a jedno wielkie zapytanie obciąża PHP i bazę. Dla listy użytkowej pobieraj 10–100 elementów i wystaw „Następna strona” lub infinite scroll. Przy generateStaticParams rozważ pre-render tylko najpopularniejszych wpisów, a resztę generuj na żądanie — pełne przejście przez tysiące slugów wydłuża build i uzależnia wdrożenie od dostępności WordPressa.

Pola seo nie należą do WPGraphQL core. Jeśli chcesz pobierać metadane Yoast albo Rank Math, potrzebujesz kompatybilnego rozszerzenia, a nazwy pól musisz potwierdzić w swoim schemacie. Nie deklaruj ich wyłącznie w interfejsie TypeScript, bo typ nie sprawi, że serwer GraphQL zacznie je zwracać.

Sanityzacja HTML z WordPressa

WPGraphQL zwraca content i excerpt jako wyrenderowany HTML. WordPress filtruje treść zależnie od uprawnień autora, ale administrator może mieć unfiltered_html, wtyczka może dołożyć niebezpieczny markup, a przejęty CMS staje się źródłem . Przy dangerouslySetInnerHTML ustaw własną granicę zaufania i listę dozwolonych elementów.

Code
yarn add sanitize-html
yarn add --dev @types/sanitize-html
Code
// lib/sanitize-wordpress-html.ts
import 'server-only'
import sanitizeHtml from 'sanitize-html'
 
export function sanitizeWordPressHtml(html: string) {
  return sanitizeHtml(html, {
    allowedTags: sanitizeHtml.defaults.allowedTags.concat([
      'img',
      'figure',
      'figcaption',
    ]),
    allowedAttributes: {
      ...sanitizeHtml.defaults.allowedAttributes,
      img: ['src', 'alt', 'width', 'height', 'loading'],
    },
    allowedSchemes: ['http', 'https', 'mailto'],
  })
}
 
export function wordpressText(html: string) {
  return sanitizeHtml(html, { allowedTags: [], allowedAttributes: {} }).trim()
}

Listę dozwolonych elementów dopasuj do bloków Gutenberga faktycznie używanych w projekcie. Osadzenia iframe, formularze, style inline i skrypty wymagają osobnej decyzji — nie dopuszczaj ich globalnie tylko dlatego, że jeden wpis ich potrzebuje. Dołóż restrykcyjne jako drugą warstwę ochrony.

Strony w Next.js renderowane z WPGraphQL

Code
// app/blog/page.tsx
import { getPosts } from '@/lib/queries/posts'
import Image from 'next/image'
import Link from 'next/link'
import { sanitizeWordPressHtml } from '@/lib/sanitize-wordpress-html'
 
export const revalidate = 3600
 
export default async function BlogPage() {
  const posts = await getPosts()
 
  return (
    <main className="mx-auto max-w-4xl py-12">
      <h1 className="mb-8 text-3xl font-bold">Blog</h1>
      <div className="space-y-8">
        {posts.map((post) => (
          <Link
            key={post.id}
            href={`/blog/${post.slug}`}
            className="group block"
          >
            <article className="flex gap-6">
              {post.featuredImage && (
                <Image
                  src={post.featuredImage.node.sourceUrl}
                  alt={post.featuredImage.node.altText || post.title}
                  width={300}
                  height={200}
                  sizes="(max-width: 768px) 100vw, 300px"
                  className="rounded-lg object-cover"
                />
              )}
              <div>
                <h2 className="text-xl font-semibold group-hover:text-blue-600">
                  {post.title}
                </h2>
                <div
                  className="mt-2 line-clamp-2 text-gray-600"
                  dangerouslySetInnerHTML={{
                    __html: sanitizeWordPressHtml(post.excerpt),
                  }}
                />
                <time className="mt-2 block text-sm text-gray-400">
                  {new Date(post.date).toLocaleDateString('pl-PL')}
                </time>
              </div>
            </article>
          </Link>
        ))}
      </div>
    </main>
  )
}
Code
// app/blog/[slug]/page.tsx
import { getPostBySlug, getAllPostSlugs } from '@/lib/queries/posts'
import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import {
  sanitizeWordPressHtml,
  wordpressText,
} from '@/lib/sanitize-wordpress-html'
 
export const revalidate = 3600
 
export async function generateStaticParams() {
  const slugs = await getAllPostSlugs()
  return slugs.map((slug) => ({ slug }))
}
 
export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const post = await getPostBySlug(slug)
  if (!post) return {}
 
  const description = wordpressText(post.excerpt).slice(0, 160)
 
  return {
    title: post.title,
    description,
    alternates: { canonical: `/blog/${post.slug}` },
    openGraph: {
      type: 'article',
      title: post.title,
      description,
      publishedTime: post.date,
      modifiedTime: post.modified,
      images: post.featuredImage
        ? [post.featuredImage.node.sourceUrl]
        : undefined,
    },
  }
}
 
export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await getPostBySlug(slug)
  if (!post) notFound()
 
  return (
    <article className="prose prose-lg mx-auto max-w-3xl py-12">
      <h1>{post.title}</h1>
      <time className="text-gray-400">
        {new Date(post.date).toLocaleDateString('pl-PL')}
      </time>
      <div
        dangerouslySetInnerHTML={{
          __html: sanitizeWordPressHtml(post.content),
        }}
      />
    </article>
  )
}

Relatywny adres kanoniczny wymaga poprawnego metadataBase w głównym layoucie. W sytuacji, w której korzystasz z rozszerzenia SEO WordPressa, ustal jedno źródło prawdy dla tytułu, adresu kanonicznego, noindex i Open Graph. Nie mieszaj części pól z wtyczki z innymi, generowanymi niezależnie w Next.js. Pamiętaj też, by dla danych strukturalnych Article serializować bezpiecznie JSON (co najmniej zamień < na \u003c) i nie kopiuj w ciemno JSON-LD wygenerowanego dla starego frontendu WordPressa.

Powyższy wariant używa modelu cache bez cacheComponents, zgodnego z next.revalidate na fetch i segmentowym export const revalidate. Jeśli w Next.js 16 włączysz cacheComponents: true, konfiguracje segmentu takie jak revalidate są zastępowane przez jawne zakresy 'use cache', cacheLife i cacheTag. Wybór powinien być zapisany w next.config.ts i testowany po buildzie produkcyjnym.

Funkcja generateStaticParams nie ogranicza listy dostępnych adresów, dopóki nie ustawisz dynamicParams = false. W przypadku bloga zazwyczaj pozwala się na generowanie brakujących wpisów „na żądanie”. Pamiętaj jednak, że przy ustawieniu dynamicParams = false, nowy artykuł nie zadziała od razu po unieważnieniu cache’u przez webhook, bo będzie musiał zostać uwzględniony w kolejnym buildzie aplikacji.

Bezpieczny podgląd szkiców przez Next.js Draft Mode

Endpoint podglądu powinien dostać podpisany sekret i stabilny identyfikator wpisu, samodzielnie pobrać szkic z WPGraphQL oraz przekierować wyłącznie na slug zwrócony przez CMS. Nie przekierowuj wprost do parametru redirect lub slug z query string — powstaje open redirect albo możliwość wejścia na dowolną trasę.

Code
// app/api/draft/route.ts
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const id = searchParams.get('id')
 
  if (secret !== process.env.DRAFT_SECRET || !id) {
    return new Response('Invalid preview request', { status: 401 })
  }
 
  // Funkcja używa osobnego, serwerowego tokenu i cache: 'no-store'.
  // Sposób pobrania rewizji zależy od schematu oraz rozszerzeń WPGraphQL.
  const post = await getAuthenticatedPreviewPost(id)
  if (!post?.slug) return new Response('Draft not found', { status: 404 })
 
  const draft = await draftMode()
  draft.enable()
  redirect(`/blog/${encodeURIComponent(post.slug)}`)
}

Na stronie wpisu sprawdzaj status za pomocą await draftMode(). Jeśli podgląd jest aktywny, wywołaj uwierzytelnioną funkcję getAuthenticatedPreviewPost z opcją cache: 'no-store'. Dla standardowego ruchu korzystaj z cachowanego getPostBySlug. Pamiętaj, aby nigdy nie umieszczać tokenu WordPressa w zmiennych środowiskowych z prefiksem NEXT_PUBLIC_, komponentach klienckich ani w adresie URL podglądu. Stwórz endpoint wyłączający tryb podglądu i ustaw prefetch={false} dla linku wyjścia, aby uniknąć przypadkowego zaciągnięcia zawartości przez przeglądarkę.

Pamiętaj, że szkice mogą zawierać poufne informacje. Konto techniczne powinno mieć nadane minimalne uprawnienia (capabilities), a linki do podglądu powinny być podpisane czasowo lub mieć krótki okres ważności. Warto również logować próby dostępu i ograniczyć możliwość podglądu wyłącznie do zalogowanych redaktorów.

ISR na żądanie z webhookami WordPressa w Next.js

Code
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { createHmac, timingSafeEqual } from 'node:crypto'
import { z } from 'zod'
 
const webhookSchema = z.object({
  event_id: z.string().min(1),
  post_type: z.literal('post'),
  slug: z.string().min(1).optional(),
  previous_slug: z.string().min(1).optional(),
  category_slugs: z.array(z.string().min(1)).default([]),
})
 
function validSignature(rawBody: string, timestamp: string, signature: string) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!Number.isFinite(age) || age > 300) return false
 
  const expected = createHmac('sha256', process.env.REVALIDATION_SECRET!)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex')
  const received = signature.replace(/^sha256=/, '')
 
  if (received.length !== expected.length) return false
  return timingSafeEqual(Buffer.from(received), Buffer.from(expected))
}
 
export async function POST(req: Request) {
  const rawBody = await req.text()
  const timestamp = req.headers.get('x-wp-timestamp') ?? ''
  const signature = req.headers.get('x-wp-signature') ?? ''
 
  if (!validSignature(rawBody, timestamp, signature)) {
    return Response.json({ error: 'Invalid signature' }, { status: 401 })
  }
 
  let input: unknown
  try {
    input = JSON.parse(rawBody)
  } catch {
    return Response.json({ error: 'Invalid JSON' }, { status: 400 })
  }
 
  const parsed = webhookSchema.safeParse(input)
  if (!parsed.success) {
    return Response.json({ error: 'Invalid payload' }, { status: 422 })
  }
 
  const { event_id, slug, previous_slug, category_slugs } = parsed.data
  if (!(await claimWebhookEvent(event_id))) {
    return Response.json({ revalidated: false, duplicate: true })
  }
 
  try {
    revalidateTag('wp-posts', 'max')
    revalidatePath('/blog')
 
    const affectedSlugs = [slug, previous_slug].filter(
      (value): value is string => Boolean(value),
    )
    for (const affectedSlug of new Set(affectedSlugs)) {
      revalidateTag(`wp-post-${affectedSlug}`, 'max')
      revalidatePath(`/blog/${affectedSlug}`)
    }
 
    for (const category of category_slugs) {
      revalidateTag(`wp-category-${category}`, 'max')
      revalidatePath(`/blog/kategoria/${category}`)
    }
 
    await markWebhookCompleted(event_id)
    return Response.json({ revalidated: true })
  } catch {
    await markWebhookFailed(event_id)
    return Response.json({ error: 'Revalidation failed' }, { status: 500 })
  }
}

W WordPressie konfiguruj wysyłkę webhooków tylko dla kluczowych zmian statusu lub treści: odfiltruj automatyczne zapisy (autosave), rewizje oraz typy wpisów, które nie są wykorzystywane na frontendzie. Podpis HMAC obliczaj w oparciu o identyczne, surowe ciało żądania (raw body) oraz znacznik czasu. Dla event_id stosuj unikalne indeksy, a w środowisku produkcyjnym zaprojektuj mechanizm, który oznaczy nieudane próby obsługi jako gotowe do ponowienia, zamiast trwale blokować ponowienia.

revalidateTag(..., 'max') stosuje stale-while-revalidate, więc pierwszy odczyt może jeszcze dostać starą wartość. revalidatePath unieważnia wynik konkretnej trasy i pomaga przy publikacji lub wycofaniu wpisu. W sytuacji wymagania natychmiastowej widoczności zmiany, przetestuj strategię wygaszania dla używanego modelu cache.

Konfiguracja next/image dla mediów z WordPressa

Code
// next.config.ts
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'twoj-wordpress.pl',
        pathname: '/wp-content/uploads/**',
      },
      // Jeśli używasz CDN, dodaj jego konkretny hostname i pathname osobno.
    ],
  },
}

Ogranicz remotePatterns do rzeczywistej ścieżki uploadów i konkretnych hostów . Szeroki wzorzec pozwala używać optymalizatora obrazów jako proxy dla większej liczby zasobów, niż potrzebujesz. W sytuacji, w której WordPress zwraca adresy po migracji domeny albo CDN przepisuje hostname, znormalizuj URL w jednej funkcji i przetestuj oba środowiska. Dla SVG zachowaj szczególną ostrożność i nie włączaj dangerouslyAllowSVG bez odpowiedniej polityki CSP i Content-Disposition.

Zapytuj o mediaDetails.width i height albo utrzymuj znane proporcje wariantu obrazu. Atrybut sizes jest potrzebny przy responsywnym layoucie, aby przeglądarka nie pobierała zbyt dużego pliku. next/image nie naprawi oryginału o wadze 15 MB ani braku właściwych wariantów po stronie biblioteki mediów.

Bezpieczeństwo i utrzymanie WPGraphQL na produkcji

Headless nie usuwa obowiązków WordPressa. Endpoint /graphql nadal uruchamia PHP i zapytania do bazy, a panel administracyjny pozostaje powierzchnią ataku. Aktualizuj WordPress, WPGraphQL i rozszerzenia, usuwaj nieużywane wtyczki, rób testowane backupy oraz monitoruj błędy i czas odpowiedzi. Oddziel staging od produkcji i sprawdzaj kompatybilność schematu przed aktualizacją.

Stosuj rozsądne limity paginacji, głębokości lub złożoności zapytań, limitowanie ruchu na odwrotnym proxy oraz trwały object cache, jeśli obciążenie tego wymaga. WPGraphQL Smart Cache może pomóc, ale nie zastępuje pomiaru. Zapytania GraphQL wysyłane metodą POST nie korzystają automatycznie z typowego cache CDN tak jak stabilne GET; jeśli publiczny ruch trafia bezpośrednio do WordPressa, rozważ persisted/safelisted queries albo warstwę cache świadomą GraphQL.

Nie wystawiaj w schemacie pól na zapas, bo WPGraphQL respektuje wiele uprawnień WordPressa, ale każde rozszerzenie może dodawać własne typy i resolvery. Przetestuj schemat jako użytkownik anonimowy, redaktor i konto podglądu, a introspekcję oraz IDE produkcyjne skonfiguruj zgodnie z przyjętym modelem zagrożeń.

Elastyczne i wydajne narzędzia dla biznesu, które dotrzymają kroku Twojemu rozwojowi.
Next.js

Często zadawane pytania

WPGraphQL czy REST API — co wybrać do headless WordPress?

WPGraphQL daje typowany schemat, relacje i precyzyjny wybór pól, co pomaga przy złożonych modelach treści. REST jest prostszy operacyjnie, działa bez dodatkowej wtyczki i łatwiej korzysta z cache HTTP/CDN. GraphQL nie gwarantuje mniejszej ani szybszej odpowiedzi, bo nieprawidłowo ograniczone zapytanie może być bardziej kosztowne niż REST. Wybór zależy od modelu i infrastruktury.

Czy muszę utrzymywać dwa serwery?

Utrzymujesz dwa środowiska uruchomieniowe: WordPress z bazą oraz frontend Next.js, choć mogą działać u jednego dostawcy. WordPress nadal obsługuje GraphQL, redakcję, media i webhooki, więc jego wydajność, backupy, aktualizacje i bezpieczeństwo bezpośrednio wpływają na proces budowania aplikacji oraz świeżość frontendu. Tani hosting współdzielony nie zawsze wystarczy.

Jak obsłużyć podgląd szkicu (preview) z WordPressa?

Draft Mode w Next.js tylko omija statyczny cache, nie daje dostępu do prywatnych danych WordPressa. Potrzebujesz podpisanego URL-a podglądu, uwierzytelnionego zapytania WPGraphQL z kontem o minimalnych uprawnieniach oraz obsługi rewizji lub szkicu. Integrację można napisać samodzielnie albo użyć utrzymywanego rozwiązania zgodnego z wybranym mechanizmem uwierzytelniania.

Dlaczego next/image nie ładuje zdjęć z WordPressa?

Domyślnie next/image optymalizuje tylko obrazy z dozwolonych hostów, więc dodaj host swojego WordPressa do images.remotePatterns w next.config. W innym wypadku Next.js odrzuci zewnętrzne URL-e z biblioteki mediów.

Jak odświeżać front po publikacji w WordPressie?

Przez podpisany webhook reagujący na zmianę statusu wpisu. Endpoint w Next.js unieważnia tag kolekcji, tag wpisu oraz ścieżki nowego i starego sluga. Odfiltruj autosave i rewizje, waliduj ciało żądania, chroń się przed ponowieniem starego żądania i ponawiaj nieudane dostarczenia.

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 rozwijam własne projekty.

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

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Headless WordPress z Next.js — kiedy ma sens, a kiedy nie

Headless WordPress brzmi jak przepis na nowoczesną architekturę: WordPress do zarządzania treścią, Next.js do frontendu, lepsza wydajność i więcej kontroli. Problem w tym, że ten układ potrafi być świetnym wyborem albo kosztownym przerostem formy, zależnie od rodzaju projektu.

Maciej Sala

Maciej Sala

Founder StriveLab

Next.js czy WordPress w 2026 roku? Co wybrać

Z jednej strony mamy WordPressa — weterana, który wciąż napędza ponad 40% globalnego internetu. Z drugiej Next.js, flagowy framework wśród nowoczesnych technologii opartych na Reakcie. Cel obu narzędzi jest ten sam, by wypuścić stronę w świat. Różnią się jednak diametralnie drogą do tego celu, a wybór między nimi to kwestia tego, co konkretny projekt ma osiągnąć i kto będzie go utrzymywał.

Maciej Sala

Maciej Sala

Founder StriveLab

REST API WordPressa — integracja z React i Next.js

WordPress to nie tylko PHP i szablony, ponieważ od wersji 4.7 ma wbudowane REST API , dzięki któremu może funkcjonować jako headless CMS . Backend w WordPressie, frontend w React lub Next.js to w wielu projektach bardzo rozsądne rozwiązanie.

Maciej Sala

Maciej Sala

Founder StriveLab