wagtail-tutorials-5.png

Wagtail Tutorials #5: Customize Blog Post URL

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

In this chapter, I will talk about how to customize the permanent link of our post page. As you know, RoutablePageMixin can be used to add route function to blog page, so here we continue to dive into RoutablePageMixin to get this job done. For example, we can add publish date of the post to URL so the permanent link would seem like this /blog/2017/06/27/post-page-1/. Which means user can visit the same page from /blog/2017/06/27/post-page-1/ or /blog/post-page-1/

Add date info into blog post url

In the last post, we talked about how to use RoutablePageMixin and route decorator to make our blog page routable, now we keep moving to make the blog have more control of post permanent link.

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

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
        context['search_type'] = getattr(self, 'search_type', "")
        context['search_term'] = getattr(self, 'search_term', "")
        return context

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

    @route(r'^(\d{4})/$')
    @route(r'^(\d{4})/(\d{2})/$')
    @route(r'^(\d{4})/(\d{2})/(\d{2})/$')
    def post_by_date(self, request, year, month=None, day=None, *args, **kwargs):
        self.posts = self.get_posts().filter(date__year=year)
        self.search_type = 'date'
        self.search_term = year
        if month:
            self.posts = self.posts.filter(date__month=month)
            df = DateFormat(date(int(year), int(month), 1))
            self.search_term = df.format('F Y')
        if day:
            self.posts = self.posts.filter(date__day=day)
            self.search_term = date_format(date(int(year), int(month), int(day)))
        return Page.serve(self, request, *args, **kwargs)

    @route(r'^(\d{4})/(\d{2})/(\d{2})/(.+)/$')
    def post_by_date_slug(self, request, year, month, day, slug, *args, **kwargs):
        post_page = self.get_posts().filter(slug=slug).first()
        if not post_page:
            raise Http404
        return Page.serve(post_page, request, *args, **kwargs)

    @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)

There are some points you should notice from the code above. First, many people have no idea if Wagtail support multiple routes, the answer is yes, you can add more than one decorator to the view function to make it handle different url patterns. Here we add multiple routes to post_by_date method, make it can handle different patterns of urls. The view function can filter the posts by year, month and day of publishing date. If you want to blog application support archive, then you must be happy with this feature.

Second, you should understand what Page.serve do here.

All page classes have a serve() method that internally calls the get_context and get_template methods and renders the template. This method is similar to a Django view function, taking a Django Request object and returning a Django Response object

Every page has serve method, so if we want to render some specific blog post, we can just call post.serve and return the response object back. This method sounds very simple here but almost all repos on GitHub about Wagtail blog use a very complex solution by hacking the urls.py. Do not use urls.py if you can do it with RoutablePageMixin

As you can see, in post_by_date_slug we first get the slug from the URL, then find the post which have the slug, if not found, we return 404 HTTP error, if found, we call Page.serve to render the post, the first parameters passed in is the post object instead of blog page object itself, post_page.serve(request, *args, **kwargs) should also work here too

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

Reversing post urls

Now our blog application can handle url which contains the date info of blog post, http://127.0.0.1:8000/blog/2017/06/27/post-page-1/ and http://127.0.0.1:8000/blog/post-page-1/ would return the same HTML. What should I do if we want the links in blog page also have date info?

Since there is no direct way to generate the link, we need to create our own Django template tags now. It is not a big problem if you have no idea what is Django template tags, just follow this article step by step.

Create directory blog/templatetags, create blog/templatetags/__init__.py in directory to make it treated as packages in python, and create blog/templatetags/blogapp_tags.py to edit.

# -*- coding: utf-8 -*-
from django.template import Library, loader
from django.core.urlresolvers import resolve

register = Library()

@register.simple_tag()
def post_date_url(post, blog_page):
    post_date = post.date
    url = blog_page.url + blog_page.reverse_subpage(
        'post_by_date_slug',
        args=(
            post_date.year,
            '{0:02}'.format(post_date.month),
            '{0:02}'.format(post_date.day),
            post.slug,
        )
    )
    return url

The logic here is very simple, but you should know the basic points here. register.simple_tag is used to create custom template tags in Django, the variable passed to the function should be post object, and blog page object. Because we did not set name in the route decorator, so the name passed in blog_page.reverse_subpage is the method name.

Now edit blog/templates/blog/blog_page.html to call the template tags in our template.

{% load wagtailcore_tags blogapp_tags %}

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

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

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

{% endblock %}

In template we first load blogapp_tags to make them available in this template, then we use post_date_url custom tag to help us generate post urls. After work, we can see the post link in the blog page now have publish date info and we can click the post link to ask wagtai to handle the post url for us, which is awesome!

If you do not know what is blog_page, it is a value set in get_context method of BlogPage.

Conclusion

In this chapter, we successfully customized the permanent link of post page, and we also learned how to create Django template tags to help us keep the template code clean and easy to manage.

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 customize-blog-post-url

# 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