gdamian.pl
← Wszystkie wpisy
14 min

Headless WordPress + Astro: jak połączyć CPT i ACF z szybkim frontem (konfiguracja krok po kroku) 

Damian GębalaAutor

Headless WordPress + Astro łączy się tak: WordPress zostaje zapleczem i trzyma treść, a Astro czyta ją raz podczas budowania strony i generuje z niej gotowe pliki HTML. Custom Post Types muszą mieć włączone show_in_rest, pola ACF trafiają do odpowiedzi REST automatycznie, a Astro odbiera je przez fetch() albo przez Content Layer. Dopiero potem zostaje pytanie o tryb renderowania, podgląd szkiców i SEO, bo to trzy miejsca, w których takie wdrożenie najczęściej się sypie.

Co właściwie łączy Headless WordPress z Astro

W klasycznym WordPressie każde wejście na stronę uruchamia PHP, który składa HTML, odpytuje bazę i ładuje wtyczki. W układzie headless dzielisz to na dwie części: WordPress zostaje wyłącznie magazynem treści, a front rysuje Astro. Kontent pobierany jest raz, podczas astro build, a wynikiem są statyczne pliki oddawane z CDN.

Źródła opisują to wprost: WordPress działa w tym układzie wyłącznie jako zaplecze, a Astro buduje front, czytając treść z wbudowanego REST API WordPressa albo z wtyczki GraphQL. Efekt to strony z zerową ilością JavaScriptu na starcie.

Praktyczna konsekwencja: hostujesz dwa środowiska, nie jedno. Jedno dla backendu WordPress, drugie dla frontu. Przy czym zaplecze wciąż potrzebuje kopii zapasowych, zabezpieczeń i kopii testowej, a front jako pliki statyczne trafia na niemal dowolny hosting - Netlify, Vercel, Cloudflare Pages. Taniej jest tylko wtedy, gdy zaplecze i tak masz.

Dla właściciela strony najważniejsza różnica jest praktyczna, nie architektoniczna: edytor nadal klika w WordPressie, a odwiedzający dostaje stronę, która nie czeka na PHP i bazę danych przy każdym wejściu. Ten sam backend można do tego dorzucić, o czym pisałem przy automatyzacjach, które oszczędzają godziny tygodniowo.

REST czy GraphQL - wybór przed pierwszą linią kodu

Wbudowane REST API działa od razu, bez żadnej wtyczki i bez konfiguracji. Każda instalacja WordPressa wystawia ścieżkę /wp-json/wp/v2/, która zwraca wpisy, strony, media, autorów i kategorie w JSON-ie. To wystarczy do prostego bloga, a wtyczki nie ma.

Jest jeden parametr, którego nie wolno pominąć: _embed=1. WordPress wtedy dokłada do tej samej odpowiedzi obraz wyróżniający, autora i kategorie. Bez tego Astro musi odpytywać API wielokrotnie, żeby dołożyć te dane ręcznie.

WPGraphQL daje coś innego: jeden endpoint, zwykle twojadomena.pl/graphql, z której możesz zapytać dokładnie o te pola, których potrzebujesz, razem z relacjami. Przy zagnieżdżonym modelu treści - autorzy pod postami, taksonomie powiązane z wpisami, pola ACF w relacji z innymi wpisami - zapytanie oszczędza lawinę dodatkowych odpytań, znaną jako problem N+1.

Reguła wyboru jest prosta. Płaski blog z wpisami i stronami zostaw na REST, bo zostawia prostszy stack. Gdy wejście zaczyna zagnieżdżać autorów, taksonomie, media i powiązane wpisy, przesiądź się na GraphQL. W praktyce, gdy zapytania w Astro rozrastają się w długie sekwencje wywołań, to sygnał, że warstwa GraphQL uprości cały potok.

Warto znać kontekst rynkowy: badanie Smashing Magazine wskazuje, że z WordPressa naprawdę korzystający z trybu headless, przez GraphQL albo REST API, to najwyżej cienka warstwa rynku. Rosło przyjęcie w dużych organizacjach, z 53 procent w 2019 roku do 64 procent w 2021 roku. Od 2024 roku GraphQL jest też częścią rdzenia WordPressa 6.5, więc wtyczki mogą deklarować między sobą zależności.

Krok 1. Custom Post Type, który faktycznie widać w REST

