Headless Wagtail CMS: Serializing RichText Stream Blocks
Published November 29, 2023
This video features Kalob Taulien at Wagtail CMS 2023 .
Tutorial: https://learnwagtail.com/course/routable-pages-categories-and-years/
Wagtail for Beginners Course: https://learnwagtail.com/wagtail-for-beginners/
Let's take a look at how we can filter Wagtail CMS blog posts by their URL using a Routable Page Mixin and a couple of @route's.
Previously we made a Blog Listing Page with the URL of /blog/. In this tutorial we'll add /blog/category/{category_name}/ and filter pages based on the slug in the URL.
For more free tutorials: https://learnwagtail.com/tutorials/
Don't forget to subscribe to this channel and follow me on Twitter at https://twitter.com/kalobtaulien
Git Commit: https://github.com/CodingForEverybody/learn-wagtail/commit/1e21b345cd4f8cdfbc1a3fdd9bb53234aaacb820
Part 1 of this video: https://www.youtube.com/watch?v=W2oHKiAYl3U
Learn Wagtail from scratch with the official Wagtail for Beginners Course
https://learnwagtail.com/wagtail-for-beginners/
Used in this video: Wagtail 2.5.1, Python 3.7, Django 2.2.2
#Wagtail #Django #Python
Wagtail’s RoutablePageMixin can add dynamic sub-URLs to an existing page, such as `/blog/category/coding`, instead of relying on query-string filters. The speaker shows how to capture category slugs with regex routes, look up and validate the category, filter blog detail pages, and generate links with Wagtail’s routable-page URL helpers. The same approach can capture a year, a year and month, or a named route such as `July-2019`, while noting that missing categories, templates, and route-specific filtering must be handled by the application.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
Hello and welcome to another lesson on Learn Wagtail with your host Caleb Tallin and today's co-host, Routable Page Mixin. Hello everybody. So once upon a time, a long time ago, well I guess not that long ago, but a while ago, I made a video about using routeable pages and the routeable page mix-in. And that was a way for us to create views off of existing pages. And as an example, if I just open up uh not story, it's called Models, there we go, blog models, uh routable page, there it is. We made one routable page mix-in, or just one route rather, and we called it latest, and all it did was get the latest post. But since then a lot of people have been asking, hey Caleb, how do I go about creating categories or filtering by year?
And so in this video what I want to do is I want to explore creating a custom category name in here. And maybe if we have some time also go over adding a custom year and maybe a month or something like that so that we can really start customizing our views. So if you're unfamiliar with a routable page or a routable page mix-in, or routes, or routes, if you say it that way, uh you are going to want to watch that initial video first. It's all about routable page mix-ins and and all that good stuff. So you can go and find that video and watch that one first. And that will give you the context of how we set all of this up. In this video we're just sort of going to jump into it assuming that we already have a routable page set up Alright, before we get started, don't forget you can always subscribe, hit that little bell underneath the video, turn on your notifications so that you see alerts and notifications when new videos like this come out.
And now for your feature presentation. Now the first thing I'm going to need to do is start my server. And I'm going to uh I'm going to go to my website, so jump. My site, pipbenv, shell, and then lastly, manage. py run soiva. And you can see here it's running on port 8000 So I boot up my browser and I go to localhost port 8000. And here we have our super ugly website with a super cool blog, and we're going to be working in the blog today. In the blog, we already have somewhat of a filter here where we have question mark, you can actually see it at the bottom left here. Question mark category is equal to coding, question mark category
is equal to people skills. Let's go ahead and change that so that your URL is going to look more like slash blog slash category slash coding, something like that. Now the first thing we're going to need to do, oh and by the way, I'm just in my blog app and I'm in models. py. So the first thing we're going to need to do is create a new route. So at route because it's a decorator. And we are going to give this a little bit of reg X. So it starts with R, give it quotations or apostrophes, and we're just going to say if it starts with category, so this is going to match. blog slash category because this is the blog listing page so if I scroll up here
You can see I'm on the blog listing page. And so blog is simply the URL for this Wagtail page. Category is what we're going to automatically filter by. And then we want to be able to give it another actual parameter, a real category here. So we want to do slash and then opening and closing parentheses. And let's do uh Let's say P, let's let's name this. Let's name this cat slug. So category slug. And if my regex brain is working properly today, which it may or may not, We are going to want to do slash W. Oh, it's been a while since I've had to actually properly regex. Ah, we'll debug that in a bit if that doesn't work.
And then lastly we're let's let's just give it a name. So uh I don't know category view. Def category view. And it's going to take self, request, and it's also going to take that fella right there. And we're going to name him Cat Slug so it matches. Give it a doc string. Find blog posts based on a category. And I'm going to copy context here, so we don't actually need the args and quargs. That was overkill from a previous lesson, and we want to simply return a rendered view. So we want to use the blog latest post. html. We should technically change that, but we're not going to.
And then we're going to give it a context and of course we are going to start off with the request so that render always knows what's going on with the request. Now if I save this and my terminal says that there's nothing wrong with this, and I give this a little refresh. That seems fine, but we didn't do anything. What we did was we said, hey, there's going to be a sub-URL called category. Well, this is not going to work, but It is looking for this one, so this should work. What this is basically saying is there's a sub-URL off of the page URL, just happens to be called blog. And we are looking for a dynamic variable in the URL called coding. And there we go. Now we don't actually know if this works, so let's go ahead and let's print this out.
Cat slug. And let's print it out like Seemingly a million times. Life is good in Terminal Land, and let's go ahead and refresh. And let's take a look at our terminal once more here Look at that! Coding, coding, coding, coding, coding! So that is how we get a slug out of the URL with a routable page. Now that right now that doesn't really do a whole lot for us because that's just going to return whatever it's currently returning. So title one and custom article blog page. We actually want a little more than that. So first of all, I think what we should do is check to see if this category actually exists. So you can try something like this. Let's do category is equal to
blog category dot objects dot get slug is equal to cat slug And if that doesn't work, it's going to throw you some sort of exception. I am going to be lazy here and I'm not going to deal with any particular exception Okay, finally got it there. And then uh if there is no category, we could do category is equal to none or something. And then you could also say if category, category is none. I'm gonna say pass, but you could also uh redirect the user to slash blog if you wanted to. Or you could handle it in here. You could do whatever you like in there, but really this is just going to validate that there is a category in here. So let's go ahead and try this out.
And let's go ahead and type in category. So this is just going to Print out whatever this category object is. Actually, let's not do the category object. Let's do the category name. Accept accept The exception there. Let's give this a refresh. And let's see what we are saying. Oh yeah, so here it says coding with a capital C. That's the name of the category. So that's good. That's not the slug. The slug is usually lowercase. So our slug is lowercase and our name is uppercase. A better example of that might not be here. It might be with, for example, people skills. So you see at the bottom left where it says people-skills. Let's go back. Let's change coding to people -skills.
Open up our toy minimal and here we say people skills so we know that our category works and if it does not work so let's put something in here a missing category 404040404 Yep there it is And that's because we didn't handle this properly. So this is basically saying print out category, which is currently none, dot name. So none dot name doesn't exist. Now I'm going to leave that up to you to implement that however way you like. I'm not going to implement a particular redirect or anything like that because your application might be different from my application, but that is a way to deal with this So let's go back to people skills and let's get all of our blog posts that have to do with people skills. So we could say context
And we want to make sure that matches posts here. So let's do context posts is equal to uh blog detail page, that's correct, dot objects. dot filter and what was that called? We probably just called it category. So we got a blog page tag, blog page detail, and in this blog detail page we have ha ha categories Filter categories is in and we're going to give it a list of category And so let's go ahead and save that. Let's open up Firefox and I'm just gonna open up my admin here. And da-da -da-da. Let's go find our blogs. Blog, blog, blog, blog, blogs.
We have a few blogs in here. First blog, article blog, video blog, and blog post too. So let's just make sure we actually have one in here with our proper category. So let's make sure this one is people skills and let's rename this too. People skills blog post and fill that and this did not seem to do the trick and yeah the validation error. I knew that Let's do me. Okay, let's publish. And let's see, where are you? Are you in here? People skills blog post. Cool. That does not do what we want it to do though. So let's go into category slash people dash skills. And here we have people skills blog post.
So the image doesn't show up because we are actually getting the parent page So there is a detail page in here, right? So we've got a blog detail page, but if we keep on scrolling, keep on scrolling, we've got an article blog page, which inherits from the blog detail page. So we need to run dot specific inside of our template. Now that's cool and everything, so let's open up our blog listing page and let's change slash category or not slash category question mark category to let's try self. url. category slash and then let's just do cat slug yep that works we are manually building out this URL at this point Let's go to super cool blog.
And now you can see at the bottom left when you hover over it's going to go to slash blog slash category slash people dash skills or websites or coding. And this will work the way we are expecting it to work. Now, it says latest posts because that's the template we're using. Again, I'm going to leave that in your hands to fix that up. And maybe use a different template if that's what you prefer to do. But here we have some coding. We have people skills. And we have websites. Now you can't really tell by the blog post title because those are poorly named, but that is how we go about creating a blog listing page or a subcategory of or I guess a sub-URL of a blog listing page where it takes a particular category. Now let's go ahead and experiment with a year.
So I'm not going to get too far into this rabbit hole, but I just want to show you how this one's going to work. So the year again is going to start with Reg X. We're going to say it's going to start with a year slash and then again if my regex brain is working properly, I believe it's just slash d plus. I mean I'm I'm I have my fingers crossed here. I hope that's what it is. Your name is just going to be do do do do do do do do uh let's do blogs by year, I guess? Give it a new function. Blogs by year. Self request. And this one is going to take year, so we're not using Cat
slug, we're just going to call it year, and that's just going to be whatever's in the brackets here. Let's go ahead and get our context. Let's go ahead and copy and paste our render. And in here, all I want to do at this point in time is print the year. I mean I'm gonna print it over and over and over again so that we can see that this is actually working inside of our terminal. So let's go back to the blog, say slash year, and let's give it a 2019. Now it's looks like it's not doing a whole lot. But in Terminal Land, we can see that it says 2019 over and over and over again. So that's how we filter by a year. Now if you wanted to actually filter by a year, you could just do something like this.
Where are we here? Plug detail page dot objects dot filter and then you could filter by the year. Let's go ahead and delete most of these and let's give this one more. Let's give this also a month. So if we wanted to add a month in here we could do this. So all I did was add another digit in here. So the first one is going to be the year, the second digit is going to be the month, and we're going to print year and month. And then I refresh my page and this is going to break on me because it says, oh there's nothing matching in there, but what if I said 07 Okay, well again our template looks gross, but that is a pretty minor job to fix. And here it says 2019. 07. So now we have two variables in our URL that we're getting from a routable page.
And again, you could do something like blog detail page. objects. filter, year is equal to year, month is equal to month, something like that. Whichever way you decide to filter is the way you decide to filter. And lastly, since I think we have a couple minutes left before this whole thing is going to end, uh let's let's stack a couple routes in here. So let's again create another regex in here, and let's say we wanted to call this one July dash 2019. And we're going to see that it sort of matches with uh latest here, right? Nope. Now it will. I knew I was going to mess up Red Jax at some point, and let's just call this one
July 2019. Now this is somewhat of a bad example just because uh as soon as the month changes, this is no longer going to be July 2019, so you're going to have to have extra logic in here. You might want to split that out into a different route, but regardless, this is going to work. But before we do that, let's say the default is going to be none. And let's do print July 2019. And let's also print the year. as a variable and let's also print the month as a variable. So it should say July 2019 and then year and then month. The last two are going to be none inside of the terminal so we're gonna save that. Refresh our page. And here it says July 2019.
Oh, that actually says 2019. 07. My bad. What I wanted to do was go to slash blog slash July 2019. Looks like the exact same page, that's good news. And here we can see it says July 2019, none and none as expected. So now you can say in your logic, if the year is none and if the month is none. Just don't filter by the year and month. And simply just get all of them from July 2019 if you wanted to do that. Now one more thing we should go over. Let's uh let's use the routeable page And let's go ahead and copy this. So instead of manually creating out our URL like we originally did, let's go ahead and just replace that with rotable page. We're going to give page or self since they're essentially the same thing. Not p self, but
Self. And instead of latest posts, what did we call this? Blogs by year? No, let's go with category view. And then we are giving it a category slug. So let's go ahead and give it that slug, whatever that slug is going to be. And we know that's going to be cat. slug. Let's go back to our super cool blog and we can see that it is working down there. Awesome. And so all we did here was we said Use a routable page URL from the existing page, the view that we're currently on. Use the one called category underscore view. So we gave it a name, category view. And the only parameter that it takes is cat slug. And that is right here. Cat slug.
Now you could do the same thing with your year if you wanted to pass in a particular year. You could do the year filter. And what did I call that one? Blogs by year. But this is going to look for two parameters in here. So let's go ahead and shave that And you will see at the bottom left it says slash blog slash year slash twenty nineteen slash seven. Now they all say that, and that's no way to live your life when it comes to filtering by category, so let's just undo that. But that's how you would go about using your readable page template tag. The regex part is honestly the hardest part for me at least But once you get past that, and you're able to actually access your variables inside of, well, your logic, inside of your view or your method.
Then life gets a lot easier. Now don't forget you can always check out the source code on github. com. You can go to github. com slash coding for everybody slash learn dash wagtail. And hey, if you like this video, and you maybe want a couple more, you want to watch a few more, I have plenty of these videos, you just have to go to wagtail. io slash course, it will bring you to the YouTube playlist with like 45 or 50 videos on there now, so there's a ton of knowledge on there that you can access at any point in time. But more importantly If you ever get stuck, there are docs on this, so you can always go to docs. wagtail. io and do a search for routable page ', and that will give you Some idea on how this works beyond what this video is currently giving you.
If you like this video, don't forget you can share, subscribe, leave a comment. Give me a suggestion down below about what you would like to see next. I love hearing from everybody. And lastly, my name is Caleb Tolling. Thank you for coming with me on this Rotable Page journey.
Add a routable-page route such as `category/<cat_slug>` and accept the captured category slug in the view. The slug can then be used to render a category-specific blog listing.
Discussed at 2:28Look up the category by its slug with the category model’s `objects.get()` method, handling the case where the lookup fails. If no category is found, you can leave the result empty, redirect, or handle the missing category according to the application’s needs.
Discussed at 6:19Filter the blog detail-page queryset using the selected category, such as filtering where the post’s categories contain that category. If the concrete child page type is needed in the template, call `.specific` so fields and images from the child model are available.
Discussed at 8:38Define a route whose regular expression captures a year and, optionally, a month—for example, `/year/2019/07`—then use those captured values in the view to filter posts by year and month.
Discussed at 11:41Use the routable page’s URL helper or template tag with the named route, such as `category_view`, and pass its `cat_slug` argument. Wagtail then builds the URL from the current page and route definition.
Discussed at 16:20Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published September 19, 2026
Published July 9, 2026
Published May 20, 2026
Published April 16, 2026
Published April 1, 2026
Published March 10, 2026