A comprehensive Python CMS framework review allows you to single out tools that can enrich your coding practice. We want to focus your attention on Wagtail, a Python CMS. So let’s dive in!

What is the Wagtail CMS?

Wagtail is a Python based CMS made for Django. The Wagtail CMS was released in 2015 by a digital agency named Torchbox, the same agency that created South migrations for Django in 2008. So when we encountered a project that required a content management system, we had an additional reason to give Wagtail CMS a try. We decided to try it out in practice.

At the moment, Wagtail has a few different versions that support the Django framework from version 1.8.x up to version 2.0.x, and a version that supports Django 2.1.x is currently in development.

The Wagtail CMS was designed to be simple, ergonomic, and fast, and all of that was achieved by distributing responsibilities between the programmer and content manager. This distribution means that a content manager can’t create any new entity in the system using the admin panel interface without it being predefined in code. In other words, before using a page or a block of content in the admin panel, it must be created programmatically first.

Pros and cons of the Django Wagtail CMS

Compared to other famous content management systems like Drupal, WordPress, and even the Django Content Management System, which provides a ton of flexibility to the administrator (content manager) at the cost of interface complexity, the Wagtail CMS has a rather simple interface with minimum settings. And all of these settings and functionality have to be implemented in code. In other words, to build a page, content manager can only use a set of tools that are implemented programmatically as Python classes or so-called blocks. That’s why Wagtail requires a certain level of expertise with Python and Django and might seem slightly more complex at first than it really is. This CMS provides:

  • a lightweight and straightforward interface;
  • flexibility in development, providing only a platform and set of tools and giving full freedom to the programmer in how to use them;
  • a performance boost, as all querying and database use can be fully controlled by the developer;
  • ease and speed of development because Wagtail is Django and uses Django’s authentication backend, templating system, and so on.

One more great thing about the Wagtail CMS is that it’s designed to keep a minimum amount of HTML code in the database which, along with its external API functionality, provides easy content management on multiple platforms so one page or piece of content can look different on web and mobile applications, for example.

Yet another great feature is the built-in Elasticsearch engine, which requires only a few lines of code to get up and running.

Of course there’s another side of the coin. Despite all of the advantages, the Wagtail CMS has its disadvantages as well:

  • there’s no frontend out of the box, so after installation the developer has only an admin panel interface;
  • documentation isn’t bad and is more than enough for beginners, but there are still a few aspects that are poorly documented or not documented at all (for example, advanced customization of the admin interface, a number of class methods are simply missing, etc.);
  • the community is pretty active but not as big as might be desired.

A closer look at pages

In the Wagtail CMS, pages are Django models. Pages inherit from the abstract Wagtail Page model, which has all the service methods, fields, and properties that a page may need.

Let’s say we want to create a blog application and a page for an article on this blog. The basic django model for that might look like this:

# blog/models.py
from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, RichTextFieldPanel
from wagtail.core.fields import RichTextField
from wagtail.core.models import Page

