Traditional CMS platforms lock your content into a single frontend. Wagtail's headless capabilities break that lock, serving structured JSON through a powerful REST API that any frontend - React, Vue, Next.js, SwiftUI, or mobile - can consume. In this lesson, you'll learn how to enable, customize, and secure Wagtail's API v2, build custom viewsets that expose StreamField content, and connect a modern JavaScript frontend for a true decoupled architecture.
1. Learning Objectives
By the end of this lesson, you will be able to:
2. Why This Matters (Real-World Scenario)
Imagine you're building a high-traffic news website that needs to publish content to a web app, an iOS app, and an Android app simultaneously. A traditional CMS that couples content management with frontend rendering forces you to rebuild or duplicate logic for each platform. Your editorial team needs a single place to write articles, upload images, and manage categories—while your frontend developers want the freedom to use React, SwiftUI, or Jetpack Compose.
A headless CMS decouples the content management backend from the presentation layer. Wagtail's built-in REST API turns your Wagtail site into a pure content API, serving structured JSON to any client. This is exactly how major media outlets and enterprise sites power their omnichannel strategies.
3. Core Concepts
Headless CMS: A content management system that provides content via API (headless = no built-in frontend \"head\"). The backend handles content creation, storage, and delivery; the frontend is a completely separate application.
Wagtail API: Wagtail ships with a RESTful API package (wagtail.api.v2) that exposes pages, images, and documents as JSON endpoints. It supports filtering, field selection, pagination, and preview.
API v1 vs v2: Wagtail 2.x introduced v2 of the API, which is the recommended version. v1 is deprecated but available for backward compatibility. v2 offers richer filtering, better performance, and cleaner response structure.
StreamField serialization: When Wagtail serves a page with StreamField content via the API, each block type is serialized to JSON automatically. Custom blocks need their own serialization logic.
Private API vs Public API: The API can be configured to require authentication (private) or be open to the internet (public). For production headless setups, you typically authenticate with API tokens or JWT.
4. Hands-On Practice: Setting Up Wagtail Headless API
We'll walk through enabling the API, creating custom endpoints, and querying content from a frontend application.
Step 1: Enable the Wagtail API
First, ensure wagtail.api.v2 is in your INSTALLED_APPS:
# wagtail_project/settings/base.py\nINSTALLED_APPS = [\n 'wagtail.api.v2',\n 'wagtail.admin',\n 'wagtail',\n]
Next, add the API URL routes to your main urls.py:
# wagtail_project/urls.py\nfrom wagtail.api.v2.router import WagtailAPIRouter\nfrom wagtail.api.v2.views import PagesAPIViewSet\nfrom wagtail.images.api.v2.views import ImagesAPIViewSet\nfrom wagtail.documents.api.v2.views import DocumentsAPIViewSet\n\napi_router = WagtailAPIRouter('wagtailapi')\napi_router.register_endpoint('pages', PagesAPIViewSet)\napi_router.register_endpoint('images', ImagesAPIViewSet)\napi_router.register_endpoint('documents', DocumentsAPIViewSet)\n\nurlpatterns = [\n path('api/v2/', api_router.urls),\n]
Step 2: Test the API Endpoints
Restart your server and test the endpoints:
# List all published pages\ncurl http://localhost:8000/api/v2/pages/\n\n# Get a single page by ID\ncurl http://localhost:8000/api/v2/pages/1/\n\n# List images\ncurl http://localhost:8000/api/v2/images/\n\n# List documents\ncurl http://localhost:8000/api/v2/documents/
The pages endpoint returns JSON with pagination metadata and results:
{\n \"meta\": {\n \"total_count\": 5,\n \"limit\": 20,\n \"offset\": 0\n },\n \"items\": [\n {\n \"id\": 1,\n \"meta\": {\n \"type\": \"home.HomePage\",\n \"detail_url\": \"http://localhost:8000/api/v2/pages/1/\"\n },\n \"title\": \"Home\",\n \"slug\": \"home\"\n }\n ]\n}
Step 3: Filtering and Selecting Fields
The API supports powerful query parameters:
# Select specific fields\ncurl \"http://localhost:8000/api/v2/pages/?fields=title,slug,first_published_at\"\n\n# Filter by type\ncurl \"http://localhost:8000/api/v2/pages/?type=blog.BlogPage\"\n\n# Search\ncurl \"http://localhost:8000/api/v2/pages/?search=wagtail\"\n\n# Ordering\ncurl \"http://localhost:8000/api/v2/pages/?order=-first_published_at\"\n\n# Pagination\ncurl \"http://localhost:8000/api/v2/pages/?limit=5&offset=10\"
Step 4: Customize API Output with Serializers
To expose custom page fields (like StreamField body content), create a custom API viewset:
# blog/api.py\nfrom wagtail.api.v2.views import PagesAPIViewSet\nfrom wagtail.api.v2.filters import (\n FieldsFilter, OrderingFilter, SearchFilter, LocaleFilter\n)\n\nclass BlogPagesAPIViewSet(PagesAPIViewSet):\n body_fields = PagesAPIViewSet.body_fields + [\n 'body', 'excerpt', 'date_published', 'categories',\n ]\n\n def get_queryset(self):\n return super().get_queryset().live().public().type('blog.BlogPage')\n\n filter_backends = [\n FieldsFilter, OrderingFilter, SearchFilter, LocaleFilter,\n ]
# urls.py\nfrom blog.api import BlogPagesAPIViewSet\napi_router.register_endpoint('blog', BlogPagesAPIViewSet)\n# Now available at: /api/v2/blog/
Step 5: Authentication & Permissions for the API
For private APIs or draft previews, configure authentication:
# settings.py\nREST_FRAMEWORK = {\n 'DEFAULT_AUTHENTICATION_CLASSES': [\n 'rest_framework.authentication.TokenAuthentication',\n 'rest_framework.authentication.SessionAuthentication',\n ],\n 'DEFAULT_PERMISSION_CLASSES': [\n 'rest_framework.permissions.IsAuthenticatedOrReadOnly',\n ],\n 'DEFAULT_THROTTLE_RATES': {\n 'anon': '100/day',\n 'user': '1000/hour',\n },\n}\n\nWAGTAILAPI_BASE_URL = 'https://yourdomain.com'
To preview unpublished pages via the API (useful for headless previews):
# Preview mode requires authentication\n# Get the latest draft of a page\ncurl -H \"Authorization: Token <your_token>\" \\\n \"http://localhost:8000/api/v2/pages/5/?show_draft=true\"\n\n# Or by token-based preview URL\ncurl \"http://localhost:8000/api/v2/pages/5/?token=abc123def456\"
Step 6: Connecting a React Frontend
Here's a minimal React component that fetches and displays blog posts from Wagtail's API:
// BlogList.jsx\nimport { useState, useEffect } from 'react';\n\nconst API_URL = 'https://yourdomain.com/api/v2/blog/';\n\nfunction BlogList() {\n const [posts, setPosts] = useState([]);\n const [loading, setLoading] = useState(true);\n\n useEffect(() => {\n fetch(API_URL)\n .then(res => res.json())\n .then(data => {\n setPosts(data.items);\n setLoading(false);\n })\n .catch(err => {\n console.error('API Error:', err);\n setLoading(false);\n });\n }, []);\n\n if (loading) return <div>Loading...</div>;\n\n return (\n <div className=\"blog-list\">\n {posts.map(post => (\n <article key={post.id}>\n <h2>{post.title}</h2>\n <p>{post.excerpt}</p>\n <time>{post.first_published_at}</time>\n </article>\n ))}\n </div>\n );\n}\n\nexport default BlogList;
For Next.js (SSR/SSG), fetch during build or at request time:
// pages/blog.js (Next.js Pages Router)\nexport async function getStaticProps() {\n const res = await fetch('https://yourdomain.com/api/v2/blog/?fields=title,slug,body,excerpt');\n const data = await res.json();\n return { props: { posts: data.items }, revalidate: 60 };\n}\n\nexport default function Blog({ posts }) {\n return (\n <main>\n {posts.map(post => (\n <article key={post.id}>\n <h1>{post.title}</h1>\n <div dangerouslySetInnerHTML={{ __html: post.body }} />\n </article>\n ))}\n </main>\n );\n}
5. Common Errors & Solutions
6. Summary Checklist
7. Practice Exercise
Challenge: Build a Headless Blog API with Wagtail & Next.js
Create a Wagtail project with a custom BlogPage model that includes:
Then:
Test that your frontend correctly renders the body StreamField content and that images load properly.
8. Next Steps
Congratulations! You've mastered using Wagtail as a headless CMS backend. This pattern is used in production by organisations that need to deliver content across web, mobile, and IoT platforms from a single Wagtail instance.
Coming up in Lesson 4: Wagtail SEO & Performance — We'll dive deep into meta tags, XML sitemaps, caching strategies, and performance optimization techniques to make your Wagtail site lightning-fast and search-engine-friendly.
Key resources for further learning:
Common pitfalls to avoid:
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment