Wagtail Custom URL based on slugs using RoutablePageMixin
Asked Answered
A

1

7

I would like to change the URL for a blog entry to include the slug for category (@route(r'^([category-slug]/[post-slug] - e.g. localost/health/health_blog_1).

How would I change the model for this?

class PostPage(RoutablePageMixin, 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
Affable answered 14/1, 2019 at 16:30 Comment(0)
P
12

Leaving RoutablePageMixin aside for a moment, something to understand with Wagtail is that you shouldn't think about URLs the same way as you do with a regular Django website (or Rails or whatever framework which maps routes to views/controllers). With Wagtail, the path to a page is built from the page tree. Therefore, if you want the category to appear in the path, it needs to be part of the tree. In your case, it would be something like this:

HomePage (title: My Website, path: /)
|- CategoryPage (title: Health, path: /health/)
|  |- PostPage (title: Post 1, path: /health/post-1/)
|  |- PostPage (title: Post 2, path: /health/post-2/)
|  \- PostPage (title: Post 3, path: /health/post-3/)
\- CategoryPage (title: Diet, path: /diet/)
|- ...
\- ...

In this scenario, you wouldn't need any further mangling. It's simple and you get category pages for free which can list all the posts within that category. Obviously, the downside of this solution is that a post can only belong to a single category this way.

Now let's see how RoutablePageMixins work and what they can offer. They allow you to create virtual pages (pages that don't exist in the tree). They achieve this by suffixing the path, e.g. you have a BlogIndexPage at /blog/ but you also want to provide yearly archives at /blog/archives/<the-year>/ and tags filtering at /blog/tags/<the-tag>. The way you can think of it (disclaimer: it's not how it actually works) is that when Wagtail receives a request for /blog/archives/<the-year>/, this path won't exist in the tree (nor will /blog/archives/), but /blog/ does, so Wagtail loads you're routable page and checks for a route that would match /archives/<the-year> (because it has already resolved /blog/).

So if you wanted to modify what's before the path of your current page, the RoutablePageMixin must be a part of the declaration of the parent page in the tree. Assuming your PostPagees are direct children of your HomePage like so:

HomePage (title: My Website, path: /)
|- PostPage (title: Post 1, path: /health/post-1/)
|- PostPage (title: Post 2, path: /health/post-2/)
\- PostPage (title: Post 3, path: /health/post-3/)

Then you would include RoutablePageMixin in the declaration of HomePage:

form django.shortcuts import get_object_or_404
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.core.models import Page

class HomePage(RoutablePageMixin, Page):
    # Some fields

    @route(r'^(?P<category_slug>[\w-]+)/(?P<page_slug>[\w-]+)')
    def post_page(self, requets, category_slug, page_slug):
        post = get_object_or_404(PostPage, slug=page_slug, category_slug=category_slug)
        context = {
            'post': post
        }
        return render(request, 'posts/post_page.html', context)

But now you have an issue because your posts technically exist at 2 different URLs, i.e. /<category_slug>/<page_slug>/ (because of the RoutablePageMixin) and /<page_slug>/ (because of its place in the tree). You could live with that and simply set the canonical URL or make your posts redirect to the correct URL:

form django.shortcuts import redirect
from wagtail.core.models import Page

class PostPage(Page):
    # Some fields

    def serve(self, request, *args, **kwargs):
        homepage = self.get_parent().specific
        url = homepage.url + homepage.reverse_subpage('post_page', category_slug=self.category_slug, page_slug=self.page_slug)
        return redirect(url)

That being said, I would advise against using redirect. This is really bending Wagtail against its core principles.

If you did change the URL structure so it doesn't match Wagtail's tree path, you'll also want to consider changing the sitemap urls.

class PostPage(Page):

    def serve(..):
        ....

    def get_sitemap_urls(self, request):
        """Overwrite the url."""
        return [
            {
                "location": '',  # Reverse subpage url
                "lastmod": (self.last_published_at or self.latest_revision_created_at),
            }
        ]
Parasitize answered 14/1, 2019 at 17:44 Comment(1)
Thanks for this explanation, somehow I thought I had to build complex URL structures manually though RoutablePageMixin instead of simply nesting pages within the wagtail structure.Delirious

© 2022 - 2024 — McMap. All rights reserved.