Skip to main content
The people functionality includes both a powerful Person snippet with advanced Wagtail features and PersonPage for public-facing team member profiles.

Person Snippet

A sophisticated snippet demonstrating workflow, draft states, revisions, locking, and preview capabilities.

Model Definition

bakerydemo/base/models.py
class Person(
    WorkflowMixin,
    DraftStateMixin,
    LockableMixin,
    RevisionMixin,
    PreviewableMixin,
    index.Indexed,
    ClusterableModel,
):
    """
    A Django model to store Person objects.
    It is registered using `register_snippet` as a function in wagtail_hooks.py
    
    `Person` uses the `ClusterableModel`, which allows the relationship with
    another model to be stored locally to the 'parent' model until the parent 
    is explicitly saved.
    """
    first_name = models.CharField("First name", max_length=254)
    last_name = models.CharField("Last name", max_length=254)
    job_title = models.CharField("Job title", max_length=254)
    image = models.ForeignKey(
        "wagtailimages.Image",
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="+",
    )

Advanced Mixins

The Person snippet uses multiple mixins to enable advanced features:

WorkflowMixin

Enables Wagtail workflows for approval processes.
workflow_states = GenericRelation(
    "wagtailcore.WorkflowState",
    content_type_field="base_content_type",
    object_id_field="object_id",
    related_query_name="person",
    for_concrete_model=False,
)

DraftStateMixin

Provides draft/live state management.Person records can exist in draft form before being published live.

LockableMixin

Enables locking to prevent concurrent edits.Prevents multiple editors from editing the same person simultaneously.

RevisionMixin

Tracks full revision history.
revisions = GenericRelation(
    "wagtailcore.Revision",
    content_type_field="base_content_type",
    object_id_field="object_id",
    related_query_name="person",
    for_concrete_model=False,
)

PreviewableMixin

Enables preview before publishing.Supports multiple preview modes including blog post context.

ClusterableModel

Enables draft-aware relationships.Relationships can be previewed before saving to the database.

Fields

first_name
CharField
Person’s first name. Required. Maximum 254 characters.
last_name
CharField
Person’s last name. Required. Maximum 254 characters.
job_title
CharField
Person’s job title or role. Required. Maximum 254 characters.
image
ForeignKey
Profile photo for the person. Optional.

Admin Configuration

panels = [
    MultiFieldPanel(
        [
            FieldRowPanel(
                [
                    FieldPanel("first_name"),
                    FieldPanel("last_name"),
                ]
            )
        ],
        "Name",
    ),
    FieldPanel("job_title"),
    FieldPanel("image"),
    PublishingPanel(),
]
  • First and last name appear side-by-side using FieldRowPanel
  • PublishingPanel provides draft/live controls

Preview Functionality

The Person snippet includes sophisticated preview capabilities:
@property
def preview_modes(self):
    return PreviewableMixin.DEFAULT_PREVIEW_MODES + [("blog_post", _("Blog post"))]

def get_preview_template(self, request, mode_name):
    from bakerydemo.blog.models import BlogPage
    
    if mode_name == "blog_post":
        return BlogPage.template
    return "base/preview/person.html"

def get_preview_context(self, request, mode_name):
    from bakerydemo.blog.models import BlogPage
    
    context = super().get_preview_context(request, mode_name)
    if mode_name == self.default_preview_mode:
        return context
    
    # Find a blog post by this author
    page = BlogPage.objects.filter(blog_person_relationship__person=self).first()
    if page:
        page.authors = [
            self if author.pk == self.pk else author for author in page.authors()
        ]
        if not self.live:
            page.authors.append(self)
    else:
        page = BlogPage.objects.first()
        page.authors = [self]
    
    context["page"] = page
    return context
The preview system allows editors to see how a person will appear in different contexts (standalone or as a blog author) before publishing.

Helper Methods

__str__()

String representation combining first and last name.
def __str__(self):
    return "{} {}".format(self.first_name, self.last_name)

thumb_image

Property to get a 50x50 thumbnail for admin listings.
@property
def thumb_image(self):
    try:
        return self.image.get_rendition("fill-50x50").img_tag()
    except:
        return ""

Workflow Integration

Person snippets can be integrated with Wagtail’s workflow system:
  1. Editors create/edit Person records
  2. Changes are saved as drafts
  3. Workflow is triggered for approval
  4. Reviewers can approve or reject changes
  5. Upon approval, the Person is published live
  6. History is tracked via revisions
The GenericRelation for workflow_states and revisions is required when using these mixins on non-Page models.

PersonPage

Public-facing page for team member profiles with social media links.

Model Definition

