wagtail-tutorials-4-header.png

Wagtail Tutorials #4: Routable Page

Last updated on by michaelyin

Update in 2018-03-26: The code in article is working in Wagtail 1.10-1.13, if you want to get the code working in Wagtail 2.0, just check master branch of wagtail-bootstrap-blog

If you have used some CMS such as WordPress before, you must know that the permanent link of post, category link should be customized in some cases, so how to implement the same feature in Wagtail application? In this chapter, I will show you how to use RoutablePageMixin to make our blog routable, after that our blog can handle some types of sub urls such as category and tag. You will see this is a very powerful feature of Wagtail, however, the doc of Wagtail or some other blog posts did not talk much about this point, so I will try to make this point clear in this chapter.

Router

As I mentioned before, the URL of our blog posts are generated from the hierarchical tree by default, but sometimes if you want to change the way of handling the sub urls for a blog page, the RoutablePageMixin can help us get things done.

Here is quote from wagtail official doc:

A Page using RoutablePageMixin exists within the page tree like any other page, but URL paths underneath it are checked against a list of patterns. If none of the patterns match, control is passed to subpages as usual (or failing that, a 404 error is thrown).

So here we start to make our blog page can handle custom url like http://127.0.0.1:8000/blog/category/category_test_1/ and http://127.0.0.1:8000/blog/tag/test_tag/

First, activate RoutablePageMixin by adding wagtail.contrib.wagtailroutablepage to INSTALLED_APPS of wagtail_tuto/settings/base.py, the wagtail_tuto here is my wagtail project name, just change the path if your project name is different.

INSTALLED_APPS = [
   ...
   "wagtail.contrib.wagtailroutablepage",
   ...
]

We need to make the BlogPage inherit from both wagtail.contrib.wagtailroutablepage.models.RoutablePageMixin and wagtail.wagtailcore.models.Page, then create view methods and decorate them with wagtail.contrib.wagtailroutablepage.models.route

You should care about the order when changing the code because it has not been mentioned in the doc of wagtail. If the Page is located before the RoutablePageMixin, the route function would fail

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import datetime
from datetime import date

from django import forms
from django.db import models

from django.utils.dateformat import DateFormat
from django.utils.formats import date_format

from wagtail.wagtailcore.models import Page
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel, PageChooserPanel
from wagtail.wagtailsnippets.models import register_snippet

from modelcluster.fields import ParentalKey, ParentalManyToManyField
from modelcluster.tags import ClusterTaggableManager

from taggit.models import TaggedItemBase, Tag as TaggitTag

from wagtail.contrib.wagtailroutablepage.models import RoutablePageMixin, route

class BlogPage(RoutablePageMixin, Page):
    description = models.CharField(max_length=255, blank=True,)

    content_panels = Page.content_panels + [
        FieldPanel('description', classname="full")
    ]

    def get_context(self, request, *args, **kwargs):
        context = super(BlogPage, self).get_context(request, *args, **kwargs)
        context['posts'] = self.posts
        context['blog_page'] = self
        return context

    def get_posts(self):
        return PostPage.objects.descendant_of(self).live()

    @route(r'^tag/(?P<tag>[-\w]+)/$')
    def post_by_tag(self, request, tag, *args, **kwargs):
        self.search_type = 'tag'
        self.search_term = tag
        self.posts = self.get_posts().filter(tags__slug=tag)
        return Page.serve(self, request, *args, **kwargs)

    @route(r'^category/(?P<category>[-\w]+)/$')
    def post_by_category(self, request, category, *args, **kwargs):
        self.search_type = 'category'
        self.search_term = category
        self.posts = self.get_posts().filter(categories__slug=category)
        return Page.serve(self, request, *args, **kwargs)

    @route(r'^$')
    def post_list(self, request, *args, **kwargs):
        self.posts = self.get_posts()
        return Page.serve(self, request, *args, **kwargs)

Above is BlogPage definition, as you can see, we make the BlogPage inherited from RoutablePageMixin and Page, then we add some view functions and use route to decorate them. The parametre passed in route decorator is a regex expression. If you have no idea what regex is, just check this good learning resource

Now BlogPage can handle the sub url of category and tag, the view function can filter the posts based on parameters passed in. If you have no idea what get_context is, I will talk about it later.

Next, We need to add a new date field to our PostPage

class PostPage(Page):
    body = RichTextField(blank=True)
    date = models.DateTimeField(verbose_name="Post date", default=datetime.datetime.today)
    categories = ParentalManyToManyField('blog.BlogCategory', blank=True)
    tags = ClusterTaggableManager(through='blog.BlogPageTag', blank=True)

    content_panels = Page.content_panels + [
        FieldPanel('body', classname="full"),
        FieldPanel('categories', widget=forms.CheckboxSelectMultiple),
        FieldPanel('tags'),
    ]

    settings_panels = Page.settings_panels + [
        FieldPanel('date'),
    ]

    @property
    def blog_page(self):
        return self.get_parent().specific

    def get_context(self, request, *args, **kwargs):
        context = super(PostPage, self).get_context(request, *args, **kwargs)
        context['blog_page'] = self.blog_page
        return context

As we can see, we add a new field date to our PostPage, the value will be set when page instance is created. To make user can set it in edit page, we also add it to Page.settings_panels

