Custom Wagtail Snippets Development
Snippets in Wagtail are Django models registered in the admin panel as managed content objects. Unlike pages, snippets have no URL and don't participate in site tree. Used for reusable fragments: menus, banners, contact info, team, partners, testimonials — everything needed in different places regardless of page structure.
Snippet Registration
# models.py
from django.db import models
from wagtail.snippets.models import register_snippet
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
from wagtail.fields import RichTextField
@register_snippet
class Testimonial(models.Model):
author_name = models.CharField('Name', max_length=100)
author_title = models.CharField('Title', max_length=100, blank=True)
author_photo = models.ForeignKey(
'wagtailimages.Image',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+'
)
text = RichTextField('Testimonial text', features=['bold', 'italic'])
rating = models.PositiveSmallIntegerField('Rating', default=5)
is_featured = models.BooleanField('Show on homepage', default=False)
panels = [
MultiFieldPanel([
FieldPanel('author_name'),
FieldPanel('author_title'),
FieldPanel('author_photo'),
], heading='Author'),
FieldPanel('text'),
FieldPanel('rating'),
FieldPanel('is_featured'),
]
def __str__(self):
return f'{self.author_name} — {self.rating}★'
class Meta:
verbose_name = 'Testimonial'
verbose_name_plural = 'Testimonials'
ordering = ['-is_featured', '-id']
ViewSetGroup for Menu Grouping
Starting from Wagtail 4.1 snippets are registered via SnippetViewSet — providing full control over admin behavior:
from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup
from wagtail.snippets.models import register_snippet
class TestimonialViewSet(SnippetViewSet):
model = Testimonial
icon = 'comment'
menu_label = 'Testimonials'
menu_name = 'testimonials'
list_display = ['author_name', 'author_title', 'rating', 'is_featured']
list_filter = ['is_featured', 'rating']
search_fields = ['author_name', 'text']
ordering = ['-is_featured', '-id']
class TeamMemberViewSet(SnippetViewSet):
model = TeamMember
icon = 'user'
menu_label = 'Team'
menu_name = 'team'
list_display = ['full_name', 'position', 'department']
list_filter = ['department']
class ContentSnippetsGroup(SnippetViewSetGroup):
menu_label = 'Content'
menu_icon = 'folder-open-inverse'
menu_order = 300
items = [TestimonialViewSet, TeamMemberViewSet]
register_snippet(ContentSnippetsGroup)
Snippet with Version History
from wagtail.models import DraftStateMixin, RevisionMixin, PreviewableMixin
from wagtail.admin.panels import PublishingPanel
@register_snippet
class SiteAnnouncement(DraftStateMixin, RevisionMixin, PreviewableMixin, models.Model):
title = models.CharField(max_length=200)
body = RichTextField()
show_from = models.DateTimeField(null=True, blank=True)
show_until = models.DateTimeField(null=True, blank=True)
is_dismissible = models.BooleanField(default=True)
panels = [
FieldPanel('title'),
FieldPanel('body'),
FieldPanel('show_from'),
FieldPanel('show_until'),
FieldPanel('is_dismissible'),
PublishingPanel(),
]
def get_preview_template(self, request, mode_name):
return 'previews/announcement.html'
def __str__(self):
return self.title
class Meta:
verbose_name = 'Announcement'
verbose_name_plural = 'Announcements'
DraftStateMixin adds drafts and publishing. RevisionMixin — version history with rollback ability. PreviewableMixin — preview button in editor.
Using Snippets in StreamField
from wagtail.snippets.blocks import SnippetChooserBlock
from wagtail.blocks import StructBlock, ListBlock
class TestimonialsBlock(StructBlock):
heading = CharBlock(max_length=100, required=False)
items = ListBlock(SnippetChooserBlock('content.Testimonial'))
class Meta:
label = 'Testimonials Block'
template = 'blocks/testimonials.html'
In block template, snippet data is directly available:
{# blocks/testimonials.html #}
<section class="testimonials">
{% if value.heading %}<h2>{{ value.heading }}</h2>{% endif %}
<div class="testimonials__grid">
{% for item in value.items %}
{% include "snippets/testimonial_card.html" with testimonial=item %}
{% endfor %}
</div>
</section>
Snippet as Global Settings
Common pattern — snippet for settings editor rarely changes but needed on all pages: contacts, social networks, SEO defaults:
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
@register_setting
class SiteSettings(BaseSiteSetting):
phone = models.CharField('Phone', max_length=30, blank=True)
email = models.EmailField('Email', blank=True)
address = models.TextField('Address', blank=True)
vk_url = models.URLField('VKontakte', blank=True)
telegram_url = models.URLField('Telegram', blank=True)
google_analytics_id = models.CharField('Google Analytics ID', max_length=20, blank=True)
panels = [
MultiFieldPanel([
FieldPanel('phone'),
FieldPanel('email'),
FieldPanel('address'),
], heading='Contacts'),
MultiFieldPanel([
FieldPanel('vk_url'),
FieldPanel('telegram_url'),
], heading='Social Networks'),
FieldPanel('google_analytics_id'),
]
class Meta:
verbose_name = 'Site Settings'
In templates accessible via {% get_settings %} tag:
{% load wagtailsettings_tags %}
{% get_settings %}
<a href="tel:{{ settings.website.SiteSettings.phone }}">
{{ settings.website.SiteSettings.phone }}
</a>
API for Headless
If site uses Wagtail API, snippets must be explicitly registered:
# api.py
from wagtail.api.v2.router import WagtailAPIRouter
from wagtail.api.v2.views import BaseAPIViewSet
from .models import Testimonial
api_router = WagtailAPIRouter('wagtailapi')
class TestimonialsAPIViewSet(BaseAPIViewSet):
model = Testimonial
body_fields = BaseAPIViewSet.body_fields + [
'author_name', 'author_title', 'text', 'rating',
]
listing_default_fields = BaseAPIViewSet.listing_default_fields + [
'author_name', 'rating', 'is_featured',
]
filter_fields = ['is_featured']
api_router.register_endpoint('testimonials', TestimonialsAPIViewSet)
Request returns structured JSON:
GET /api/v2/testimonials/?is_featured=true&order=-rating
Timeline
One snippet with panels, search, and filtering — 2–3 hours. Set of 5–7 snippets for typical corporate website (team, partners, testimonials, settings, menus, banners) with ViewSetGroup setup and API access — 2–3 days.