bakerydemo/people/models.py
class PersonPage(Page):
    """
    Detail view for a specific person
    """
    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)
    location = models.ForeignKey(
        Country,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    social_links = StreamField(
        [("social", SocialMediaBlock())],
        blank=True,
        help_text="Add social media profiles",
    )

Fields

introduction
TextField
Brief introduction or bio for the team member. Optional.
image
ForeignKey
Profile photo or hero image. Should be landscape mode with horizontal width between 1000px and 3000px.
body
StreamField
Full biography and details using BaseStreamBlock.
location
ForeignKey
Team member’s location/country. Links to the Country snippet from the breads app.
Social media profile links using the custom SocialMediaBlock.

Social Media Block

Custom StructBlock for social media profiles:
bakerydemo/people/models.py
class SocialMediaBlock(StructBlock):
    platform = ChoiceBlock(
        choices=[
            ("github", "GitHub"),
            ("twitter", "Twitter/X"),
            ("linkedin", "LinkedIn"),
            ("instagram", "Instagram"),
            ("facebook", "Facebook"),
            ("mastodon", "Mastodon"),
            ("website", "Personal Website"),
        ],
        help_text="Select the social media platform",
    )
    url = URLBlock(
        label="URL",
        help_text="Full URL to your profile (e.g., https://github.com/username)",
    )
    
    class Meta:
        icon = "link"
        label = "Social Media Link"
        value_class = SocialMediaValue
The custom SocialMediaValue provides helper methods:
class SocialMediaValue(StructValue):
    def get_platform_label(self):
        return dict(self.block.child_blocks["platform"].field.choices).get(
            self["platform"]
        )

Admin Configuration

content_panels = Page.content_panels + [
    FieldPanel("introduction"),
    FieldPanel("image"),
    FieldPanel("location"),
    FieldPanel("body"),
    FieldPanel("social_links"),
]

Template Context

The get_context() method enhances social links with readable labels:
def get_context(self, request):
    context = super(PersonPage, self).get_context(request)
    
    platform_block = SocialMediaBlock().child_blocks["platform"]
    platform_labels = dict(platform_block.field.choices)
    social_links = [
        {
            "platform": link.value["platform"],
            "label": platform_labels.get(
                link.value["platform"], link.value["platform"]
            ),
            "url": link.value["url"],
        }
        for link in (self.social_links or [])
    ]
    
    context["social_links"] = social_links
    return context
This transforms the social links into a template-friendly format:
[
    {
        "platform": "github",
        "label": "GitHub",
        "url": "https://github.com/username"
    },
    {
        "platform": "twitter",
        "label": "Twitter/X",
        "url": "https://twitter.com/username"
    }
]

Page Hierarchy

Parent: PersonPage can only be created under PeopleIndexPage.

PeopleIndexPage

Index page listing all team members with pagination.

Model Definition

bakerydemo/people/models.py
class PeopleIndexPage(Page):
    """
    Index page for people.
    Lists all People objects with pagination.
    """
    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.",
    )

Pagination

The index page includes pagination displaying 12 people per page:
def paginate(self, request, *args):
    page = request.GET.get("page")
    paginator = Paginator(self.get_people(), 12)
    try:
        pages = paginator.page(page)
    except PageNotAnInteger:
        pages = paginator.page(1)
    except EmptyPage:
        pages = paginator.page(paginator.num_pages)
    return pages

Helper Methods

get_people()

Returns all live person pages ordered by publication date.
def get_people(self):
    return (
        PersonPage.objects
        .live()
        .descendant_of(self)
        .order_by("-first_published_at")
    )

children()

Returns live child pages with specific types.
def children(self):
    return self.get_children().specific().live()

get_context()

Provides paginated people to the template.
def get_context(self, request):
    context = super().get_context(request)
    people = self.paginate(request, self.get_people())
    context["people"] = people
    return context

Page Hierarchy

Children: PeopleIndexPage can only have PersonPage children.

Usage Examples

Using the Person Snippet

  1. Navigate to Snippets > People in the Wagtail admin
  2. Create a new Person record
  3. Fill in first name, last name, job title
  4. Upload a profile image
  5. Save as draft to preview
  6. Submit for workflow approval (if workflows are configured)
  7. Publish when approved
  8. Use the person in blog posts or recipes via the author chooser

Using PersonPage

  1. Create a PeopleIndexPage under your home page
  2. Add PersonPage instances for each team member
  3. Fill in introduction and body content
  4. Add social media links:
    • Select platform (GitHub, LinkedIn, etc.)
    • Enter full profile URL
  5. Select location/country if applicable
  6. Pages appear in paginated index

Differences: Snippet vs Page

Person Snippet

  • Used for internal references (blog authors, recipe authors)
  • Includes workflow, drafts, locking, revisions
  • No public URL
  • Minimal fields (name, title, image)
  • Preview in context of blog posts

PersonPage

  • Public-facing team member profiles
  • Standard page with URL
  • Rich content with StreamField
  • Social media integration
  • Can be listed, searched, and indexed
You can use both systems together: maintain Person snippets for authorship and PersonPage for public team profiles. They serve different purposes in the content architecture.

Build docs developers (and LLMs) love