Introduction
If you have used some CMS such as WordPress before, you must know that the permanent link of the 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.
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 the quote from wagtail official doc:
A
Pageusing
RoutablePageMixinexists 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 and
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.routable_page.models .RoutablePageMixin and
wagtail.core.models.Page, then create view methods and decorate them with
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.http import Http404, HttpResponse from django.utils.dateformat import DateFormat from django.utils.formats import date_format import wagtail from wagtail.admin.edit_handlers import (FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, PageChooserPanel, StreamFieldPanel) from wagtail.contrib.forms.models import AbstractEmailForm, AbstractFormField from wagtail.contrib.routable_page.models import RoutablePageMixin, route from wagtail.core import blocks from wagtail.core.fields import RichTextField, StreamField from wagtail.core.models import Page from wagtail.embeds.blocks import EmbedBlock from wagtail.images.blocks import ImageChooserBlock from wagtail.images.edit_handlers import ImageChooserPanel from wagtail.snippets.models import register_snippet from blog.blocks import TwoColumnBlock from modelcluster.fields import ParentalKey, ParentalManyToManyField from modelcluster.tags import ClusterTaggableManager from taggit.models import Tag as TaggitTag from taggit.models import TaggedItemBase from wagtailmd.utils import MarkdownField, MarkdownPanel 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.
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_contextmethod 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(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, 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 category 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.