Headless WordPress z WPGraphQL i Next.js 14
Opublikowano: 10 kwietnia 2026 · Kategoria: WordPress
WordPress rządzi rynkiem CMS z dobrych powodów: intuicyjny edytor Gutenberg, tysiące pluginów, znany przez redaktorów. Ale klasyczny motyw WordPress ma ograniczenia wydajnościowe i technologiczne. Rozwiązanie: headless architecture — WordPress tylko jako backend API, a nowoczesny Next.js jako frontend. Redaktorzy pracują w znajomym panelu WP, deweloperzy piszą React i TypeScript, a użytkownicy dostają błyskawicznie szybką stronę generowaną statycznie przez ISR.
REST API vs GraphQL — porównanie
| Kryterium | WordPress REST API | WPGraphQL |
|---|---|---|
| Endpoint | /wp-json/wp/v2/posts | /graphql (jeden endpoint) |
| Selektywne pola | Ograniczone (_fields param) | Pelna kontrola (tylko to co prosisz) |
| Powiązane dane | Wiele requestów (posts + authors + tags) | Jeden request (wszystko naraz) |
| Typowanie | Brak (JSON bez schematu) | Silnie typowany schemat GraphQL |
| Custom Post Types | Automatyczne, ograniczone | register_graphql_object_type() |
| Cache | Łatwy (URL-based) | Persisted Queries lub APQ |
| Krzywa uczenia | Niska | Średnia (GraphQL query language) |
Instalacja WPGraphQL
WPGraphQL to plugin do WordPressa. Instalujesz go jak każdy inny plugin — przez panel admina lub WP-CLI.
# Przez WP-CLI (SSH na hosting/VPS)
wp plugin install wp-graphql --activate
# Lub przez panel WP: Pluginy > Dodaj nowy > szukaj "WPGraphQL" > Zainstaluj
# Sprawdz ze GraphQL endpoint dziala
curl -X POST https://twojadomena.pl/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ posts { nodes { id title } } }"}'
# GraphiQL IDE (w przegladarce — panel WP)
# WP Admin > GraphQL > GraphiQL IDE
# Interaktywne testowanie zapytan
# Przykładowe zapytanie GraphQL — pobierz posty z autorami i tagami
# query {
# posts(first: 10) {
# nodes {
# id
# title
# slug
# date
# excerpt
# content
# featuredImage {
# node {
# sourceUrl
# altText
# }
# }
# author {
# node {
# name
# avatar {
# url
# }
# }
# }
# tags {
# nodes {
# name
# slug
# }
# }
# }
# }
# } Next.js 14 — projekt i pobieranie danych
# Utwórz projekt Next.js
npx create-next-app@latest my-headless-wp \
--typescript --tailwind --eslint --app
cd my-headless-wp
# Zainstaluj klienta GraphQL
npm install graphql-request graphql
# .env.local
# WORDPRESS_API_URL=https://twojadomena.pl/graphql
# WORDPRESS_PREVIEW_SECRET=twoj-tajny-token-preview
# lib/graphql.ts — klient zapytan
# import { GraphQLClient } from 'graphql-request';
#
# export const client = new GraphQLClient(
# process.env.WORDPRESS_API_URL!
# );
#
# export async function getAllPosts() {
# const query = `
# query AllPosts {
# posts(first: 20, where: { status: PUBLISH }) {
# nodes {
# id
# slug
# title
# date
# excerpt(format: RENDERED)
# featuredImage {
# node { sourceUrl altText }
# }
# }
# }
# }
# `;
# const data = await client.request(query);
# return data.posts.nodes;
# }
#
# export async function getPostBySlug(slug: string) {
# const query = `
# query PostBySlug($slug: String!) {
# postBy(slug: $slug) {
# id title content date
# author { node { name } }
# tags { nodes { name slug } }
# }
# }
# `;
# const data = await client.request(query, { slug });
# return data.postBy;
# } App Router — strony z ISR
Next.js App Router pozwala na Incremental Static Regeneration przez pole revalidate. Strony są wstępnie generowane przy buildzie i odświeżane w tle co określony czas:
# app/page.tsx — strona glowna z lista postow (ISR co 60s)
# import { getAllPosts } from '@/lib/graphql';
#
# export const revalidate = 60; // ISR: odswiezaj co 60 sekund
#
# export default async function HomePage() {
# const posts = await getAllPosts();
#
# return (
# <main>
# <h1>Blog</h1>
# <ul>
# {posts.map((post) => (
# <li key={post.id}>
# <a href={`/posts/${post.slug}`}>{post.title}</a>
# </li>
# ))}
# </ul>
# </main>
# );
# }
# app/posts/[slug]/page.tsx — dynamiczna strona posta z ISR
# import { getPostBySlug, getAllPosts } from '@/lib/graphql';
# import { notFound } from 'next/navigation';
#
# export const revalidate = 3600; // ISR: co godzine
#
# // Generuj sciezki statycznie przy buildzie
# export async function generateStaticParams() {
# const posts = await getAllPosts();
# return posts.map((post) => ({ slug: post.slug }));
# }
#
# export default async function PostPage({ params }) {
# const post = await getPostBySlug(params.slug);
# if (!post) notFound();
#
# // Renderowanie treści z WordPress — pamiętaj o sanitizacji
# // (wp_kses_post() po stronie WP czyści HTML przed zapisem do bazy)
# return (
# <article>
# <h1>{post.title}</h1>
# <time>{new Date(post.date).toLocaleDateString('pl-PL')}</time>
# <div class="post-content" /> {/* wstaw tresc przez set innerHTML po sanitizacji */}
# </article>
# );
# } Webhook — natychmiastowe ISR revalidation
ISR odświeża strony co określony czas. Jeśli chcesz żeby strona odświeżyła się natychmiast po publikacji posta w WordPress, użyj mechanizmu On-Demand Revalidation:
# app/api/revalidate/route.ts — endpoint do rewalidacji
# import { revalidatePath } from 'next/cache';
# import { NextRequest } from 'next/server';
#
# export async function POST(req: NextRequest) {
# const secret = req.nextUrl.searchParams.get('secret');
# if (secret !== process.env.WORDPRESS_PREVIEW_SECRET) {
# return Response.json({ message: 'Invalid secret' }, { status: 401 });
# }
#
# const body = await req.json();
# const slug = body?.post?.post_name;
#
# if (slug) {
# revalidatePath(`/posts/${slug}`);
# }
# revalidatePath('/'); // strona glowna
#
# return Response.json({ revalidated: true, slug });
# }
# WordPress — dodaj w functions.php do motywu lub pluginie
# function send_nextjs_revalidation_webhook($post_id) {
# $post = get_post($post_id);
# $payload = json_encode(array('post' => array('post_name' => $post->post_name)));
# wp_remote_post(
# 'https://twoj-nextjs.pl/api/revalidate?secret=TWOJ_SECRET',
# array(
# 'body' => $payload,
# 'headers' => array('Content-Type' => 'application/json')
# )
# );
# }
# add_action('publish_post', 'send_nextjs_revalidation_webhook'); Deployment na VPS — Next.js z PM2
# Na VPS — instalacja Node.js LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -
sudo apt install nodejs -y
npm install -g pm2
# Build i uruchomienie
cd /var/www/my-headless-wp
npm install
npm run build
# PM2 ekosystem (ecosystem.config.js)
# module.exports = {
# apps: [{
# name: 'nextjs-blog',
# script: 'node_modules/.bin/next',
# args: 'start',
# env: {
# NODE_ENV: 'production',
# PORT: 3000,
# WORDPRESS_API_URL: 'https://wp.example.com/graphql'
# }
# }]
# };
pm2 start ecosystem.config.js
pm2 save
pm2 startup # autostart po restarcie serwera
# Nginx reverse proxy do Next.js
# server {
# listen 443 ssl;
# server_name blog.example.com;
# location / {
# proxy_pass http://localhost:3000;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection 'upgrade';
# proxy_set_header Host $host;
# proxy_cache_bypass $http_upgrade;
# }
# }
# Deploy bez downtime
git pull
npm ci
npm run build
pm2 reload nextjs-blog # zero-downtime reload