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
Page
usingRoutablePageMixin
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.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.
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 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.
The source code of this Wagtail tutorial is available on Github, you can get it here wagtail-bootstrap-blog, and I would appreciate that if you could star my repo.
You can also check the full list of my wagtail tutorial here wagtail tutorial series