Tu leży najczęstsza przyczyna tego, że headless „nie działa”, choć WordPress wygląda poprawnie. Custom Post Type to nie tylko wpis z nową etykietą. Ma własne reguły ekspozycji w REST, własną bazę adresów, własne ustawienia przepisywania linków i własną rejestrację pól. Kolekcje w Astro, strony archiwum i trasy typu [slug].astro nie mogą zakładać domyślnych ustawień bloga.

Trzy przełączniki, które decydują o wszystkim

show_in_rest steruje tym, czy Astro w ogóle pobierze ten typ. Wyłączony - i zapytanie po prostu nie zwraca Twojego CPT, bez komunikatu błędu po stronie frontu. rest_base ustawia segment w adresie API, rewrite ustawia adres na stronie, a has_archive decyduje, czy powstaje strona archiwum. Te trzy wartości mogą wskazywać na zupełnie różne adresy, i to jest najczęstsze źródło pomyłek przy pierwszym uruchomieniu.

Do tego dochodzi publicly_queryable. Typ może wyglądać dobrze w panelu administracyjnym i nadal zawieść w układzie headless, gdy show_in_rest, public i publicly_queryable nie są ze sobą zszyte. Backend w takim układzie definiuje, co frontend może bezpiecznie zakładać.

Szczególny przypadek: has_archive = false. Bez strony archiwum nie zbudujesz listy wpisów danego typu, nawet jeśli pojedyncze wpisy pobierają się poprawnie.

add_action( 'init', function () {
	register_post_type( 'projekt', [
		'label'        => 'Projekty',
		'public'       => true,
		'show_in_rest' => true,
		'rest_base'    => 'projekty',
		'has_archive'  => true,
		'rewrite'      => [ 'slug' => 'projekty' ],
		'supports'     => [ 'title', 'editor', 'excerpt', 'thumbnail' ],
	] );
} );

To szkielet do dostosowania, nie fragment gotowy do skopiowania - taksonomie, pola i adresy zależą od Twojego projektu. Zasada zostaje jednak jedna: show_in_rest musi być włączone, najlepiej z jawnie ustalonym rest_base.

Zanim napiszesz szablon w Astro, sprawdź endpoint w przeglądarce. Otwórz https://twojadomena.pl/wp-json/wp/v2/projekty. Jeśli widzisz JSON, warstwa backendowa jest gotowa i dalsze problemy leżą po stronie frontu. Jeśli dostajesz pustą listę albo błąd, nie ma sensu dotykać Astro - najpierw popraw rejestrację typu.

Krok 2. Pola ACF w drodze do frontu

ACF pozwala zarejestrować własny typ posta i własne pola bezpośrednio w panelu, z ponad trzydziestoma typami pól do dyspozycji. Taksonomie też można prowadzić w ACF, zamiast wpisywać je ręcznie. To realna oszczędność czasu przy większych modelach treści.

Kolejna rzecz zaskakuje przy pierwszym podejściu: pola ACF trafiają do odpowiedzi REST automatycznie. Jedyny warunek dotyczy własnych typów pól, a nie gotowych z ACF. Klasa rozszerzająca acf_field musi mieć show_in_rest ustawione na true. Ustawienie tej właściwości na false wycina typ pola z REST API, nawet jeśli sam typ postu jest wystawiony poprawnie.

Wpisywanie tam własnych typów pól to jednak przypadek brzegowy. Znacznie częściej pole ginie z odpowiedzi z innego powodu. Wtedy zostaje filtr acf/rest/get_fields, który pozwala ręcznie dopchnąć typ pola do listy widocznych w API. Osobno działa filtr acf/rest/get_field_schema, sterujący schematem odpowiedzi.

add_filter( 'acf/rest/get_fields', function ( $fields ) {
	$fields[] = acf_get_field( 'field_hero_tresc' );
	return $fields;
} );

Kolejna sprawa to wspomniane wcześniej zapytania N+1. Gdy dla każdego wpisu osobno doczytujesz autorów, taksonomie i media, build się rozjeżdża. Jedno zapytanie GraphQL albo warstwa rozwijająca relacje przy pobieraniu z REST rozwiązuje to w jednym miejscu. Dla Astro ma to też drugi skutek: surowe odpowiedzi REST i GraphQL warto zatrzymać w warstwie mapującej, bo różne typy postów zwracają różne nazwy pól, inne struktury mediów i niespójne formaty dat. Dodanie nowego typu treści staje się wtedy zadaniem w maperze, a nie przepisywaniem szablonu.

