Headless Wagtail: die API für ein bestehendes Projekt nachrüsten
Eine wachsende Zahl meiner Wagtail-Anfragen lautet: "Wir haben Wagtail, wollen aber ein React-/Next.js-Frontend davorsetzen." Die gute Nachricht: Wagtail bringt eine Headless-API mit. Hier zeige ich, wie man sie in ein bestehendes Projekt integriert.
Warum Headless?
Headless trennt Content-Verwaltung (Wagtail-Admin) vom Ausliefern (eigenes Frontend). Vorteile: freie Frontend-Wahl, mehrere Kanäle aus einer Quelle, moderne Entwickler-Tooling. Preis: mehr bewegliche Teile — Wagtails integrierte Templates entfallen.
Die API v2 aktivieren
# settings.py
INSTALLED_APPS += [
"wagtail.api.v2",
"rest_framework",
]# api.py im Projektordner
from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.images.api.v2.views import ImagesAPIViewSet
from wagtail.documents.api.v2.views import DocumentsAPIViewSet
api_router = WagtailAPIRouter("wagtailapi")
api_router.register_endpoint("pages", PagesAPIViewSet)
api_router.register_endpoint("images", ImagesAPIViewSet)
api_router.register_endpoint("documents", DocumentsAPIViewSet)# urls.py
from .api import api_router
urlpatterns = [
path("api/v2/", api_router.urls),
# ...
]Endpoints konfigurieren
Damit ein Seitentyp Felder über die API ausliefert, müssen diese in api_fields deklariert werden — ein bewusster Opt-in-Mechanismus:
from wagtail.api import APIField
class BlogPage(Page):
intro = models.TextField()
body = StreamField(...)
date = models.DateField()
api_fields = [
APIField("intro"),
APIField("body"),
APIField("date"),
]StreamField als JSON serialisieren
Der spannendste Teil bei Headless-Wagtail ist StreamField. Über die API liefert es strukturiertes JSON — jeder Block mit Typ und Wert:
// GET /api/v2/pages/12/
{
"id": 12,
"title": "Mein Beitrag",
"body": [
{"type": "heading", "value": "Überschrift", "id": "..."},
{"type": "paragraph", "value": "Text...
", "id": "..."},
{"type": "cta", "value": {"heading": "...", "button_url": "..."}, "id": "..."}
]
}Das Frontend rendert dann pro type die passende Komponente. Für komplexe Blöcke kann man die Serialisierung anpassen, indem man am Block get_api_representation überschreibt.
Bilder & Renditions über die API
from wagtail.images.api.fields import ImageRenditionField
class BlogPage(Page):
hero_image = models.ForeignKey("wagtailimages.Image", ...)
api_fields = [
APIField("hero_image"),
# fertige Rendition-URL direkt mitliefern:
APIField("hero_thumb", serializer=ImageRenditionField("fill-400x300", source="hero_image")),
]Anbindung ans Frontend
// Next.js: Seite per Slug laden
async function getPage(slug) {
const res = await fetch(
`https://cms.example.com/api/v2/pages/?slug=${slug}&fields=*`
);
const data = await res.json();
return data.items[0];
}
// StreamField-Blöcke auf Komponenten mappen
function StreamField({ blocks }) {
return blocks.map((b) => {
switch (b.type) {
case "heading": return {b.value} ;
case "paragraph": return ;
case "cta": return ;
default: return null;
}
});
}Tipp: Preview im Wagtail-Admin funktioniert bei Headless nicht out-of-the-box. Das Paket wagtail-headless-preview schließt diese Lücke, damit Redakteure weiter eine Vorschau haben.
Fazit
Ein bestehendes Wagtail Headless zu öffnen ist gut machbar: API v2 aktivieren, api_fields deklarieren, StreamField-JSON im Frontend auf Komponenten mappen. Der Aufwand steckt weniger in Wagtail als im Frontend und in Details wie Preview und Caching — genau die Stellen, an denen ich Kunden unterstütze.
