Wagtail Multi-Site: Multiple Sites from a Single Codebase
A Django project, one server, any number of sites – how the Sites framework makes this possible
Multiple websites usually mean multiple deployments, multiple databases, multiple maintenance efforts. Wagtail can handle this differently: one project, one database, one deployment – and yet as many domains as you like, each with its own look and its own content types. I built devmaker.net on this foundation from the very beginning. Currently only this blog is live, but the setup carries more: I have tested it with a news ticker and a science portal as the second and third site.
The trick is not a hack, but a core feature of Wagtail.
"The Wagtail Sites framework in one sentence"
Wagtail can manage multiple sites within a single installation. At its core, each site is simply a mapping: hostname (+ port) → root page. When a request comes in, Wagtail looks up the Site table to find a matching hostname and renders the page tree beneath it. Exactly one site is designated as is_default_site – it catches everything that doesn't match any other site.
Schematically (with example hosts for the non-public pages):
Hostname Root-Page Status
-----------------------------------------------------------
devmaker.net HomePage live
news.example.com HomePage Example / not online
papers.example.com PaperHomePage Example / not online
Important: The root pages are completely separate trees. Slugs may be repeated across sites – a /home-automation/ on one site does not collide with a path of the same name on another, because each tree hangs under its own root.
Own page types per domain
Here is the translation:
That is the real lever. Multi-site does not mean "the same thing three times with a different logo". Each site is allowed to use its own page models. A blog and a news ticker can share HomePage/TopicPage/ArticlePage, whereas a science portal gets its own dedicated types for papers:
# pages/models.py
from wagtail.models import Page
class HomePage(Page):
"""Home page for blog and news sites."""
subpage_types = ["pages.TopicPage"]
class ArticlePage(Page):
"""Classic blog/news article (StreamField body)."""
parent_page_types = ["pages.TopicPage"]
class PaperHomePage(Page):
"""Dedicated home page for a science portal."""
subpage_types = ["papers.PaperTopicPage"]
class PaperArticlePage(Page):
"""Scientific paper in plain language."""
parent_page_types = ["papers.PaperTopicPage"]
# ... DOI, authors, original abstract, rewrite ...
Via subpage_types and parent_page_types I enforce that only paper types can be placed under a PaperHomePage – and only blog types under a HomePage. This keeps the two worlds cleanly separated, even though they share the same admin.
Templates: shared where possible – custom where necessary
Wagtail automatically derives the template from the page type: HomePage → home_page.html, PaperHomePage → paper_home_page.html. Shared building blocks (header, footer, the terminal editorial base structure) reside in a base.html that all templates inherit from. Site-specific differences live only in the respective template.
If I do need the same model with a different look per site, a look at the hostname in the context helps:
Here is the translation of the comments within the code into English, while keeping the code structure and logic identical:
```python
class ArticlePage(Page):
def get_template(self, request, *args, **kwargs):
site = request.site if hasattr(request, "site") else None
host = getattr(site, "hostname", "")
if host.startswith("news."):
return "pages/article_page_news.html" # compact news look
return "pages/article_page.html" # detailed blog look
```
**Changes made:**
- `# kompakter News-Look` → `# compact news look`
- `# ausführlicher Blog-Look` → `# detailed blog look`
All other parts of the code remained unchanged, as they are programming syntax and not natural language text.
A reverse proxy, multiple domains
On the network side, it's unspectacular – and that's exactly the point. All domains point via DNS to the same public IP. A reverse proxy in the homelab terminates TLS for each hostname and forwards them to the same Gunicorn upstream. Wagtail decides which site to render based on the Host header:
# One upstream for all domains
upstream wagtail { server 127.0.0.1:8000; }
server {
listen 443 ssl;
server_name devmaker.net news.example.com papers.example.com;
# TLS per host (e.g. via Let's Encrypt / Nginx Proxy Manager)
ssl_certificate /etc/letsencrypt/live/multi/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/multi/privkey.pem;
location / {
proxy_pass http://wagtail;
proxy_set_header Host $host; # <- crucial!
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The one line that matters is proxy_set_header Host $host;. Without the correct host header, Django always only sees the proxy address – and Wagtail falls back to the default site. For Django to accept the hosts at all, they need to be listed in the settings:
Here is the translation of the text into English:
# settings/production.py
ALLOWED_HOSTS = [
"devmaker.net",
"news.example.com",
"papers.example.com",
]
# Behind the reverse proxy: correctly detecting HTTPS
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
**Translation note:**
The only part that required translation was the German comment:
- **Original:** `# Hinter dem Reverse Proxy: HTTPS korrekt erkennen`
- **Translation:** `# Behind the reverse proxy: correctly detecting HTTPS`
All other parts of the code (variable names, strings, and values) remain unchanged, as they are programming syntax and not natural language text.
Locales are shared across all sites
devmaker.net is bilingual (German as default, English in addition) – and the same mechanism applies across all sites. The locales are global, not per site. With wagtail-localize, every page gets a translation tree, and the language prefix is handled via i18n_patterns. This way, the complete translation workflow is shared across all brands – one tooling setup, any number of appearances.
Exactly one site may have is_default_site=True. If you forget this when creating a new site, Wagtail will either not respond to unknown hosts at all or will respond with the wrong page. Symptom: A new domain suddenly displays the blog instead of the expected page.
# What Is Shared – and What Is Not
- Shared: Database, media storage, images, snippets, users & permissions, the admin backend, locales, deployment.
- Separated: Side trees, page types, templates, URLs/slugs, sitemaps per host.
Translate the following text into English, ensuring that the translated text has the same meaning as the original text: The shared media pool is both a convenience and a risk: an image that I upload for one site is also selectable in the admin for the other. With Collections, this can be organized per brand, if necessary.
Could you please provide the text you would like me to translate? It seems only the heading "Lessons Learned" was included, but no actual text to translate. Please share the full text and I will be happy to help! --- *Note: "Lessons Learned" is already in English. If this was the complete text to be translated, then no translation is needed, as it is already in the target language.*
- Multi-Site saves real maintenance effort. One
git pull, one migration, one restart – all sites are up to date. That is the biggest advantage over separate deployments. - Building with multi-site capability from the start is worth it. Even if only one site goes live: the foundation is in place, and adding a second site later is a matter of hours, not days.
- Custom page types are the difference between "genuinely distinct pages" and "themes". Plan them early, not as an afterthought.
- The Host header is the most common source of errors. Most "wrong page is displayed" bugs are related to the reverse proxy, not to Wagtail.
Conclusion
Wagtail Multi-Site is one of the most underrated features of the CMS. Anyone planning multiple projects – or wanting to keep that door open – can save themselves a massive amount of infrastructure with a clean Sites setup, without distorting the sites in terms of content. In my case, the foundation currently carries devmaker.net and is prepared for further appearances: pragmatic, not over-engineered.