Migrating from Wagtail: when Django teams should move to a JavaScript-native platform
The case for leaving Wagtail is rarely about Wagtail. It is about stack alignment: when the front end is React, when TypeScript engineers are easier to hire, and when the design-to-CMS gap slows delivery. This covers the triggers, the destinations, and when to stay.
The case for moving off Wagtail is almost never about Wagtail’s technical quality. Wagtail is a well-engineered, actively maintained CMS with real strengths in editorial workflow, StreamField flexibility, and deep Django integration. The case for moving is about stack alignment: the front-end team has moved to React and Next.js, Django engineers are harder to hire than TypeScript engineers, or the gap between how designers work and how the CMS is built has become a delivery bottleneck. This guide is for teams where one or more of those is true, and it is honest about the cases where none of them is.
What actually triggers a Wagtail migration
The triggers are organisational more than technical.
The front end has moved to React
Wagtail renders through Django templates. Once a front-end team moves to React and Next.js, which is where most front-end talent now sits, Wagtail becomes a data source rather than a rendering platform. That is architecturally fine, since Wagtail has a working headless API. The friction is operational: the CMS is configured in Python and the front end is JavaScript, so the two need specialists with different backgrounds to maintain. For a team that started full-stack on Django and has since moved its front end to React, that split is a standing cost.
Hiring against one stack, not two
An organisation that has standardised on TypeScript across its product stack but still runs a Python CMS is keeping a specialised hiring lane open that the rest of the team does not need. The pool of JavaScript and TypeScript engineers is large, while Python engineers with real Wagtail experience are a narrower niche, so every CMS hire is a harder search than it would be if the CMS lived on the same stack as everything else. At the engineering-leadership level that is a standing cost, not a one-off.
The design-to-development gap
Wagtail’s admin is a Django application that you customise in Python. It does use React for a few built-in pieces, such as the rich text editor, but the admin is not a React app you build from your own components the way Payload’s and Sanity’s admins are. For a team where designers build components in Figma and the front-end team builds them in React, that split shows up in the admin: it does not run your production component library, previewing content against the real components means configuring an external headless preview rather than seeing it in place, and changing the admin itself is Python and Django work rather than the front-end team’s normal tooling.
The site has outgrown Wagtail’s sweet spot
Wagtail is at its best with structured editorial content, stable content models, and workflows that map to its page model. Teams building content-driven web applications, where the CMS is one component in a larger system rather than the centre of it, tend to find that Wagtail’s page-centric model creates friction at the edges.
When Wagtail is still the right answer
The conversation should start here, because a migration that does not need to happen is a cost with no return.
Wagtail is still the right answer when the engineering team has real Django depth and uses it across more than the CMS, so the stack stays coherent and there is no cross-language friction. It is the right answer when editorial workflows are complex and working, since Wagtail’s page explorer, workflow management, and revision history are mature and often fit a large editorial team better than the alternatives. And it is the right answer when the content model maps cleanly to Wagtail’s page hierarchy, which is what the platform is built for. A Wagtail install that works, that the team can maintain, and that is not blocking delivery is an asset; migrating it to satisfy an architectural preference rather than a concrete problem is expensive.
There is also a middle path that avoids a migration altogether. If the only trigger is that the front end has moved to React, you can run Wagtail headless behind a React or Next.js front end and leave the CMS in place. That keeps the editorial workflows and the content model your team already knows, and it solves the rendering-stack problem without the cost of moving the CMS. A full migration earns its keep when the triggers go past rendering: hiring pressure, the design-to-admin gap, or a content model that has outgrown the page tree.
We migrate teams off Wagtail only when the trigger is a real organisational problem, not a general preference for JavaScript. We also maintain a Wagtail SEO Toolkit, so we know the platform from the inside, which is part of why we will tell you when staying put is the better call.
The three destinations that make sense
For teams where the triggers are real, a few destinations account for most Wagtail migrations, and three come up again and again: Payload, Strapi, and Sanity.
| Payload | Strapi | Sanity | |
|---|---|---|---|
| Content model | Code (TypeScript) | GUI builder or code | Code (JavaScript schemas) |
| Hosting | Self-hosted, open source (MIT) | Self-hosted, open source | Managed SaaS (content on Sanity) |
| Editorial UX | React admin, customisable | Polished admin out of the box | Sanity Studio, real-time and collaborative |
| Next.js fit | Runs inside Next.js | Separate backend, REST/GraphQL | Separate service, GROQ and API |
| Cost model | Free core; pay for infrastructure | Free core; paid plans for governance | Usage-based SaaS pricing |
| Closest to Wagtail on | Code-first, self-hosted model | JavaScript-native with a visual schema | Editorial experience |
Payload is the most natural successor for a code-first team. Wagtail’s content model is defined in Python (models.py, with StreamField blocks declared as classes), and Payload’s is defined in TypeScript (collections, with fields declared as objects). The conceptual model is nearly the same; the language is what changes. Both favour explicitness over convention, self-hosting over managed SaaS, and code ownership over a configuration UI, and both generate the admin from the content-model definition. Engineers productive in Wagtail’s model-first approach tend to reach the TypeScript equivalent faster than the more UI-driven models of Contentful or Hygraph.
Strapi suits a team that wants a JavaScript-native open-source CMS with a visual schema builder and a larger plugin ecosystem. Its content model can be built through a GUI rather than written entirely in code, which lowers onboarding friction for engineers new to the CMS layer. The trade-off against Payload is less architectural control: Strapi’s schema builder is more opinionated, and complex access-control logic takes more workarounds than Payload’s TypeScript-first approach.
Sanity is the destination when the main goal is editorial experience. Sanity Studio is a React application that engineers build with the same tooling as the production front end, and its real-time preview and collaborative editing address the design-to-development gap directly. Where the editorial team’s frustration is the main driver, Sanity is often the fastest win.
One difference cuts across the three. Payload and Strapi are self-hosted open source, so the content stays on infrastructure you control, whereas Sanity is a managed SaaS where the content lives on Sanity’s backend. A team leaving Wagtail is leaving a self-hosted platform, so a move to Sanity changes the ownership model in a way that a move to Payload or Strapi does not. For teams under data-residency or self-hosting requirements, that alone can decide it.
Why Payload is the most natural successor
The code-first parallel is worth seeing directly, because it is what makes Payload feel familiar to a Python engineer. In Wagtail, a content type is a Python class:
# Wagtail: models.py
from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail import blocks
class ArticlePage(Page):
body = StreamField([
('heading', blocks.CharBlock()),
('paragraph', blocks.RichTextBlock()),
('image', ImageChooserBlock()),
], use_json_field=True)
content_panels = Page.content_panels + [
FieldPanel('body'),
]
In Payload, the equivalent is a TypeScript object:
// Payload: collections/Articles.ts
import type { CollectionConfig } from 'payload'
export const Articles: CollectionConfig = {
slug: 'articles',
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'body', type: 'richText' }, // Lexical, with custom blocks
{ name: 'heroImage', type: 'upload', relationTo: 'media' },
],
}
The structure differs, since Payload uses a flat field array where Wagtail nests panels and blocks, but declaring content types as typed code objects is immediately recognisable to a Python engineer, and the conceptual jump is smaller than it looks. Django engineers already hold the mental models that transfer: ORM-style data modelling, typed field definitions, and API design. What they pick up is the TypeScript type system, Node’s async-first model, and Next.js’s rendering boundaries, none of which is difficult, though each needs deliberate attention rather than picking it up on the side.
The migration in practice
Two parts of a Wagtail migration need the most care: converting StreamField content to Lexical, and preserving the URL structure.
StreamField stores content as a JSON array of typed blocks, each with a type, a value, and an id. Payload’s Lexical editor stores rich text as a tree of typed nodes. Moving from one to the other is a block-type-by-block-type mapping, where each StreamField block type becomes a Lexical node, and each custom block you defined in Python needs its own mapping, either to a custom Lexical node registered in Payload or to a separate Payload field or collection when the content is better modelled that way. A couple of details cause most of the trouble. Wagtail’s RichTextBlock stores HTML with Wagtail-specific link and embed formats (linktype="page"), which have to be resolved to real URLs server-side in Python, using Wagtail’s expand_db_html (the same expansion the richtext template filter runs), before the content leaves Wagtail. And image references in StreamField are internal Wagtail IDs, so the media library has to move to Payload first, with an old-to-new ID map built, before the block conversion runs, or the images will not resolve.
On URLs, Wagtail derives paths from the page tree: a post at /news/2024/our-product-update/ sits three levels deep, and each page’s slug is concatenated with its parent’s path. In Payload and Next.js, URLs are routing decisions, set by the app directory structure or by dynamic segments mapped to collection slugs, so the old paths can be reproduced exactly; they just have to be configured rather than derived automatically. Build the redirect map by exporting every Wagtail page URL. The pages API returns each page’s meta.html_url and meta.slug by default, or a management command can walk the tree, and you map each URL to its new route and add a redirect only where a path changes. One Wagtail-specific catch: routable pages (RoutablePageMixin) create sub-URLs below a page that the standard pages API does not return, so they have to be found and mapped separately.
The SEO signals beyond URLs, canonical tags, structured data, meta descriptions, and Open Graph, were handled in your Wagtail setup (some in the core, most through templates or a package) and have to be rebuilt as fields in Payload and rendered in the Next.js head. Wagtail’s built-in SEO fields, seo_title and search_description on the Page model, map straight onto Payload fields; to pull them through the API you add them to the page model’s api_fields, or you read them in a management command.
How WAYF can help
WAYF is an official Payload partner and top contributor, and we migrate teams off legacy and Python-native systems, Wagtail among them, onto Payload and Next.js. We also maintain a Wagtail SEO Toolkit, so the platform is familiar to us from both sides of a migration. If you are weighing a move off Wagtail, we will look at your stack, your team, and your content model, and tell you whether a move earns its cost or whether Wagtail is still the right home. For a migration assessment, see wayf.ai/services, and see our work for platforms we have built.
FAQ
-
What is Wagtail CMS?
Wagtail is an open-source CMS built on Django and Python, maintained by Torchbox with a large community of contributors. It is widely used in media organisations, universities, government agencies, and non-profits. Its main strengths are StreamField (a flexible, type-safe block-based content model), mature editorial workflows with revision history and page approval, and deep integration with the Django ecosystem. It is an excellent tool for organisations with Python-native engineering teams and structured editorial requirements.
-
What is Wagtail StreamField?
StreamField is Wagtail's structured content field. It lets a single content field hold a sequence of typed blocks, headings, paragraphs, images, code blocks, embeds, and custom structured blocks, defined in Python code. StreamField is stored as JSON in the database and rendered through Wagtail's template or API layer. It is conceptually similar to Sanity's Portable Text or Payload's Lexical rich text with custom blocks, which is why the block-to-block mapping is the most technically demanding part of a Wagtail migration.
-
Should I migrate from Wagtail to Payload?
Migrate if your front-end team has moved to React and Next.js and running a Python CMS alongside it creates ongoing cross-stack friction, if staffing that Python CMS is a harder hiring search than staffing your TypeScript stack, or if the gap between Figma components and Wagtail's admin is slowing delivery. Stay if the engineering team uses Django broadly and the CMS is not an outlier, if editorial workflows are established and working, or if the migration cost cannot be justified by a concrete operational problem.
-
How do you export content from Wagtail?
Wagtail exposes content through its API v2 (
/api/v2/pages/), which returns page data, and StreamField content as JSON once you expose the field through the page'sapi_fields. For large content sets or content the API does not expose (some custom page types, image renditions), a Django management command is more reliable. Media files come out of Wagtail's library via the Image and Document models and transfer to the new CMS's storage. StreamField exported as JSON needs a block-type mapping before it can be imported into Payload's Lexical format. -
How long does a Wagtail to Payload migration take?
It depends on the number of pages and content types, how many custom StreamField blocks need mapping, and whether the site uses routable pages or multiple locales, so we scope a timeline per project rather than quote a standard one. The StreamField-to-Lexical conversion and custom-block mapping are usually the least predictable parts.
-
Is Strapi a good Wagtail replacement?
Strapi is a reasonable replacement for teams that want an open-source JavaScript CMS with a visual schema builder and a broader plugin ecosystem than Payload. It suits code-first teams less well than Payload, since it favours GUI-built schemas, and complex custom access control takes more work. Teams on Next.js also get a tighter integration from Payload. Note that Strapi places SSO, audit logs, and review workflows behind its paid plans, so factor that in if you need them.
-
What is Lexical?
Lexical is an extensible rich text editor framework developed by Meta, and Payload uses it as its default rich text field. Rather than storing HTML like a traditional WYSIWYG editor, Lexical stores editor state as a typed JSON tree of nodes. Custom block types, pullquotes, code blocks, and embedded media, are registered as custom Lexical nodes, the equivalent of Wagtail's custom StreamField blocks. That JSON tree is the target format for a StreamField migration.
-
Does Payload support the same editorial workflows as Wagtail?
Payload supports draft and published states, version history, and custom workflow logic written in TypeScript. It does not ship Wagtail's page-model workflow (the page explorer, tree-level approval, and scheduled publishing at the tree level) out of the box. Workflow in Payload is code, hooks and access-control functions, which is more flexible than Wagtail's built-in workflow but takes more implementation. For organisations with complex multi-stage approval chains, that workflow build should be scoped into the migration rather than assumed.
Sources
We're booking content platform
engagements for 2026.
Twenty-five minutes to walk through the work and decide if we're the right team for it. Scoping and a fixed price come after.