Разработка кастомных StreamField блоков Wagtail

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомных StreamField блоков Wagtail
Средняя
~2-3 рабочих дня
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

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