Skip to main content

Overview

The blog models provide the foundation for the bakery demo’s blogging functionality. These models include support for blog posts with authors, tags, StreamField content, and a routable index page with tag filtering.

BlogPage

A Blog Page model that represents individual blog posts.
class BlogPage(Page):
    """
    A Blog Page

    We access the Person object with an inline panel that references the
    ParentalKey's related_name in BlogPersonRelationship. More docs:
    https://docs.wagtail.org/en/stable/topics/pages.html#inline-models
    """

    introduction = models.TextField(help_text="Text to describe the page", blank=True)
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
    )
    body = StreamField(
        BaseStreamBlock(), verbose_name="Page body", blank=True, use_json_field=True
    )
    subtitle = models.CharField(blank=True, max_length=255)
    tags = ClusterTaggableManager(through=BlogPageTag, blank=True)
    date_published = models.DateField("Date article published", blank=True, null=True)

Fields

introduction
TextField
Text to describe the page. Optional field that provides a brief introduction to the blog post.
image
ForeignKey
Main image for the blog post. References wagtailimages.Image. Recommended landscape mode with horizontal width between 1000px and 3000px.
body
StreamField
Main content body using BaseStreamBlock. Supports rich content blocks including headings, paragraphs, images, and embeds. Uses JSON field storage.
subtitle
CharField
Optional subtitle for the blog post. Maximum length of 255 characters.
tags
ClusterTaggableManager
Tag management through BlogPageTag model. Allows categorization and filtering of blog posts.
date_published
DateField
Publication date for the article. Optional field used for sorting and display.

Content Panels

content_panels = Page.content_panels + [
    FieldPanel("subtitle"),
    FieldPanel("introduction"),
    FieldPanel("image"),
    FieldPanel("body"),
    FieldPanel("date_published"),
    MultipleChooserPanel(
        "blog_person_relationship",
        chooser_field_name="person",
        heading="Authors",
        label="Author",
        panels=None,
        min_num=1,
    ),
    FieldPanel("tags"),
]

Methods

def authors(self):
    """
    Returns the BlogPage's related people. Again note that we are using
    the ParentalKey's related_name from the BlogPersonRelationship model
    to access these objects. This allows us to access the Person objects
    with a loop on the template. If we tried to access the blog_person_
    relationship directly we'd print `blog.BlogPersonRelationship.None`
    """
    # Only return authors that are not in draft
    return [
        n.person
        for n in self.blog_person_relationship.filter(
            person__live=True
        ).select_related("person")
    ]
Returns a list of live Person objects associated with the blog post through the BlogPersonRelationship.

Properties

@property
def get_tags(self):
    """
    Similar to the authors function above we're returning all the tags that
    are related to the blog post into a list we can access on the template.
    We're additionally adding a URL to access BlogPage objects with that tag
    """
    tags = self.tags.all()
    base_url = self.get_parent().url
    for tag in tags:
        tag.url = f"{base_url}tags/{tag.slug}/"
    return tags
Returns all tags with URLs for accessing filtered blog posts by tag.

API Fields

api_fields = [
    APIField("introduction"),
    APIField("image"),
    APIField("body"),
    APIField("subtitle"),
    APIField("tags"),
    APIField("date_published"),
    APIField("blog_person_relationship"),
]

Page Hierarchy

  • Parent page types: BlogIndexPage
  • Subpage types: None (empty list - no child pages allowed)

BlogIndexPage

Index page for blogs with routable tag filtering functionality.
class BlogIndexPage(RoutablePageMixin, Page):
    """
    Index page for blogs.
    We need to alter the page model's context to return the child page objects,
    the BlogPage objects, so that it works as an index page

    RoutablePageMixin is used to allow for a custom sub-URL for the tag views
    defined above.
    """

    introduction = models.TextField(help_text="Text to describe the page", blank=True)
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
        help_text="Landscape mode only; horizontal width between 1000px and 3000px.",
    )

Fields

introduction
TextField
Text to describe the index page.
image
ForeignKey
Header image for the blog index. Landscape mode recommended with width between 1000px and 3000px.

Methods

def children(self):
    return self.get_children().specific().live()
Returns all live child BlogPage objects.
def get_context(self, request):
    context = super(BlogIndexPage, self).get_context(request)
    context["posts"] = (
        BlogPage.objects.descendant_of(self).live().order_by("-date_published")
    )
    return context
Adds posts to the template context, ordered by publication date (newest first).
@route(r"^tags/$", name="tag_archive")
@route(r"^tags/([\w-]+)/$", name="tag_archive")
def tag_archive(self, request, tag=None):
    try:
        tag = Tag.objects.get(slug=tag)
    except Tag.DoesNotExist:
        if tag:
            msg = 'There are no blog posts tagged with "{}"'.format(tag)
            messages.add_message(request, messages.INFO, msg)
        return redirect(self.url)

    posts = self.get_posts(tag=tag)
    context = {"self": self, "tag": tag, "posts": posts}
    return render(request, "blog/blog_index_page.html", context)
Custom routable view for filtering blog posts by tag. Accessible at /tags/ and /tags/{tag-slug}/.
def get_posts(self, tag=None):
    posts = BlogPage.objects.live().descendant_of(self)
    if tag:
        posts = posts.filter(tags=tag)
    return posts
Returns child BlogPage objects, optionally filtered by tag.
def get_child_tags(self):
    tags = []
    for post in self.get_posts():
        # Not tags.append() because we don't want a list of lists
        tags += post.get_tags
    tags = sorted(set(tags))
    return tags
Returns a sorted list of all unique tags used by child blog posts.

Page Hierarchy

  • Subpage types: BlogPage

BlogPersonRelationship

Defines the many-to-many relationship between BlogPage and Person models.
class BlogPersonRelationship(Orderable, models.Model):
    """
    This defines the relationship between the `Person` within the `base`
    app and the BlogPage below. This allows people to be added to a BlogPage.

    We have created a two way relationship between BlogPage and Person using
    the ParentalKey and ForeignKey
    """

    page = ParentalKey(
        "BlogPage", related_name="blog_person_relationship", on_delete=models.CASCADE
    )
    person = models.ForeignKey(
        "base.Person", related_name="person_blog_relationship", on_delete=models.CASCADE
    )
    panels = [FieldPanel("person")]

    api_fields = [
        APIField("page"),
        APIField("person"),
    ]

Fields

page
ParentalKey
Reference to the BlogPage. Uses related_name="blog_person_relationship" for reverse access.
person
ForeignKey
Reference to the Person model in the base app. Uses related_name="person_blog_relationship" for reverse access.

BlogPageTag

Tag model for creating many-to-many relationships between BlogPage and tags.
class BlogPageTag(TaggedItemBase):
    """
    This model allows us to create a many-to-many relationship between
    the BlogPage object and tags. There's a longer guide on using it at
    https://docs.wagtail.org/en/stable/reference/pages/model_recipes.html#tagging
    """

    content_object = ParentalKey(
        "BlogPage", related_name="tagged_items", on_delete=models.CASCADE
    )

Fields

content_object
ParentalKey
ParentalKey to BlogPage with related_name="tagged_items".

Usage Example

# Get all blog posts ordered by publication date
from bakerydemo.blog.models import BlogPage

posts = BlogPage.objects.live().order_by('-date_published')

# Get blog post authors
for post in posts:
    authors = post.authors()
    for author in authors:
        print(f"{author.first_name} {author.last_name}")

# Get all tags for a blog post
post = BlogPage.objects.first()
tags = post.get_tags
for tag in tags:
    print(f"{tag.name}: {tag.url}")

Build docs developers (and LLMs) love