Customize Wagtail Page URL

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

Models

Update wagtail_app/blog/models.py

import datetime


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 wagtail_app/blog/models.py

from django.http import Http404


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.
  6. Do not forget to add from django.http import Http404 at the top

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

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

Template

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

{% extends "base.html" %}

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

      <div class="p-6">
        <h2 class="title-font text-3xl text-blue-900 mb-6">
          <a href="{% post_page_date_slug_url post blog_page %}">{{ post.title }}</a>
        </h2>
        <p>
          {{ post.search_description }}
        </p>
        <a href="{% post_page_date_slug_url post blog_page %}" 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. Replace posturl post with {% post_page_date_slug_url post blog_page %}, so it would run custom template tag we just build
  2. Replace {{ post.last_published_at }} with {{ post.post_date }}

Now if we test on the blog page, the post page URL would have date info.

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/2022/08/11/postpage1/
  2. http://127.0.0.1:8000/postpage1/

For better SEO, we will add canonical link.

Update wagtail_app/blog/models.py

class PostPage(Page):

    # code omitted for brevity

    def canonical_url(self):
        # we should import here to avoid circular import
        from wagtail_app.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_app/templates/base.html

<head>
    // code omitted for brevity
    {% 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="/2022/10/20/postpage1/"/>. Let's change the relative url to absolute.

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(           # new
        "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 might need to config the site in Wagtail admin.. Because default value is localhost:80

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. code in {% if page.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 wagtail_app/blog/models.py

from django.utils.functional import cached_property


class PostPage(Page):

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

    @cached_property
    def blog_page(self):                                               # new
        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                                     # new
        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:

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