Типичная ситуация: клиент просит не просто страницу «О нас», а целый каталог услуг с ценами, тарифами и фильтрацией. Готовые блоки не подходят — нужны свои поля, вкладки в админке и своя маршрутизация. Решение — кастомные Page Models. Мы используем этот подход в каждом проекте на Wagtail уже более 5 лет и накопили более 30 успешных кейсов. Например, один интернет-магазин потребовал 7 различных типов страниц с уникальными полями и связями — вся разработка заняла 4 дня, а редакторы получили понятный интерфейс с вкладками. В среднем сложный проект требует 5–10 типов страниц. Стандартные поля Wagtail (заголовок, slug, тело) не покрывают потребностей: если втиснуть тарифы, характеристики и отзывы в одно RichTextField, админка превращается в кошмар. На одном проекте редакторы тратили по 20 минут на редактирование одной страницы — после внедрения кастомных моделей время сократилось до 2 минут. Снижение на 90%.
Как кастомные Page Models упрощают жизнь редактору?
Редактору нужна понятная админка. Для этого мы используем TabbedInterface с объектами ObjectList. В примере ниже — модель ServicePage с тремя вкладками. Обратите внимание на InlinePanel для связанных записей и на pricing_panels для отображения цен.
from django.db import models
from wagtail.models import Page, Orderable
from wagtail.fields import RichTextField, StreamField
from wagtail.admin.panels import FieldPanel, InlinePanel, MultiFieldPanel, TabbedInterface, ObjectList
from wagtail.search import index
from modelcluster.fields import ParentalKey
class ServicePage(Page):
intro = models.CharField(max_length=500, blank=True)
body = RichTextField(blank=True)
icon = models.ForeignKey(
'wagtailimages.Image',
null=True, blank=True,
on_delete=models.SET_NULL,
related_name='+',
)
price_from = models.DecimalField(max_digits=10, decimal_places=0, null=True, blank=True)
search_fields = Page.search_fields + [
index.SearchField('intro'),
index.SearchField('body'),
index.FilterField('price_from'),
]
content_panels = Page.content_panels + [
FieldPanel('intro'),
FieldPanel('icon'),
FieldPanel('body'),
InlinePanel('features', label='Особенности'),
]
promote_panels = [
MultiFieldPanel([
FieldPanel('slug'),
FieldPanel('seo_title'),
FieldPanel('search_description'),
], heading='SEO'),
]
pricing_panels = [
FieldPanel('price_from'),
InlinePanel('pricing_tiers', label='Тарифы'),
]
edit_handler = TabbedInterface([
ObjectList(content_panels, heading='Содержание'),
ObjectList(pricing_panels, heading='Цены'),
ObjectList(promote_panels, heading='SEO'),
])
parent_page_types = ['services.ServicesIndexPage']
subpage_types = []
class Meta:
verbose_name = 'Service Page'
class ServiceFeature(Orderable):
page = ParentalKey(ServicePage, on_delete=models.CASCADE, related_name='features')
title = models.CharField(max_length=255)
text = models.TextField(blank=True)
icon = models.CharField(max_length=50, blank=True)
panels = [
FieldPanel('title'),
FieldPanel('text'),
FieldPanel('icon'),
]
Почему RoutablePage лучше отдельных страниц?
Если нужно собрать блог с категориями и тегами, вы можете создать отдельные Page Model для каждой сущности, а можете использовать RoutablePage. Сравните: при наполнении 10 категориями и 20 тегами отдельные страницы потребуют 30 моделей. Одна BlogIndexPage с @path декораторами заменяет их все, сокращая код в 2 раза и упрощая поддержку. Согласно документации Wagtail, RoutablePage позволяет связывать одну страницу с несколькими URL-представлениями.
from wagtail.contrib.routable_page.models import RoutablePage, path, re_path
class BlogIndexPage(RoutablePage, Page):
intro = RichTextField(blank=True)
content_panels = Page.content_panels + [FieldPanel('intro')]
subpage_types = ['blog.BlogPost']
@path('')
def index_view(self, request):
posts = BlogPost.objects.live().child_of(self).order_by('-first_published_at')
from django.core.paginator import Paginator
paginator = Paginator(posts, 12)
return self.render(request, context_overrides={
'posts': paginator.get_page(request.GET.get('page')),
})
@path('category/<str:category_slug>/')
def category_view(self, request, category_slug):
category = get_object_or_404(BlogCategory, slug=category_slug)
posts = BlogPost.objects.live().child_of(self).filter(categories=category)
return self.render(request, context_overrides={
'posts': posts,
'category': category,
}, template='blog/blog_category.html')
@path('tag/<str:tag>/')
def tag_view(self, request, tag):
posts = BlogPost.objects.live().child_of(self).filter(tags__slug=tag)
return self.render(request, context_overrides={
'posts': posts,
'tag': tag,
})
@re_path(r'^archive/(\d{4})/$')
def year_archive(self, request, year):
posts = BlogPost.objects.live().child_of(self).filter(date__year=year)
return self.render(request, context_overrides={
'posts': posts,
'year': year,
})
Как кастомизировать изображения?
Для соответствия SEO и accessibility мы расширяем стандартную модель Image, добавляя поля alt_text и credit. Это позволяет редактору заполнять обязательный alt-текст и указывать автора снимка.
# core/models.py
from wagtail.images.models import AbstractImage, AbstractRendition, Image
class CustomImage(AbstractImage):
alt_text = models.CharField(max_length=255, blank=True)
credit = models.CharField(max_length=255, blank=True)
admin_form_fields = Image.admin_form_fields + [
'alt_text',
'credit',
]
@property
def default_alt_text(self):
return self.alt_text or self.title
class CustomRendition(AbstractRendition):
image = models.ForeignKey(CustomImage, on_delete=models.CASCADE, related_name='renditions')
class Meta:
unique_together = [('image', 'filter_spec', 'focal_point_key')]
get_context — передача дополнительных данных
Метод get_context позволяет обогатить шаблон данными из других таблиц, GET-параметров или форм. Он вызывается при рендеринге страницы.
def get_context(self, request):
context = super().get_context(request)
context['related_posts'] = BlogPost.objects.live().exclude(
pk=self.pk
).filter(
categories__in=self.categories.all()
).distinct().order_by('-first_published_at')[:3]
context['comment_form'] = CommentForm()
context['current_tag'] = request.GET.get('tag')
return context
Сравнение подходов к построению Page Models
| Подход | Применение | Сложность | Скорость поддержки |
|---|---|---|---|
| Базовые поля Page | Простые страницы (About, Contact) | Низкая | Высокая |
| Кастомные поля + панели | Страницы услуг, каталоги | Средняя | Средняя |
| StreamField | Гибкий лендинг, редактор | Средняя | Низкая |
| RoutablePage | Блоги, разделы с фильтрацией | Высокая | Высокая |
| Кастомные изображения | Медиа-сайты, фотоархивы | Низкая | Высокая |
| Тип поля | Пример использования | Админ-панель |
|---|---|---|
CharField |
Заголовок, краткое описание | FieldPanel |
RichTextField |
Основной контент | FieldPanel |
StreamField |
Гибкие блоки (текст, изображения, видео) | StreamFieldPanel |
ForeignKey |
Связь с изображением или категорией | FieldPanel с выбором |
InlinePanel |
Связанные записи (тарифы, особенности) | InlinePanel |
В реальном проекте для интернет-магазина с каталогом из 10 000 товаров мы разработали 7 типов страниц: CatalogPage, ProductPage, BrandPage, PromotionPage, ReviewPage, FAQPage и ContactPage. Для ProductPage использовали StreamField с блоками описания, таблицы характеристик и карусели изображений. CatalogPage построили на RoutablePage с фильтрацией по категориям, цене и тегам. Разработка заняла 4 дня, редакторы обучились за час. Время публикации нового товара сократилось в 10 раз.
Процесс работы над кастомными Page Models
- Анализ — изучаем структуру контента клиента, определяем необходимые поля и связи.
- Проектирование — рисуем ER-диаграмму моделей, продумываем интерфейс редактора (вкладки, inline-панели).
- Реализация — пишем Django модели, настраиваем
edit_handler,search_fieldsиRoutablePage. - Тестирование — проверяем ревизии, черновики, publish workflow, производительность.
- Деплой — выкатываем на продакшен, настраиваем индексацию поиска.
Что входит в результат
- Исходный код кастомных Page Models с документацией на русском.
- Админ-панель с вкладками, валидацией полей и автодополнением.
- Настроенная индексация для поиска по сайту.
- Поддержка ревизий и Workflow для согласования публикации.
- Обучение редакторов (1 час онлайн) и письменная инструкция.
Сроки и гарантии
Разработка 5–7 Page Models с RoutablePage и InlinePanel — 3–6 дней. Стоимость рассчитывается индивидуально в зависимости от сложности. Мы даём гарантию 6 месяцев на все модели: любые баги исправляем бесплатно. Такой подход позволяет сэкономить до 40% бюджета по сравнению с созданием отдельных страниц.
Оценим ваш проект за один день — просто свяжитесь. Наш опыт: более 5 лет работы с Wagtail, более 30 успешных проектов. Получите консультацию — расскажем, как кастомные Page Models решат вашу задачу.