Krok 3. Pobranie treści w Astro

Najprostsza droga to fetch wykonany w czasie budowania, przed zmienną środowiskową z adresem API. Tak wygląda pobranie pierwszej partii wpisów z osadzonymi danymi powiązanymi.

const WP_API_URL = import.meta.env.WP_API_URL;

const res = await fetch(
	`${WP_API_URL}/posts?_embed=1&per_page=100&page=1`
);
const posts = await res.json();

Przy REST z adresem zapisanym w .env jako WP_API_URL wywołanie wygląda tak samo, tyle że adres pochodzi ze zmiennej. Przy GraphQL klient jest osobną zależnością: npm install graphql-request graphql, a potem instancja GraphQLClient wskazana na endpoint. Zapytania pisz i testuj w edytorze GraphQL w panelu WPGraphQL, gdzie odpowiedź widzisz od razu, bez pisania kodu pobierającego.

Drugą drogą jest warstwa treści Astro. Wprowadzono ją w wersji 4.14.0 jako Content Layer API, a od Astro 5.0.0 jest już wbudowana. Zamiast ręcznie wywoływać fetch w każdym pliku, definiujesz kolekcję raz, a zapytanie do WordPressa trafia do funkcji load.

import { defineCollection } from 'astro:content';

export const collections = {
	projekty: defineCollection({
		type: 'content',
		load: async () => getAllProjectsFromWordPress(),
	}),
};

Osobno dopisz domenę obrazów w astro.config.mjs. Bez tego komponent obrazu nie pobierze plików z biblioteki mediów WordPressa. Sam komponent <Image /> zmniejsza rozmiar, kompresuje i konwertuje zdjęcia do WebP lub AVIF. Wymaga przy tym podania szerokości i wysokości, inaczej straci zysk w CLS, oraz atrybutu alt, nawet pustego.

Interakcje, których nie da się zrobić statycznie, wrzucasz w wyspy. Wyszpa to pojedynczy komponent, dla którego Astro wczytuje JavaScript; reszta strony zostaje statyczna. W ten sposób działają koszyk WooCommerce, wyszukiwarka czy sekcja komentarzy, bez zamiany całej strony w aplikację.

Krok 4. Static, SSR czy tryb mieszany

Wybór trybu sprowadza się do pytania, jak często zmienia się treść i ile tras obejmuje jedna zmiana. Przy rzadkich zmianach, gdy każda edycja dotyka kilku tras, wystarczy pełna statyczna generacja. Gdy jedna zmiana unieważnia dużą grupę tras albo potrzebujesz podglądów i treści zależnej od uprawnień, przechodzisz na renderowanie po stronie serwera. Istnieje też rozwiązanie pośrednie: większość tras budujesz statycznie, a pojedyncze pozostawiasz dynamiczne i renderujesz na żądanie.

Jedno ostrzeżenie o wersjach. Wiele artykułów o tym temacie podaje output: 'hybrid' jako sposób na mieszanie trybów. To nazwa z Astro 4. W Astro 5 domyślnym trybem jest static, a pojedynczą trasę włączasz przez export const prerender = false;. Numerki typu „pierwsze tysiąc wpisów statycznie, reszta na żądanie” pojawiają się w przykładach, ale to hipoteza redakcji, nie pomiar jakiejś realnej witryny.

Renderowanie po stronie serwera wymaga adaptera pod platformę, na której front trafi. Astro ma adaptery między innymi dla Cloudflare, Vercel i Netlify, które zabierają logikę SSR z Twoich rąk. Zanim wybierzesz hosting, sprawdź w jego dokumentacji, jaki adapter i jaki przepływ wdrożenia są dostępne.

Co do samych liczb, traktuj je jako widełki, nie jako obietnicę. Motyw WordPress z dużą liczbą wtyczek daje mobilny wynik Lighthouse w okolicach 50-80 punktów, a statyczny build Astro z tą samą treścią zwykle 90-100, bo nie wysyła renderującego JavaScriptu. Autorki jednego z przewodników zastrzegają wprost, że konkretne wartości zależą od motywu, wtyczek i hostingu. Warto własne liczby zmierzyć na obu wersjach tej samej strony, zamiast opierać się na cudzych.

