Розробка користувацьких 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 робочих днів з урахуванням вёрстки шаблонів та тестування в редакторі.







