Разработка кастомных StreamField блоков Wagtail
StreamField — механизм Wagtail, позволяющий редакторам собирать страницы из структурированных блоков произвольного порядка. Стандартная библиотека покрывает базовые случаи: текст, изображение, embed. Кастомные блоки нужны, когда редактор должен вводить структурированные данные — карточку продукта, таблицу цен, блок с иконками и текстом — без выхода за пределы CMS.
Анатомия блока
Каждый блок в StreamField — это Python-класс, наследующий от одного из базовых типов. Простейший кастомный блок:
# blocks.py
from wagtail.blocks import StructBlock, CharBlock, RichTextBlock, URLBlock
from wagtail.images.blocks import ImageChooserBlock
class FeatureCardBlock(StructBlock):
icon = ImageChooserBlock(required=False)
heading = CharBlock(max_length=80)
body = RichTextBlock(features=['bold', 'italic', 'link'])
cta_text = CharBlock(max_length=40, required=False)
cta_url = URLBlock(required=False)
class Meta:
icon = 'pick'
label = 'Карточка преимущества'
template = 'blocks/feature_card.html'
Meta.template указывает HTML-шаблон для рендера на фронтенде. Шаблон получает переменную value с данными блока:
{# blocks/feature_card.html #}
<div class="feature-card">
{% if value.icon %}
{% image value.icon width-64 as card_icon %}
<img src="{{ card_icon.url }}" alt="" class="feature-card__icon">
{% endif %}
<h3 class="feature-card__heading">{{ value.heading }}</h3>
<div class="feature-card__body">{{ value.body }}</div>
{% if value.cta_text and value.cta_url %}
<a href="{{ value.cta_url }}" class="btn">{{ value.cta_text }}</a>
{% endif %}
</div>
StructBlock с вложенными блоками
Блоки можно вкладывать. Типичная задача — секция с заголовком и списком карточек:
from wagtail.blocks import ListBlock
class FeatureSectionBlock(StructBlock):
section_title = CharBlock(max_length=120)
layout = ChoiceBlock(choices=[
('grid-2', '2 колонки'),
('grid-3', '3 колонки'),
('grid-4', '4 колонки'),
], default='grid-3')
cards = ListBlock(FeatureCardBlock())
class Meta:
icon = 'table'
label = 'Секция с карточками'
template = 'blocks/feature_section.html'
ListBlock оборачивает любой блок в динамический список — редактор добавляет/удаляет элементы в UI без ограничений.
StreamField в модели страницы
# models.py
from wagtail.models import Page
from wagtail.fields import StreamField
from wagtail.admin.panels import FieldPanel
from .blocks import FeatureSectionBlock, HeroBlock, TestimonialBlock, VideoEmbedBlock
class ServicePage(Page):
body = StreamField([
('hero', HeroBlock()),
('features', FeatureSectionBlock()),
('testimonials', TestimonialBlock()),
('video', VideoEmbedBlock()),
], use_json_field=True, blank=True)
content_panels = Page.content_panels + [
FieldPanel('body'),
]
use_json_field=True — обязательный параметр начиная с Wagtail 3.0. Данные хранятся в jsonb колонке PostgreSQL, что позволяет делать запросы внутрь структуры через ORM Django.
Миграция:
python manage.py makemigrations
python manage.py migrate
Кастомный StructBlock с валидацией
Когда стандартной валидации полей недостаточно — переопределяем clean():
from django.core.exceptions import ValidationError
from wagtail.blocks import StreamBlockValidationError, StructBlockValidationError
class PricingBlock(StructBlock):
plan_name = CharBlock()
monthly_price = DecimalBlock(min_value=0)
annual_price = DecimalBlock(min_value=0)
features = ListBlock(CharBlock())
def clean(self, value):
cleaned = super().clean(value)
errors = {}
if cleaned['annual_price'] >= cleaned['monthly_price'] * 12:
errors['annual_price'] = ValidationError(
'Годовая цена должна быть меньше суммы 12 месяцев'
)
if len(cleaned['features']) == 0:
errors['features'] = ValidationError(
'Укажите хотя бы одно преимущество тарифа'
)
if errors:
raise StructBlockValidationError(block_errors=errors)
return cleaned
class Meta:
label = 'Тарифный план'
template = 'blocks/pricing.html'
ChooserBlock для связанных объектов
Если блок должен ссылаться на другую страницу или сниппет:
from wagtail.snippets.blocks import SnippetChooserBlock
from wagtail.blocks import PageChooserBlock
class RelatedLinksBlock(StructBlock):
title = CharBlock(max_length=60)
# Ссылка на любую страницу сайта
page = PageChooserBlock(required=False)
# Ссылка на сниппет (например, кейс)
case_study = SnippetChooserBlock('portfolio.CaseStudy', required=False)
# Внешняя ссылка
external_url = URLBlock(required=False)
def clean(self, value):
cleaned = super().clean(value)
links = [cleaned['page'], cleaned['case_study'], cleaned['external_url']]
if not any(links):
raise StructBlockValidationError(
block_errors={'page': ValidationError('Укажите хотя бы одну ссылку')}
)
return cleaned
Блок с кастомным JavaScript в Wagtail Admin
Для сложных блоков иногда нужен собственный виджет в административной панели. Wagtail 5+ поддерживает Stimulus-контроллеры:
from wagtail.blocks import StructBlock
from django import forms
class ColorPickerWidget(forms.TextInput):
class Media:
js = ['admin/js/color-picker.js']
def __init__(self, *args, **kwargs):
kwargs.setdefault('attrs', {})
kwargs['attrs']['data-controller'] = 'color-picker'
super().__init__(*args, **kwargs)
class BrandColorBlock(StructBlock):
label = CharBlock()
color = CharBlock(
form_classname='full',
# кастомный виджет через FieldBlock
)
Для совсем нестандартных интерфейсов — StructBlock с переопределённым get_form_context и кастомным шаблоном для formset.
API и headless-режим
При использовании Wagtail как headless CMS, блоки сериализуются через Wagtail API v2. По умолчанию StreamField отдаётся как массив объектов {type, value, id}. Для кастомных блоков нужно добавить api_representation:
class FeatureCardBlock(StructBlock):
# ...поля...
def get_api_representation(self, value, context=None):
representation = super().get_api_representation(value, context)
# добавляем вычисляемые поля
if value.get('icon'):
img = value['icon']
representation['icon_url'] = img.file.url
representation['icon_srcset'] = img.get_rendition('width-128').url
return representation
Сроки
Один кастомный блок с шаблоном и валидацией — 2–4 часа. Комплект из 8–12 блоков для типичного корпоративного сайта (hero, секции, карточки, testimonials, форма, видео, галерея, таблица цен) — 3–5 рабочих дней с учётом вёрстки шаблонов и тестирования в редакторе.