Przy okazji: LCP ma próg 2,5 sekundy, a INP zastąpił wcześniejsze FID, przy czym Google wyznaczył na migrację termin 9 września 2024 roku. Te progi dotyczą obu wariantów, więc nie są argumentem za headless ani przeciw niemu.

Podgląd szkiców - pierwsza rzecz, która się psuie

Typowy objaw: redaktor klika „Podgląd” w WordPressie, a Astro odpowiada 404 albo wersją z wczoraj. Wygląda to na błąd w treści, ale nim nie jest. WordPress potrafi pokazać niezapisane rewizje w swoim uwierzytelnionym świecie, a statyczna trasa Astro nie ma skąd wziąć szkicu, którego nie ma w publicznym buildzie. Problem jest strukturalny, nie redakcyjny.

Zasada, która oszczędza najwięcej czasu: nigdy nie adresuj podglądu po slug. Slug zmienia się w trakcie edycji, przy szkicu może jeszcze nie istnieć, a po zmianie nazwy wskazuje inną wersję. Użyj niezmiennego identyfikatora, czyli identyfikatora rekordu w bazie, i adresu w rodzaju /preview?type=post&id=123.

Najczęściej stosowane rozwiązanie to własna trasa renderowana po stronie serwera. Strony publiczne zostają w pełni statyczne, a /preview działa dynamicznie, sprawdza token podglądu z WordPressa i pobiera właściwą rewizję. Do uwierzytelnienia służy hasło aplikacji WordPress. W praktyce oznacza to statyczne strony dla odwiedzających i renderowanie na serwerze dla redaktorów, bez zamiany całego modelu dostarczania.

Alternatywa jest tańsza, ale działa gorzej: po prostu wywołać build po zapisaniu. Wtedy podgląd wychodzi w pełni, lecz nie zobaczysz autosave ani ostatniej drobnej poprawki, a przy dużej witrynie build potrafi trwać chwilę. Osobne buildy podglądowe sprawdzają się przy przeglądaniu zmian w layoutzie, nie przy pojedynczych rewizjach.

Ten sam mechanizm, który odświeża podgląd, obsłuży też przebudowę po publikacji. WordPress wysyła webhook do hostingu frontu w momencie kliknięcia „Opublikuj”, co uruchamia automatyczny build w tle. Ten sam schemat webhooków opisuję przy własnym API i zabezpieczeniu HMAC.

SEO - co WordPress przestaje robić za Ciebie

W trybie headless kilka rzeczy przestaje działać automatycznie, choć wcześniej po prostu działały. Mapy witryny i kanały RSS trzeba wytworzyć i udostępnić z frontu, bo w rdzeniu WordPressa są, ale renderuje je motyw, którego już nie ma. Metadane SEO również nie przenoszą się same - musisz je pobrać z backendu i wstrzyknąć do <head> na froncie.

Spośród wtyczek SEO w headless działają te, które eksponują dane przez API. Dla Yoasta odpowiada za to dodatek WPGraphQL Yoast SEO Addon, który przenosi pola wtyczki, łącznie z adresem kanonicznym, do schematu GraphQL. Dla Rank Math analogicznie działa wtyczka wp-graphql-rank-math. Wybór wtyczki SEO nie jest więc dowolny, tylko musi umieć gadać z API.

Przy zmianie sluga dochodzą przekierowania, aktualizacja kanonicznego adresu i czyszczenie cache. Przełącznik archiwum albo nowa taksonomia potrafią wygenerować całą rodzinę tras, więc generator tras powinien czytać ustawienia rejestracji typu, a nie zakładać ich na sztywno.

Opisane wyżej decyzje o renderowaniu mają też skutek dla czytania strony przez wyszukiwarki i modele. Jeśli zależy Ci na tym, jak Twoja treść trafia do odpowiedzi generowanych, temat jest osobno.

Co headless psuje przy przeprowadzce

Nie wszystkie wtyczki przeniosą się bez śladu. Wtyczki działające w szablonach WordPressa, które wstrzykują PHP, własne CSS albo jQuery, przestaną działać, bo w układzie headless nie ma szablonu, do którego mogłyby się podpiąć. Ich odpowiedniki piszesz od nowa w projekcie Astro. To najczęstszy powód, dla którego budżet migracji rośnie poza samo przeniesienie treści.