class ArticlePage(Page):
   author = models.CharField(max_length=255)
   subtitle = models.CharField(max_length=150, null=True, blank=True)
   body = RichTextField()

   content_panels = Page.content_panels + [

Code snippet 1. Article Page with RichTextField

In this model, we’ve used a standard Django CharField and a field from the Wagtail CMS called RichTextField. The content_panels property is required and defines the so-called field panels for our model’s fields. In other words, it holds instructions on how to build these fields in the admin interface.

To be able to preview our new page, we need to create a template for it. A template file must be placed either in the project’s templates directory (under the blog subdirectory) or inside the application templates directory, and it must be named exactly like in the model, only in snake case — so for the ArticlePage model it should be article_page.html. The syntax for templates is fully Django syntax, and the Wagtail CMS has sufficient documentation about this area so we won’t duplicate it here.

The page will automatically be registered in the admin interface, so all that’s left is to create and apply a migration.

Figure 1. A basic article page editing interface

The result is nice and simple: two CharFields and a TextField with a simple WYSIWYG editor, which can be easily extended, customized, or replaced with any WYSIWYG editor that’s compatible with Django. But as one of the core missions of the Wagtail CMS is to minimize the amount of HTML code in the database, the default editor should be enough. The Promotion and Settings tabs contain additional fields and options for publishing functionality.

A closer look at StreamField and Blocks

RichTextField is good enough for simple structured pages with headings, formatted text, and images, but what if we need to build something more complex? What if our page should look different on different platforms? Here comes the killer feature of Wagtail CMS — the StreamField.

“StreamField provides a content editing model suitable for pages that do not follow a fixed structure – such as blog posts or news stories – where the text may be interspersed with subheadings, images, pull quotes and video. ”

- Wagtail documentation

To be more specific, StreamField is an alternative to RichTextField, and it’s a WYSIWYG editor. StreamField keeps all data in JSON format, structured as described in code, so the content manager can’t change its structure. It has a visual editor in the admin interface and the structure of its JSON data can be defined via the Wagtail CMS blocks.

Let’s say we’ve decided that the body of ArticlePage should contain a few headings, text paragraphs, and embedded videos. To do that, we’ll need to use Wagtail blocks. The most interesting are StructBlock and StreamBlock.

StructBlock is used to combine a number of basic blocks like CharBlock and RichTextBlock. It should have its own template which describes how these sub-blocks should be rendered. A StructBlock can even contain another StructBlock.

Then a StreamBlock is used to gather all required StructBlocks under the body field of our ArticlePage.

So let’s see how this would look in practice. Here’s the StructBlock created for headings:

# blog/blocks.py part 1
from wagtail.core.blocks import StructBlock, CharBlock, ChoiceBlock

class HeadingBlock(StructBlock):
   text = CharBlock(required=True)
   size = ChoiceBlock(choices=[
       ('h2', "H2"),
       ('h3', "H3")
   ], required=True)

   class Meta:
       icon = "title"
       label = "Heading"
       template = "blog/blocks/block_heading.html"

Code snippet 2. A StructBlock for a custom heading

This StructBlock contains two blocks: CharBlock and ChoiceBlock. We can also opt for an icon, label, and template for it, a simple version of which could look like this:

{# blocks/blog_heading.html #}

{% if self.size == 'h2' %}
   <h2>{{ self.text }}</h2>
{% else %}
   <h3>{{ self.text }}</h3>
{% endif %}

Code snippet 3. A template for HeadingBlock

Following this logic, we’ll create similar blocks for text and embedded video sections:

# blog/blocks.py part 2
from wagtail.core.blocks import StructBlock, RichTextBlock

class TextBlock(StructBlock):
   text = RichTextBlock(
       features=['bold', 'italic', 'paragraph', 'ul', 'link']

   class Meta:
       label = 'Text'
       template = "blog/blocks/block_text.html"

Code snippet 4. A StructBlock for a text section

TextBlock contains only a RichTextBlock, but let’s assume we placed it into a separate StructBlock to be able to override the rendering logic for it. In this case, the basic version of the template might look like this:

{# blocks/block_text.html #}
{% load wagtailcore_tags %}

<div class="custom-text-block">
   {{ self.text|richtext }}

Code snippet 5. A template for TextBlock

The last StructBlock we’re going to create is a block for embedded video. It will contain an EmbedBlock from the Wagtail:

# blog/blocks.py part 3
from wagtail.core.blocks import StructBlock
from wagtail.embeds.blocks import EmbedBlock

class VideoEmbedBlock(StructBlock):
   video = EmbedBlock(
       help_text="Insert a video url e.g https://youtu.be/yRmZ6WUfoOc"

   class Meta:
       icon = 'media'
       label = "Embed Video"
       template = "blog/blocks/block_video_embed.html"

And to render this block, we’re going to use the embed tag from the wagtailembeds_tags library, which will do the trick for us.

{# blocks/block_video_embed.html #}

{% load wagtailembeds_tags %}

<div class="block-video-embed">
   {% embed self.video.url %}

Code snippet 7. A template for VideoEmbedBlock.

Finally, we need to define a StreamBlock to gather all of our StructBlocks in one place:

# blog/blocks.py part 4
from wagtail.core.blocks import StreamBlock

class BaseArticleStreamBlock(StreamBlock):
   heading = HeadingBlock()
   text = TextBlock()
   video = VideoEmbedBlock()

Code snippet 8. A StreamBlock for the body of ArticlePage

Now we’re ready to update our ArticlePage model by replacing RichTextField with StreamField and changing the content panel for it to StreamFieldPanel. After all these transformations, our model should look like this:

# blog/models.py with StreamField

from django.db import models
from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel
from wagtail.core.fields import StreamField
from wagtail.core.models import Page

from apps.blog.blocks import BaseArticleStreamBlock

class ArticlePage(Page):
   author = models.CharField(max_length=255)
   subtitle = models.CharField(max_length=150, null=True, blank=True)
   body = StreamField(
       verbose_name="Page Body",

   content_panels = Page.content_panels + [

Code snippet 9. Updated ArticlePage model

Now let’s create and run migrations for our changes and check out the admin interface for ArticlePage:

Figure 2. A basic ArticlePage editing interface with StreamField used for the body

As we can see in Figure 2, in the body field there’s a TextBlock, followed by a HeadingBlock and another TextBlock, and there’s a black panel under the blocks where we can select what block we’d like to create next.

All of the created content (from Figure 2) will be saved as JSON that will have the following structure:

           'type': 'text',
           'value': {
               'text': '<p>Lorem ipsum dolor sit amet,...</p>'
           'id': 'ff0e4723-e384-44cd-be7d-aff2f1f0e913'},
               'type': 'heading',
               'value': {
                   'text': 'Ad vix probatus perpetua comprehensam',
                   'size': 'h3'
               'id': 'e084e4a7-86e8-4ba1-80ef-0fdc1cf773c7'
               'type': 'text',
               'value': {
                   'text': '<p>Eos lareformidans no. ...</p>'
               'id': '3c99b825-c166-4737-8596-cd98a307bd9d'

Taking all of this into account, we believe the Wagtail CMS could be a good fit for developing any non-ecommerce application that requires content management. We can use Wagtail to develop a fast and reliable solution, but it requires developer expertise with Python and Django.

Wagtail CMS in practice: examples

We’ve been involved in the development of several websites based on the Wagtail CMS. One is dedicated to the Scots College Old Boys’ Union, a community that wants to preserve the memory of school life.

Source: scotsoldboys.tsc.nsw.edu.au

The other platform we’ve created with Wagtail is called Biennale of Sydney. It provides a space for thought-provoking art and ideas and attracts artistic minds across Australia and the rest of the globe.

Source: biennaleofsydney.arT

In our opinion, the Wagtail is one of the best Python CMS solutions and a great tool that’s comfortable to work with and easy to learn. Despite being pretty undervalued currently, it leaves a good impression and we’re looking forward to working again with the Wagtail CMS.

Have some questions or want to build a project with the Wagtail CMS? Write to our sales representative.