We want to add a blog category and register them as a snippet.
class BlogCategory(models.Model):
"""Blog category for a snippet."""
name = models.CharField(max_length=255)
slug = models.SlugField(
verbose_name="slug",
allow_unicode=True,
max_length=255,
help_text="A slug to identify posts by this category"
)
panels = [
FieldPanel("name"),
FieldPanel("slug")
]
class Meta:
verbose_name = "Blog Category"
verbose_name_plural = "Blog Categories"
ordering = ["name"]
register_snippet(BlogCategory)

To be able to put categories inside a blog, we need to add it to their admin field panels.
class BlogDetailPage(Page):
"""Blog detail page"""
template = "blog/blog_detail_page.html"
custom_title = models.CharField(
max_length=100,
blank=False,
null=False,
help_text='overwrite default title'
)
blog_image = models.ForeignKey(
"wagtailimages.Image",
blank=False,
null=True,
related_name="+",
on_delete=models.SET_NULL
)
content = StreamField(
[
("title_and_text", blocks.TitleAndTextBlock(classname="text_and_title")),
("full_rich_text", blocks.RichTextBlock(classname="full_rich_text")),
("my_rich_text", blocks.MyRichTextBlock(classname="my_rich_text")),
("cards", blocks.CardBlock()),
("cta", blocks.CTABlock()),
],
null=True,
blank=True,
use_json_field=True
)
categories = ParentalManyToManyField("BlogApp.BlogCategory", blank=True)
content_panels = Page.content_panels + [
FieldPanel("custom_title"),
FieldPanel("blog_image"),
MultiFieldPanel(
[
InlinePanel("blog_authors", label="Author", min_num=1, max_num=4)
], heading="Author(s)"
),
MultiFieldPanel(
[
FieldPanel("categories", widget=forms.CheckboxSelectMultiple)
], heading="Categories"
),
FieldPanel("content")
]
Now we can see two checkboxes inside of our admin page for a blog detail.
As per usual, it is time to update the template to reflect this addition.
<h2>Categories</h2>
<ul>
{% for cat in self.categories.all %}
<li>
<a href="{{ self.get_parent.url}}?category={{cat.slug}}">
{{cat.name}}
</a>
</li>
{%endfor%}
</ul>
Now to make use out of that url with ?category=name, let’s update the get_context of BlogListingPage:
<h2 class="header-1">Categories</h2>
<ul>
{% for cat in categories %}
<li>
<a href="{{ self.url}}?category={{cat.slug}}">
{{cat.name}}
</a>
</li>
{%endfor%}
</ul>
Now for the next bonus step, we want to create pages for each category that filter out based on the ?category= part of the url!
Simply just alter the get_context page:
def get_context(self, request, *args, **kwargs):
"""Adding Custom stuff to our context."""
context = super().get_context(request, *args, **kwargs)
context["categories"] = BlogCategory.objects.all()
# Get the ?category= value from the URL, if it was there.
category = request.GET.get('category', None)
# If a category was provided, filter by that category
if category is not None:
posts = BlogDetailPage.objects.live().public().filter(categories__slug=category)
else:
posts = BlogDetailPage.objects.live().public()
context['posts'] = posts
return context