Formularze z PHP też przestają działać, bo Astro oddaje statyczny HTML. FormularzComments trzeba zrobić jako wyspę i wysyłać dane z powrotem do API WordPressa albo przez mutację GraphQL.

Wtyczki działające po stronie backendu, takie jak te modyfikujące bazę, obsługujące metadane albo tworzące typy postów, działają pod warunkiem, że wystawisz ich dane w API. Nie każda wtyczka to robi, część trzeba rozszerzyć ręcznie. WooCommerce jest tu wyjątkiem, który działa: wystawia produkty przez REST API albo GraphQL, a Astro rysuje sklep jako statyczne strony. Jeśli Twoja integracja idzie dalej, zapisuję o tym, jak złożyć to w jeden przepływ.

Konfiguracja CORS na zapleczu też przestaje być opcjonalna - WordPress musi wiedzieć, z jakich adresów front może pytać o API. Jeśli jednak zapytania idą z serwera Astro, a nie z przeglądarki odwiedzającego, zapytania GraphQL i klucze API nie trafiają do kodu klienta. To realna zaleta w stosunku do rozwiązań, które wysyłają klucz na frontend.

Kiedy headless się nie opłaca

Najczęstszy mit to przekonanie, że statyczny front z definicji bije wszystko. Tymczasem przy dobrze dobranym hostingu, porządnej wtyczce cache i CDN zwykły WordPress da się rozpędzić tak, że różnica przestaje być mierzalna. Dodatkowe środowisko, osobny build i osobna ścieżka wdrożenia wtedy tylko dokładają koszt i awarie.

Headless zwraca koszt, gdy zmienia się model treści, pojawiają się relacje między wpisami, rośnie liczba typów postów albo zespół redakcyjny potrzebuje podglądu w trakcie pisania. Wtedy zysk jest realny, ale wynika z architektury, nie z punktów Lighthouse.

Jedyna kwota, którą znalazłem w źródłach, dotyczy hostingu: tani hosting plików statycznych frontu, od około 3 dolarów miesięcznie, plus osobno zaplecze WordPressa. To koszt utrzymania frontu, nie koszt wdrożenia i nie koszt pracy przy takim wdrożeniu. Nie podaję tu widełek za samo wdrożenie, bo nie mam podstaw, żeby je podać rzetelnie.

Praktyczny sprawdzian: jeśli nie masz po stronie treści więcej niż blog oparty na wpisach, zacznij od dobrego cache i CDN w klasycznym WordPressie. O tym, kiedy zmiana frontu naprawdę się opłaca, pisałem osobno przy tym, co daje realny zwrot.

Checklista przed wdrożeniem

  • Typ posta ma włączone show_in_rest, a rest_base jest ustalony świadomie i różni się od rewrite tylko wtedy, kiedy to cel.
  • Adres /wp-json/wp/v2/<typ> zwraca JSON w przeglądarce, bez logowania i bez wtyczki pośredniczącej.
  • Każde pole ACF, którego użyje front, widać w odpowiedzi, a jego typ ma show_in_rest ustawione na true.
  • Zapytania GraphQL albo REST są zapisane w jednym miejscu, a nie powielane po komponentach.
  • Tryb renderowania jest świadomy: statyczny, serwerowy albo mieszany, z decyzją zapisaną razem z powodem.
  • Podgląd szkiców działa po identyfikatorze rekordu, a nie po slugu, i jest uwierzytelniony.
  • Webhook po publikacji uruchamia build, a przekierowania po zmianie slugów są zapisane po stronie hostingu.
  • Metadane SEO i mapa witryny są generowane z frontu, a wtyczka SEO wystawia dane przez API.
  • Domena biblioteki mediów jest dopisana w astro.config.mjs.

Takie wdrożenie ma sens, gdy masz już model treści, który klasyczny WordPress zaczyna ograniczać. Jeśli dopiero planujesz stronę i nie masz pewności, którą drogą iść, napisz do mnie - odpowiadam zwykle tego samego dnia. Zanim zaczniesz kodować, warto zerknąć, co dokładnie robię przy takich wdrożeniach.

Robisz coś podobnego u siebie?Napisz do mnie

Masz proces, który zjada tydzień pracy? Odzyskajmy ten tydzień.

Napisz do mnie