Customize Wagtail Page URL

Table of Contents

Wagtail Tutorial Series:

To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)

  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
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. 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 

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 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:

To get the latest learning resource for Wagtail 4, please check Build Blog With Wagtail CMS (4.0.0)

  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
  10. Add Full Text Search to Wagtail
  11. Add Markdown Support to Wagtail
  12. Add LaTeX Support & Code Highlight In Wagtail
  13. How to Build Form Page in Wagtail
  14. How to Create and Manage Menus in Wagtail
  15. Wagtail SEO Guide
  16. 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 

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

This book will teach you how to build a SPA (single-page application) with React and Wagtail CMS

Read More
© 2018 - 2023 AccordBox