Wagtail Routable Page

Michael Yin

Last updated on September 28 2021

Table of Contents

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL

The source code is available on https://github.com/AccordBox/wagtail-bootstrap-blog

Objective

By the end of this chapter, you should be able to:

  1. Understand how to create Routable page in Wagtail
  2. Make Caregory and Tag work with Routable page
  3. Implement Pagination in Wagtail.

Router

Wagtail pages are organized following tree structure, as each page in the tree has its own URL path, like so:

/
    people/
        nien-nunb/    (http://www.example.com/people/nien-nunb)
        laura-roslin/
    blog/
        post-page-1/
        post-page-2/

You can check more on Wagtail doc: Introduction to Trees

The RoutablePageMixin mixin provides a convenient way for a page to respond on multiple sub-URLs with different views. For example, a blog section on a site might provide several different types of index page at URLs like /blog/2013/06/, /blog/authors/bob/, /blog/tagged/python/, all served by the same page instance.

So here we will make our blog page can handle custom url like http://127.0.0.1:8000/category/slug/ and http://127.0.0.1:8000/tag/slug/

Add wagtail.contrib.routable_page to the INSTALLED_APPS of wagtail_bootstrap_blog/settings/base.py

INSTALLED_APPS = [
    'home',
    'search',
    'blog',

    'wagtail.contrib.forms',
    'wagtail.contrib.redirects',
    'wagtail.embeds',
    'wagtail.sites',
    'wagtail.users',
    'wagtail.snippets',
    'wagtail.documents',
    'wagtail.images',
    'wagtail.search',
    'wagtail.admin',
    'wagtail.core',
    'wagtail.contrib.routable_page',

    # code omitted for brevity
]

Update blog/models.py

from wagtail.contrib.routable_page.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['blog_page'] = self
        context['posts'] = self.posts
        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.posts = self.get_posts().filter(tags__slug=tag)
        return self.render(request)

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

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

Notes:

  1. Update blog.BlogPage to make it inherit from both wagtail.contrib.routable_page.models.RoutablePageMixin and Wagtail Page
  2. Please make sure the RoutablePageMixin is before the Page, if not, the router function would fail.
  3. We added three routes, the parameters passed in the route decorator is a regex expression. If you are new to this, please check Django doc: regular expressions
  4. get_posts is a common method which return the public PostPage of the BlogPage. The routes would then filter and set the value to self.posts.
  5. The route works similar with Django view, here we use return self.render(request) to return the Response back to visitor.
  6. I will talk about get_context method in a bit so let's ignore it for now.

Context

Sometimes, if we want to make some variables available in the template, we need to overwrite the get_context method.

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

Update blog/models.py

class BlogPage(RoutablePageMixin, Page):

    # code omitted for brevity

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


class PostPage(Page):

    # code omitted for brevity

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

Notes:

  1. Now blog_page would be available when rendering blog_page.html and post_page.html
  2. posts would be available in blog_page.html

Template

Update wagtail_bootstrap_blog/templates/blog/blog_page.html

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags %}

{% block content %}

    {% for post in posts %}
        <div class="card mb-4">
          {% if post.header_image %}
            {% image post.header_image original as header_image %}
            <a href="{% pageurl post %}">
              <img src="{{ header_image.url }}" class="card-img-top">
            </a>
          {% endif %}

          <div class="card-body">
            <h2 class="card-title">
              <a href="{% pageurl post %}">{{ post.title }}</a>
            </h2>
            <p class="card-text">
              {{ post.description }}
            </p>
            <a href="{% pageurl post %}" class="btn btn-primary">Read More &rarr;</a>

          </div>

          <div class="card-footer text-muted">
            Posted on {{ post.last_published_at }}
          </div>

        </div>
    {% endfor %}

{% endblock %}

Notes:

  1. We changed {% for post in page.get_children.specific %} to {% for post in posts %}. posts is now available in blog_page because of BlogPage.get_context method.
  2. After BlogPage handle the HTTP request, posts in context is the collection of the filtered posts, we add it to context object to make the templates can directly iterate it.

Let's run our project

$ docker-compose up -d --build
$ docker-compose logs -f
  1. Visit http://127.0.0.1:8000
  2. Visit http://127.0.0.1:8000/category/programming/
  3. Visit http://127.0.0.1:8000/category/test/
  4. Visit http://127.0.0.1:8000/tag/django/
  5. Visit http://127.0.0.1:8000/tag/test/

You might need to change the url a little bit, after the test, you will see the route is working.

Reversing route urls

Next, let's try to update the Category widget and Tag widget in the sidebar to make the URL work with route of the BlogPage

Update blog/templatetags/blogapp_tags.py and add blog_page to the context.

from blog.models import BlogCategory as Category, Tag
from django.template import Library, loader

register = Library()


@register.inclusion_tag('blog/components/tags_list.html',
                        takes_context=True)
def tags_list(context):
    tags = Tag.objects.all()
    return {
        'request': context['request'],
        'blog_page': context['blog_page'],
        'tags': tags
    }


@register.inclusion_tag('blog/components/categories_list.html',
                        takes_context=True)
def categories_list(context):
    categories = Category.objects.all()
    return {
        'request': context['request'],
        'blog_page': context['blog_page'],
        'categories': categories
    }

Update wagtail_bootstrap_blog/templates/blog/components/categories_list.html

{% load wagtailroutablepage_tags %}

<div class="card my-4">
  <h5 class="card-header">Categories</h5>
  <div class="card-body">
    <div class="row">
      <div class="col-lg-12">
        <ul class="list-unstyled mb-0">
          {% for category in categories %}
            <li>
              <a href="{% routablepageurl blog_page "post_by_category" category.slug %}">
                {{ category.name }}
              </a>
            </li>
          {% empty %}
            'No categories yet'
          {% endfor %}
        </ul>
      </div>
    </div>
  </div>
</div>

Notes:

  1. At the top, we {% load wagtailroutablepage_tags %}
  2. {% routablepageurl blog_page "post_by_category" category.slug %} would help us generate the url of the category. It is very similar with Django reverse

Update wagtail_bootstrap_blog/templates/blog/components/tags_list.html

{% load wagtailroutablepage_tags %}

<div class="card my-4">
  <h5 class="card-header">Tags</h5>
  <div class="card-body">
    {% for tag in tags %}
      <a href="{% routablepageurl blog_page "post_by_tag" tag.slug %}">
        <span class="badge badge-secondary">{{ tag }}</span>
      </a>
    {% empty %}
      No tags yet
    {% endfor %}
  </div>
</div>

Notes:

  1. {% routablepageurl blog_page "post_by_tag" tag.slug %} would help us generate the url of the tag.

Now the category and tag link in the sidebar would seem like this.

  1. http://127.0.0.1:8000/category/programming/
  2. http://127.0.0.1:8000/tag/django/

Wagtail Tutorial Series:

  1. Create Wagtail Project
  2. Dockerizing Wagtail App
  3. Add Blog Models to Wagtail
  4. How to write Wagtail page template
  5. Add Bootstrap Theme to Wagtail
  6. How to use StreamField in Wagtail
  7. Wagtail Routable Page
  8. Add pagination component to Wagtail
  9. Customize Wagtail Page URL

The source code is available on https://github.com/AccordBox/wagtail-bootstrap-blog


Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, Wagtail CMS and React.

He has published some ebooks on leanpub and tech course on testdriven.io.

He is also the founder of the AccordBox which provides the web development services.


Table of Contents

Search

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More

Subscribe

Get notified about new great Web Development Tutorial