Wagtail Custom Page Models Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Custom Wagtail Page Models Development

Page Models are the heart of Wagtail. Each page type is a Django model inheriting from Page. Type defines fields, admin editor, template, and routes.

Basic Page Model Structure

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):
    # Fields
    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 indexing
    search_fields = Page.search_fields + [
        index.SearchField('intro'),
        index.SearchField('body'),
        index.FilterField('price_from'),
    ]

    # Editor interface — with tabs
    content_panels = Page.content_panels + [
        FieldPanel('intro'),
        FieldPanel('icon'),
        FieldPanel('body'),
        InlinePanel('features', label='Features'),
    ]

    promote_panels = [
        MultiFieldPanel([
            FieldPanel('slug'),
            FieldPanel('seo_title'),
            FieldPanel('search_description'),
        ], heading='SEO'),
    ]

    pricing_panels = [
        FieldPanel('price_from'),
        InlinePanel('pricing_tiers', label='Pricing Tiers'),
    ]

    edit_handler = TabbedInterface([
        ObjectList(content_panels, heading='Content'),
        ObjectList(pricing_panels, heading='Pricing'),
        ObjectList(promote_panels, heading='SEO'),
    ])

    # Hierarchy constraints
    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)  # icon name

    panels = [
        FieldPanel('title'),
        FieldPanel('text'),
        FieldPanel('icon'),
    ]

RoutablePage — Multiple URLs Per Page

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,
        })

Page with Custom Images

# 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 — Passing Data to Template

def get_context(self, request):
    context = super().get_context(request)

    # Additional data
    context['related_posts'] = BlogPost.objects.live().exclude(
        pk=self.pk
    ).filter(
        categories__in=self.categories.all()
    ).distinct().order_by('-first_published_at')[:3]

    # Comment form
    context['comment_form'] = CommentForm()

    # Data from GET parameters
    context['current_tag'] = request.GET.get('tag')

    return context

Revisions and Drafts

Page Models automatically support revisions and workflow. To enable, only RevisionMixin is needed (already in base Page). Configure stored revisions count:

WAGTAIL_WORKFLOW_MAX_LOCK_EXPIRY_DAYS = 14

# In the model
class BlogPost(Page):
    MAX_NUM_REVIEWERS = 3

Developing 5–7 Page Models with RoutablePage and InlinePanel — 3–6 days.