Wagtail API for Headless Mode Setup
Wagtail API v2 is a built-in REST API for read-only access. For full-featured headless with mutations, use Strawberry Django or django-graphene with it.
Enabling Wagtail API
# settings/base.py
INSTALLED_APPS += ['wagtail.api.v2']
# urls.py
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)
urlpatterns = [
path('api/v2/', api_router.urls),
path('', include(wagtail_urls)),
]
API ViewSet customization
# blog/api.py
from wagtail.api.v2.views import PagesAPIViewSet
from wagtail.api.v2.filters import FieldsFilter, OrderingFilter, SearchFilter
class BlogPostAPIViewSet(PagesAPIViewSet):
# Allowed fields for filtering
filter_backends = [FieldsFilter, OrderingFilter, SearchFilter]
# Fields available via API
body_fields = PagesAPIViewSet.body_fields + [
'intro', 'date', 'tags', 'categories',
]
meta_fields = PagesAPIViewSet.meta_fields + [
'first_published_at',
]
listing_default_fields = PagesAPIViewSet.listing_default_fields + [
'intro', 'date',
]
def get_queryset(self):
return BlogPost.objects.live().public()
# Register custom endpoint
api_router.register_endpoint('blog-posts', BlogPostAPIViewSet)
API requests
const BASE = process.env.WAGTAIL_URL;
// List pages of specific type
const res = await fetch(
`${BASE}/api/v2/pages/?type=blog.BlogPost&fields=title,intro,date,slug,first_published_at,thumbnail&order=-first_published_at&limit=12`
);
const { items, meta } = await res.json();
// Single page by slug
const post = await fetch(
`${BASE}/api/v2/pages/?slug=my-post-slug&type=blog.BlogPost&fields=*`
).then(r => r.json()).then(r => r.items[0]);
// Search
const results = await fetch(
`${BASE}/api/v2/pages/?search=wagtail+tutorial&type=blog.BlogPost`
);
// Preview (requires separate PreviewAPIViewSet)
const preview = await fetch(`${BASE}/api/v2/pages/preview/?content_type=blog.blogpost&token=${previewToken}`);
API Images with transformations
Wagtail API returns image URLs without transformations. For rendition, use custom field:
from wagtail.api.v2.serializers import PageSerializer
from wagtail.images.shortcuts import get_rendition_or_not_found
from rest_framework import serializers
class BlogPostSerializer(PageSerializer):
thumbnail = serializers.SerializerMethodField()
def get_thumbnail(self, obj):
if not obj.header_image:
return None
rendition = get_rendition_or_not_found(obj.header_image, 'fill-800x400-c100')
return {
'url': rendition.url,
'width': rendition.width,
'height': rendition.height,
'alt': obj.header_image.alt_text,
}
class BlogPostAPIViewSet(PagesAPIViewSet):
serializer_class = BlogPostSerializer
Next.js ISR + Wagtail Webhooks
Wagtail doesn't have built-in webhooks. Implement via Django signals:
# blog/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from wagtail.signals import page_published, page_unpublished
import httpx
@receiver(page_published)
def on_page_published(sender, instance, **kwargs):
if not hasattr(instance, 'slug'):
return
try:
httpx.post(
settings.NEXTJS_REVALIDATE_URL,
json={'slug': instance.slug, 'type': instance.__class__.__name__},
headers={'x-revalidate-secret': settings.NEXTJS_REVALIDATE_SECRET},
timeout=5.0,
)
except Exception:
pass
// Next.js: app/api/revalidate/route.ts
export async function POST(request: Request) {
const { slug, type } = await request.json();
if (type === 'BlogPost') {
revalidatePath(`/blog/${slug}`);
revalidatePath('/blog');
}
return Response.json({ revalidated: true });
}
Full Wagtail API setup with Next.js — 2–4 days.







