Wagtail Routable Page

Table of Contents

Wagtail Tutorial Series:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 
  5. Add Bootstrap Theme to Wagtail

More Wagtail articles and eBooks written by me

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

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_app/settings.py

INSTALLED_APPS = [
    # code omitted for brevity

    'wagtail.contrib.routable_page',                # new

    # code omitted for brevity
]

Update wagtail_app/blog/models.py

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


class BlogPage(RoutablePageMixin, Page):

    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 wagtail_app/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_app/templates/blog/blog_page.html

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags %}

{% block content %}

  {% for post in posts %}
    <div class="mb-4 rounded-lg border border-opacity-75 border-gray-300 shadow-xl overflow-hidden">
      {% if post.header_image %}
        {% image post.header_image original as header_image %}
        <a href="{% pageurl post %}">
          <img src="{{ header_image.url }}">
        </a>
      {% endif %}

      <div class="p-6">
        <h2 class="title-font text-3xl text-blue-900 mb-6">
          <a href="{% pageurl post %}">{{ post.title }}</a>
        </h2>
        <p>
          {{ post.search_description }}
        </p>
        <a href="{% pageurl post %}" class="px-4 py-3 text-white bg-blue-500 border border-blue-500 hover:bg-blue-600 rounded-lg">Read More &rarr;</a>
      </div>

      <div class="bg-gray-100 px-6 py-4">
        <h4 class="text-base text-gray-900">Posted on {{ post.last_published_at }}</h4>
      </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 wagtail_app/blog/templatetags/blogapp_tags.py and add blog_page to the context.

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


@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'],              # new
        'tags': tags
    }


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

{% load wagtailroutablepage_tags %}

<div class="mb-4 border rounded-lg border-opacity-75 border-gray-300 shadow-xl overflow-hidden ">
  <div class="bg-gray-100 text-gray-900 px-6 py-4 ">
    <h4 class="text-base font-medium">Categories</h4>
  </div>
  <div class="px-6 py-4">
    <nav class="list-none">
      {% for category in categories %}
        <li>
          <a href="{% routablepageurl blog_page "post_by_category" category.slug %}" class="text-gray-600 hover:text-gray-800 no-underline hover:underline ">
            {{ category.name }}
          </a>
        </li>
      {% empty %}
        'No categories yet'
      {% endfor %}
    </nav>
  </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_app/templates/blog/components/tags_list.html

{% load wagtailroutablepage_tags %}

<div class="mb-4 border rounded-lg border-opacity-75 border-gray-300 shadow-xl overflow-hidden">
  <div class="bg-gray-100 text-gray-900 px-6 py-4">
    <h4 class="text-base font-medium">Tags</h4>
  </div>
  <div class="px-6 py-4">

    {% for tag in tags %}
      <a href="{% routablepageurl blog_page "post_by_tag" tag.slug %}" class="text-gray-600 hover:text-gray-800">
        <span class="inline-flex items-center justify-center px-2 py-1 mr-2 text-xs font-bold leading-none text-white bg-gray-600 hover:bg-gray-500 focus:bg-gray-700 rounded-full">{{ 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:

To learn more about Wagtail CMS, please check Build Blog With Wagtail CMS (4.0.0)

  1. Create Wagtail Project
  2. Modern Frontend Techs for Wagtail
  3. Dockerizing Wagtail App
  4. Add Blog Models to Wagtail
  5. How to write Wagtail page template
  6. Create Stylish Wagtail Pages with Tailwind CSS
  7. How to use StreamField in Wagtail
  8. Wagtail Routable Page
  9. Add Pagination Component to Wagtail
  10. Customize Wagtail Page URL
  11. Add Full Text Search to Wagtail
  12. Add Markdown Support to Wagtail
  13. Add LaTeX Support & Code Highlight In Wagtail
  14. How to Build Form Page in Wagtail
  15. How to Create and Manage Menus in Wagtail
  16. Wagtail SEO Guide
  17. Online Demo http://wagtail-blog.accordbox.com/
  18. Source code: https://github.com/AccordBox/wagtail-tailwind-blog

Wagtail Tips:

  1. Wagtail Tip #1: How to replace ParentalManyToManyField with InlinePanel
  2. Wagtail Tip #2: How to Export & Restore Wagtail Site

Write style in Wagtail:

  1. How to use SCSS/SASS in your Django project (Python Way)
  2. How to use SCSS/SASS in your Django project (NPM Way)

Other Wagtail Topics:

  1. How to make Wagtail project have good coding style
  2. How to do A/B Testing in Wagtail CMS 
  3. How to build a landing page using Wagtail CMS 
  4. How to support multi-language in Wagtail CMS 
  5. Add Bootstrap Theme to Wagtail

More Wagtail articles and eBooks written by me

Launch Products Faster with Django

SaaS Hammer helps you launch products in faster way. It contains all the foundations you need so you can focus on your product.

Michael Yin

Michael is a Full Stack Developer from China who loves writing code, tutorials about Django, and modern frontend tech.

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.

Django SaaS Template

It aims to save your time and money building your product

Learn More

Build Jamstack web app with Next.js and Wagtail CMS.

Read More
© 2018 - 2024 AccordBox