Customize Wagtail Page URL

Michael Yin

Last updated on October 15 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. Add Date to the PostPage URL
  2. Understand what is cached_property and the benefit

Models

Update blog/models.py

class PostPage(Page):
    # code omitted for brevity

    post_date = models.DateTimeField(
        verbose_name="Post date", default=datetime.datetime.today
    )

    settings_panels = Page.settings_panels + [
        FieldPanel("post_date"),
    ]
  1. We created a post_date field, which store the Post date.
  2. To make it editable in Wagtail admin, we also add it to the settings_panels
  3. Do not forget to add import datetime to the top of the file

Migrate the db

$ docker-compose run --rm web python manage.py makemigrations
$ docker-compose run --rm web python manage.py migrate

Update blog/models.py

class BlogPage(RoutablePageMixin, Page):

    # code omitted for brevity

    def get_posts(self):
        return PostPage.objects.descendant_of(self).live().order_by("-post_date")

    @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(post_date__year=year)
        if month:
            self.posts = self.posts.filter(post_date__month=month)
        if day:
            self.posts = self.posts.filter(post_date__day=day)
        return self.render(request)

    @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
        # here we render another page, so we call the serve method of the page instance
        return post_page.serve(request)

Notes:

  1. Here we added two routes to the BlogPage
  2. post_by_date would make us can check post pages which are published in specific year, month or day.
  3. post_by_date_slug would make us check post page on URL with this pattern /year/month/date/slug.
  4. In post_by_date_slug, please note that because we need to render the PostPage in the BlogPage route, we need to call post_page.serve(request) instead of self.render
  5. We added order_by("-post_date") to the query in get_posts to make it have descending order. Please check Django doc: order-by for more details.

Now the post page should be accessible on url like http://127.0.0.1:8000/2020/12/20/postpage1/

Next, let's display date_slug_url on the index page.

Template

Update blog/templatetags/blogapp_tags.py

@register.simple_tag()
def post_page_date_slug_url(post_page, blog_page):
    post_date = post_page.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_page.slug,
        ),
    )
    return url

Notes:

  1. Here we add a template tag post_page_date_slug_url, we use it to help us generate the date_slug_url of the post page.
  2. Considering we only return url instead of HTML, so we use @register.simple_tag instead of @register.inclusion_tag

Update wagtail_bootstrap_blog/templates/blog/blog_page.html

{% extends "base.html" %}

{% load wagtailcore_tags wagtailimages_tags blogapp_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="{% post_page_date_slug_url post blog_page %}">
              <img src="{{ header_image.url }}" class="card-img-top">
            </a>
          {% endif %}

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

          </div>

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

        </div>
    {% endfor %}

{% endblock %}

Notes:

  1. We load blogapp_tags at the top of the template file.
  2. Replace posturl post with {% post_page_date_slug_url post blog_page %}, so it would run custom template tag we just build
  3. Replace {{ post.last_published_at }} with {{ post.post_date }}

Canonical URL

A canonical URL is the URL of the page that Google thinks is most representative from a set of duplicate pages on your site.

Now the post page can be visited in two patterns.

  1. http://127.0.0.1:8000/2020/12/20/postpage1/
  2. http://127.0.0.1:8000/postpage1/

For better SEO, we will add canonical link.

Update blog/models.py

class PostPage(Page):

    # code omitted for brevity

    def canonical_url(self):
        # we should import here to avoid circular import
        from blog.templatetags.blogapp_tags import post_page_date_slug_url

        blog_page = self.get_parent().specific
        return post_page_date_slug_url(self, blog_page)

Notes:

  1. We added a canonical_url method to the PostPage, which would return the date_slug url of the post page.
  2. Please note that we put the import statement inside the method, to avoid circular import.

Update wagtail_bootstrap_blog/templates/base.html

// code omitted for brevity

<head>
    {% if page.canonical_url %}
      <link rel="canonical" href="{{ page.canonical_url }}"/>
    {% endif %}
</head>

Notes:

  1. If page has canonical_url, then canonical link would be added to the html head

If you check the HTML source code in your browser, you will find HTML like this <link rel="canonical" href="/2020/12/20/postpage1/"/>. Let's change the relative url to absolute url.

Update templatetags/blogapp_tags.py

@register.simple_tag()
def post_page_date_slug_url(post_page, blog_page):
    post_date = post_page.post_date
    url = blog_page.full_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_page.slug,
        ),
    )
    return url

Notes:

  1. In post_page_date_slug_url we changed blog_page.url to blog_page.full_url, which contains the protocol, domain
  2. To make the domain have correct value, we need to config the site in Wagtail admin.

Cached property

Let's review our Django template of the canonical_url

{% if page.canonical_url %}
  <link rel="canonical" href="{{ page.canonical_url }}"/>
{% endif %}

Notes:

  1. page.canonical_url, code in canonical_url would run for the first time.
  2. href="{{ page.canonical_url }}"/>, code in canonical_url would run for the second time.

As you can see, the code in the template caused redundant db query, we can optimize our code here.

Django has provided django.utils.functional.cached_property to help us Django doc: cached_property

The @cached_property decorator caches the result of a method with a single self argument as a property. The cached result will persist as long as the instance does, so if the instance is passed around and the function subsequently invoked, the cached result will be returned.

Update blog/models.py

class PostPage(Page):

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

    @cached_property
    def blog_page(self):
        return self.get_parent().specific

    @cached_property
    def canonical_url(self):
        # we should import here to avoid circular import
        from blog.templatetags.blogapp_tags import post_page_date_slug_url

        blog_page = self.blog_page
        return post_page_date_slug_url(self, blog_page)

Notes:

  1. We create a cached_property blog_page
  2. We create a cached_property canonical_url
  3. In get_context, we set context['blog_page'] with the self.blog_page property

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

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

Read More

Subscribe

Get notified about new great Web Development Tutorial