The categories and tags are implemented in another Wagtail blog tutorial Category And Tag Support. After you are done with the model, remember to migrate your databae.

python manage.py makemigrations blog
python manage.py migrate blog

Customizing context

Sometimes we need to add some extra value to the context, therefore the template can render it without any more job, which can keep our template clean and easy to maintain.

All pages have a get_context method that is called whenever the template is rendered and returns a dictionary of variables to bind into the template.

class BlogPage(RoutablePageMixin, Page):
    ....
    def get_posts(self):
        return PostPage.objects.descendant_of(self).live()

    def get_context(self, request, *args, **kwargs):
        context = super(BlogPage, self).get_context(request, *args, **kwargs)
        context['posts'] = self.posts
        context['blog_page'] = self
        return context

class PostPage(Page):
    ....
    @property
    def blog_page(self):
        return self.get_parent().specific

    def get_context(self, request, *args, **kwargs):
        context = super(PostPage, self).get_context(request, *args, **kwargs)
        context['blog_page'] = self.blog_page
        context['post'] = self
        return context

As you can see, after router of BlogPage handle the HTTP request, posts in context is the collection of filtered posts, here we add it to context object to make the templates can directly iterate it. We add blog_page to context to avoid call page.get_children or page which might confuse us.

Now edit templates/blog/blog_page.html

{% load wagtailcore_tags %}

{% block content %}
    <h1>{{ blog_page.title }}</h1>

    <div class="intro">{{ blog_page.description }}</div>

    {% for post in posts %}
        <h2><a href="{% pageurl post %}">{{ post.title }}</a></h2>
    {% endfor %}

{% endblock %}

Now the code of our template is much more readable, which make developer easy to maintain or troubleshoot.

Reversing route urls

Have you noticed that I also added a get_context method to PostPage in the code block above, make sure to add it in your project so we can keep going.

In the previous chapter, I displayed the category info and tag info in post page template, now we convert the info to links, so people can click the links to find more relevant posts.

Edit templates/blog/post_page.html

{% load wagtailcore_tags wagtailroutablepage_tags%}

{% block content %}
    <h1>{{ page.title }}</h1>

    {{ page.body|richtext }}

    <p><a href="{{ page.get_parent.url }}">Return to blog</a></p>

{% endblock %}


{% if page.tags.all.count %}
    <div class="tags">
        <h3>Tags</h3>
        {% for tag in page.tags.all %}
            <a href="{% routablepageurl blog_page "post_by_tag" tag.slug %}">{{ tag }}</a>
        {% endfor %}
    </div>
{% endif %}

{% with categories=page.categories.all %}
    {% if categories %}
        <h3>Categories</h3>
        <ul>
            {% for category in categories %}
                <li style="display: inline">
                    <a href="{% routablepageurl blog_page "post_by_category" category.slug %}">{{ category.name }}</a>
                </li>
            {% endfor %}
        </ul>
    {% endif %}
{% endwith %}

As you can see, first, we load wagtailroutablepage_tags in our template, then we can use {% routablepageurl blog_page "post_by_tag" tag.slug %} to ask wagtail reverse url for us. There are 3 parameters passed in routablepageurl here, first one is blog_page, which is added in get_context method, second one is name of router we created above, if you do not specify the router name, the method name would be used by default, third one is slug value.

Now we can see in the post url http://127.0.0.1:8000/blog/post-page-1/(This url belongs to the blog post which is created in my previous Wagtail Tutorial, Wagtail Tutorials #2: Create Data Model, You can change the url in your case), the category and tag all are links, and if we click the tag link like http://127.0.0.1:8000/blog/tag/tag1/, the router of BlogPage will call post_by_tag to handle the request and filter the posts with tag value, after the data is ready, the template of BlogPage will render it.

Now you can start to test code in your env and run, if you remember the get_context we created in PostPage, we can also try to replace page with post in templates/blog/post_page.html, this can make your template more clear and some one who have no experience about Wagtail would like that. For example,

...
{% block content %}
    <h1>{{ post.title }}</h1>

    {{ post.body|richtext }}

    <p><a href="{{ post.get_parent.url }}">Return to blog</a></p>

{% endblock %}
...

Conclusion

In this chapter, we add router function to the blog root page and now it can handle more url patterns such as cateogry url and tag url. Here I must say again the order of RoutablePageMixin and Page is important, if the order is wrong, the route function can not work as expect, Page not found 404 error will be raised, you should be careful about this point. What is more, we learn about how to use get_context to add extra values to the context to keep our templates clean and readable.

The source code of this Wagtail tutorial is available on Github, you can get it here wagtail-bootstrap-blog, you can also directly check the live demo here Wagtail Blog Demo.

git clone https://github.com/michael-yin/wagtail-bootstrap-blog.git
cd wagtail-bootstrap-blog
git checkout routerablepage

# setup virtualenv
pip install -r requirements.txt

./manage.py runserver
#http://127.0.0.1:8000/blog

Remember to use username admin and password admin to login in the wagtail CMS admin page.

Wagtail Ebook

For people who like to read ebook instead of blog posts, I have published a book on leanpub´╝îwhere you can get pdf, epub, mobi version of this Wagtail book Build Blog With Wagtail CMS.

Send Me Message

Tell me more about your project and see if I can help you.

Contact Me