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)

Untitled

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