Adding themes to articles in Wagtail

If you have many articles on your Wagtail site, it might be useful to order them in themes. In this tutorial we explain how to create themes and theme pages with all articles per theme.

July 9, 2020, 1:40 p.m.

We create a Theme model by putting a Wagtail snippet in our before ArticlePage:

from wagtail.snippets.models import register_snippet

class Theme(models.Model):
    name = models.CharField(max_length=255)

    panels = [

    def __str__(self):

The model has just one field called name, which can be created and edited in admin, in a separate menu Snippets. We create a relationship between an article page and one or more themes by adding the following field to our ArticlePage model (import ParentalManyToManyField from modelcluster.fields):

themes = ParentalManyToManyField(Theme, blank=True,  related_name='articlepages', verbose_name=_("Themes"))

and to the content_panels of ArticlePage (import forms from Django):

FieldPanel('themes', widget=forms.CheckboxSelectMultiple),

This will allow us to choose the relevant themes for every article and retrieve all articles pertaining to a theme via the related_name articlepages. Note that it is not useful to add null=True to a ManyToManyField, as mentioned in the docs. Now define a ThemePage model:

from wagtail.snippets.edit_handlers import SnippetChooserPanel

class ThemePage(TranslatablePage):
    theme = models.ForeignKey(Theme, on_delete=models.SET_NULL, null=True, related_name='themepages')
    intro = RichTextField(blank=True)
    image = models.ForeignKey(
        'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, related_name='+'
    caption = models.CharField(blank=True, null=True, max_length=250)

    def articlepages(self):
        return self.theme.articlepages.filter(language=self.language).live().order_by('-first_published_at')

    content_panels = TranslatablePage.content_panels + [
        FieldPanel('intro', classname='full'),

Here we use a ForeignKey to connect theme and theme page; the reason for this is that we will have pages in multiple languages pertaining to one theme. If you would have only one language, then you might opt for a OneToOneField. We define a method articlepages to order the articles in reverse chronological order, just as we have done in an earlier tutorial with our ArticleIndexPage model. We only want article pages in the same language, therefore we filter by the field language of the model TranslatablePage. Since we have chosen exactly the same field names and related_name as for the model ArticleIndexPage, the template theme_page.html for a theme page with all articles on that theme can be identical to the article_index_page.html template that we have created in an earlier tutorial:

{% include "article_index_page.html" %}

We also want an index page with an overview of all our themes. Our model can be simple:

class ThemeIndexPage(TranslatablePage):
    intro = RichTextField(blank=True)

    # Specifies that only ThemePage objects can live under this index page
    subpage_types = ['ThemePage']

    content_panels = TranslatablePage.content_panels + [
        FieldPanel('intro', classname='full'),

It resembles the model of ArticleIndexPage, with subpage_types indicating that we only want ThemePage pages as children. The template theme_index_page.html is almost the same as that of ArticleIndexPage, only here we will loop over page.get_children to display all theme pages; you can find it in the repository.

We would also like to display the themes on our home page. Add to the home page model a theme section title, an introduction and a link to a page where we will put all themes:

theme_section_title = models.CharField(
    help_text=_("Title to display above the theme section"),
theme_section_intro = RichTextField(blank=True)
theme_section = models.ForeignKey(
    help_text=_("Featured section for the homepage. Will display all themes."),
    verbose_name=_("Theme section"),

Add to the template home_page.html the title and the introduction and a container with the following code (full code in repository):

{% for childpage in page.theme_section.get_children %}
    <div class="col-auto mb-3">
        <div class="card theme">
            <a href="{{ childpage.url }}">
                {% if childpage.specific.image %}
                    {% image childpage.specific.image fill-320x240 class="img-front rounded-circle" %}
                {% else %}
                    <img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-default rounded-circle">
                {% endif %}
                <img alt="" src="{% static 'images/transparent.png' %}" width="320" height="240" class="img-background rounded-circle">
                <div class="card-img-overlay">
                    <br><h5 class="card-title text-center">{{ childpage.title }}</h5>
                    <p class="card-subtitle text-center">{{ childpage.specific.intro|striptags|truncatewords:15 }}</p>
{% endfor %}

This is very similar to what we have created in an earlier tutorial to display a number of articles on the home page: we use the image of the theme page to create a card with a link to that theme, and we create an overlay effect with a second transparent image that we style in our CSS. The cards this time are ellipses.

To make our home page a bit more lively we can add a carousel with images of all themes and their titles. This is a standard Bootstrap component, so we will not repeat the html code here. To iterate over the themes we use a standard Django for loop. We retrieve the themes with page.theme_section.get_children, use the image tag to display the images and the pageurl tag to link to the url of a page.

We would also like to show little badges with all relevant themes above each article and with a link to the relevant theme page in the same language. Now we run into a small problem: we know which themes belong to a given article page, but since we have theme pages in several languages we don't know to which of those to link. We can solve this by adding the following method to the class ArticlePage:

def themepages(self):
    return ThemePage.objects.filter(theme__in=self.themes.all(), language=self.language)

This will return exactly all theme pages belonging to the related themes in the same language as the article page. Now the code to list the themes on the article page is:

{% if page.themes.all %}
    <div class="container-fluid mt-4">
        {% trans "Themes:" %}
        {% for themepage in page.themepages %}
                <a href="{{ themepage.url }}" class="badge badge-primary">{{ }}</a>
        {% endfor %}
{% endif %}

Models and templates are done, so time to try it out. Migrate the database, go to the editor, create themes via the Snippets menu, select relevant themes for your articles, create an index page for all of your themes with the template for ThemeIndexPage: everything should be straightforward, check the video if you want.

For those of you that want not only your pages but also your themes in multiple languages, visit this tutorial. And anyone who wants to add navigation to his site, read on.

Comment on this article (sign in first or confirm by name and email below)