Настройка мультисайтовости Wagtail
Wagtail поддерживает несколько сайтов в рамках одной установки. Это значит — один Django-проект, одна база данных, одна административная панель, но разные домены с независимыми деревьями страниц. Такая архитектура подходит для холдингов с несколькими брендами, агентств, управляющих сайтами клиентов, и любых случаев, где нужно переиспользовать код, но разделить контент.
Как устроена мультисайтовость в Wagtail
Wagtail хранит конфигурацию сайтов в таблице wagtailcore_site. Каждая запись содержит:
-
hostname— домен -
port— порт (80/443) -
root_page— корневая страница дерева этого сайта -
is_default_site— флаг сайта по умолчанию
При запросе Wagtail смотрит на Host заголовок и выбирает нужный сайт. Остальное происходит автоматически: Page.objects.live() возвращает только страницы текущего сайта при корректной настройке.
Настройка через административную панель
Базовая настройка делается в Settings → Sites:
- Создаём корневую страницу для каждого сайта (обычно
HomePage) - Добавляем запись в Sites с нужным доменом и указываем корневую страницу
- Повторяем для каждого домена
Для локальной разработки с несколькими доменами — прописываем в /etc/hosts:
127.0.0.1 brand-a.local
127.0.0.1 brand-b.local
Программная настройка через data migration
Для воспроизводимой настройки — data migration, а не ручная работа в UI:
# migrations/0002_multisite_setup.py
from django.db import migrations
def create_sites(apps, schema_editor):
Site = apps.get_model('wagtailcore', 'Site')
Page = apps.get_model('wagtailcore', 'Page')
# Получаем корневые страницы, созданные ранее
brand_a_root = Page.objects.get(slug='brand-a')
brand_b_root = Page.objects.get(slug='brand-b')
Site.objects.filter(is_default_site=True).update(
hostname='brand-a.example.com',
root_page=brand_a_root,
)
Site.objects.create(
hostname='brand-b.example.com',
port=443,
root_page=brand_b_root,
site_name='Brand B',
is_default_site=False,
)
class Migration(migrations.Migration):
dependencies = [('website', '0001_initial')]
operations = [migrations.RunPython(create_sites, migrations.RunPython.noop)]
Общие и раздельные модели страниц
Если у сайтов разная структура — создаём отдельные модели для каждого. Если общая — используем одну модель с условной логикой:
# models.py
from wagtail.models import Page, Site
class ArticlePage(Page):
body = StreamField([...], use_json_field=True)
# Поля общие для всех сайтов
def get_context(self, request, *args, **kwargs):
context = super().get_context(request, *args, **kwargs)
site = Site.find_for_request(request)
# Разная логика в зависимости от сайта
context['theme'] = 'brand-b' if 'brand-b' in site.hostname else 'brand-a'
return context
class Meta:
verbose_name = 'Статья'
Мультисайтовые настройки через wagtail-settings
BaseSiteSetting привязывает настройки к конкретному сайту:
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
@register_setting
class BrandSettings(BaseSiteSetting):
logo = models.ForeignKey(
'wagtailimages.Image', null=True, blank=True,
on_delete=models.SET_NULL, related_name='+'
)
primary_color = models.CharField(max_length=7, default='#000000')
footer_text = models.TextField(blank=True)
analytics_id = models.CharField(max_length=30, blank=True)
panels = [
FieldPanel('logo'),
FieldPanel('primary_color'),
FieldPanel('footer_text'),
FieldPanel('analytics_id'),
]
class Meta:
verbose_name = 'Настройки бренда'
В шаблоне автоматически берётся нужный сайт:
{% load wagtailsettings_tags %}
{% get_settings %}
<style>:root { --primary: {{ settings.website.BrandSettings.primary_color }}; }</style>
Разделение медиафайлов по сайтам
По умолчанию медиатека общая для всех сайтов. Для разделения — тегируем изображения:
from taggit.managers import TaggableManager
from wagtail.images.models import AbstractImage, AbstractRendition, Image
class CustomImage(AbstractImage):
site = models.ForeignKey(
'wagtailcore.Site', null=True, blank=True,
on_delete=models.SET_NULL, related_name='images'
)
tags = TaggableManager(through='ImageTag', blank=True)
admin_form_fields = Image.admin_form_fields + ['site']
class CustomRendition(AbstractRendition):
image = models.ForeignKey(
CustomImage, on_delete=models.CASCADE, related_name='renditions'
)
class Meta:
unique_together = [('image', 'filter_spec', 'focal_point_key')]
Настройка роутинга и NGINX
Один uwsgi/gunicorn процесс обслуживает все домены — Django получает Host заголовок и определяет сайт:
# nginx.conf
upstream wagtail {
server 127.0.0.1:8000;
}
server {
listen 443 ssl;
server_name brand-a.example.com;
ssl_certificate /etc/ssl/brand-a.crt;
ssl_certificate_key /etc/ssl/brand-a.key;
location / {
proxy_pass http://wagtail;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 443 ssl;
server_name brand-b.example.com;
ssl_certificate /etc/ssl/brand-b.crt;
ssl_certificate_key /etc/ssl/brand-b.key;
location / {
proxy_pass http://wagtail;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
Wagtail сам разберётся с роутингом по заголовку Host.
Разграничение доступа редакторов
Редакторы разных сайтов не должны видеть чужой контент. Wagtail решает это через группы и коллекции:
from django.contrib.auth.models import Group, Permission
from wagtail.models import GroupPagePermission, Page
def setup_brand_editors(brand_root_page, group_name):
group, _ = Group.objects.get_or_create(name=group_name)
# Разрешение редактировать страницы своего дерева
GroupPagePermission.objects.get_or_create(
group=group,
page=brand_root_page,
permission_type='change',
)
GroupPagePermission.objects.get_or_create(
group=group,
page=brand_root_page,
permission_type='publish',
)
return group
Редактор бренда A видит в дереве страниц только ветку brand-a и не может переходить в ветку brand-b.
Сроки
Базовая настройка мультисайта на 2–3 домена с общими моделями страниц: 1–2 дня. С раздельными настройками брендов, кастомными медиатеками и разграничением доступа: 3–4 дня. Миграция существующего однодоменного сайта в мультисайтовую структуру — отдельная задача: 2–3 дня в зависимости от объёма контента.







