20 Mar 2026
Django community aggregator: Community blog posts
How to Show a Waitlist Until Your Wagtail Site Is Ready

This year, I want to bring my centralized gamified donation platform www.make-impact.org to life (at least technically). Earlier I had the version I was developing separate from the waiting list, but I decided to merge them and have a switch between the waitlist and an early preview.
This allows me to have no data duplication, the possibility to create user accounts immediately, and saves hosting and maintenance costs.
This guide walks through a pattern that lets you ship a temporary waitlist page while your Wagtail site is still being built, with the ability to show your progress to chosen people. If you are building a Software as a Service (SaaS) or a web platform with Django, this article is for you.
The Concept
A custom start page view will check for a specific cookie value. If it is unset, the visitor will be redirected to a waitlist form at /waitlist/. If it is set, the visitor will be served the Wagtail home page.
All views under development will have a decorator that checks the cookie value and redirects to the start page if it is unset.
There will be a special view at /preview-access/ with a passphrase form that allows the visitor to gain preview access by setting the mentioned cookie. This view will also allow preview access to be deactivated.
These are the steps to implement this:
1. Generate and store two secrets
You will need two secret values, either set manually or generated with a cryptographically secure random generator (e.g. Python's secrets module):
PREVIEW_ACCESS_PASSPHRASE- the human-readable passphrase typed into the form. Share this with the people who need site access.PREVIEW_ACCESS_TOKEN- the opaque random value stored in the cookie. Never exposed to users; only the server compares against it.
>>> import secrets
>>> print(secrets.token_urlsafe(16)) # passphrase
dI5nGNftZOBx8m-r0m6glg
>>> print(secrets.token_hex(32)) # cookie token
c1b7a76e3ad5cbfb1657fa4e9885a3c8baa6a5a869f49a136abd0e873a9be9ee
Add both to environment variables or a secrets file untracked by Git, and load them in the Django project settings:
# myproject/settings/_base.py
PREVIEW_ACCESS_PASSPHRASE = get_secret("PREVIEW_ACCESS_PASSPHRASE")
PREVIEW_ACCESS_TOKEN = get_secret("PREVIEW_ACCESS_TOKEN")
The get_secret() here is my custom function to retrieve a secret from the secrets source.
2. Create the access-control decorator
Create myproject/apps/misc/decorators.py. Every protected view will import from here.
# myproject/apps/misc/decorators.py
from functools import wraps
from django.conf import settings
from django.shortcuts import redirect
def preview_access_required(view_func):
@wraps(view_func)
def wrapper(request, *args, **kwargs):
if request.COOKIES.get("preview_access") == settings.PREVIEW_ACCESS_TOKEN:
return view_func(request, *args, **kwargs)
return redirect("misc:home_page")
return wrapper
The decorator compares the cookie against the opaque unguessable token from settings, so unless the token value is known, a random attacker cannot gain access by setting the cookie manually in DevTools.
3. Create the passphrase form
Create myproject/apps/misc/forms.py. The form will have a single required password field. Validation will reject any value that does not match the setting.
# myproject/apps/misc/forms.py
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class PreviewAccessForm(forms.Form):
passphrase = forms.CharField(
label=_("Passphrase"),
widget=forms.PasswordInput(
attrs={"autocomplete": "current-password"}
),
required=True,
)
def clean_passphrase(self):
value = self.cleaned_data["passphrase"]
if value != settings.PREVIEW_ACCESS_PASSPHRASE:
raise forms.ValidationError(
_("Incorrect passphrase.")
)
return value
4. Build the cookie toggle view
Point your browser to /preview-access/. When access is off it shows a passphrase form; when access is on it shows a disable button.
# myproject/apps/misc/views.py
from django.conf import settings
from django.shortcuts import redirect, render
from .forms import PreviewAccessForm
def preview_access(request):
has_access = request.COOKIES.get("preview_access") == settings.PREVIEW_ACCESS_TOKEN
if request.method == "POST":
if has_access:
response = redirect("misc:home_page")
response.delete_cookie("preview_access")
return response
form = PreviewAccessForm(request.POST)
if form.is_valid():
response = redirect("misc:home_page")
response.set_cookie(
"preview_access",
settings.PREVIEW_ACCESS_TOKEN,
httponly=True,
samesite="Strict",
)
return response
else:
form = PreviewAccessForm()
return render(
request,
"preview_access/preview_access.html",
{"has_access": has_access, "form": form}
)
Key points: - Disabling never requires the passphrase - the cookie is already proof of prior access. - The cookie is set with httponly=True (not readable by JavaScript) and samesite="Strict" (not sent on cross-site requests). - The cookie value is the opaque token, not "1", so it cannot be guessed.
The template renders the passphrase input only when not has_access, and shows field-level errors from the form if the passphrase is wrong.
5. Wrap the Wagtail catch-all with the decorator
Replace the default Wagtail catch-all route handler with a thin wrapper that enforces the same cookie check.
# myproject/apps/misc/views.py
from myproject.apps.misc.decorators import preview_access_required
from wagtail.views import serve as wagtail_serve
@preview_access_required
def serve_wagtail_page(request, path=""):
return wagtail_serve(request, path)
Without this, a visitor who knows any Wagtail page URL could bypass the gate by typing it directly into the browser.
6. Build the proxy home page view
This view is the only entry point to the site. It decides what every visitor sees first.
# myproject/apps/misc/views.py
from django.conf import settings
from wagtail.models import Site
from wagtail.views import serve as wagtail_serve
def home_page(request):
if request.COOKIES.get("preview_access") == settings.PREVIEW_ACCESS_TOKEN:
# serve Wagtail directly
site = Site.find_for_request(request)
return wagtail_serve(request, "")
# redirect to waiting list
return redirect("waiting_list")
Key point: the waiting_list view and a Wagtail Site and page must exist and be matched to the request domain before wagtail_serve is called.
7. Wire up the URLs
Django project URL rules:
# myproject/urls.py
from django.conf.urls.i18n import i18n_patterns
from django.urls import re_path
from wagtail.coreutils import WAGTAIL_APPEND_SLASH
from myproject.apps.misc import views as misc_views
if WAGTAIL_APPEND_SLASH:
wagtail_serve_pattern = r"^((?:[\w\-]+/)*)$"
else:
wagtail_serve_pattern = r"^([\w\-/]*)$"
urlpatterns += i18n_patterns(
# ... all your other app URLs above ...
# Catch-all - must be last
re_path(
wagtail_serve_pattern,
misc_views.serve_wagtail_page,
name="wagtail_serve"
),
)
The misc app URLs:
# myproject/apps/misc/urls.py
from django.urls import path
from . import views
app_name = "misc"
urlpatterns = [
path("", views.home_page, name="home_page"),
path("preview-access/", views.preview_access, name="preview_access"),
]
The waiting_list app URLs:
# myproject/apps/waiting_list/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("waitlist/", views.show_waiting_list_form, name="waiting_list"),
]
8. Protect every other app view
Import and apply @preview_access_required to every view that belongs to the real site. Class-based views can be wrapped at assignment time:
from myproject.apps.misc.decorators import preview_access_required
# Function-based view
@preview_access_required
def event_list(request):
...
# Class-based view
event_list = preview_access_required(
EventListView.as_view()
)
Waiting-list views, API views, social authentication views, and static/legal pages (/imprint/, /privacy/, etc.) must not receive this decorator - they need to remain publicly accessible.
Final words
You get a lot of benefits from this setup. The waitlist measures demand for your website while you are still building. Invited test users can evaluate your progress at any time. While you are developing the website, you do not necessarily need multiple servers. Launching later is also easier - no hassle or delays with domain IP updates and SSL certificates.
20 Mar 2026 5:00pm GMT
19 Mar 2026
Django community aggregator: Community blog posts
OpenAI Acquiring Astral: A 4th Option for Funding Open Source
Thoughts on the recent acquisition and what it portends for open source software.
19 Mar 2026 10:56am GMT
18 Mar 2026
Django community aggregator: Community blog posts
PyCon US 2026 - Elaine Wong & Jon Banafato
🔗 Links
- PyCon US website
- Volunteer mailing list
- Elaine on LinkedIn and Jon's personal site
🎥 YouTube
Sponsor
This episode is brought to you by Six Feet Up, the Python, Django, and AI experts who solve hard software problems. Whether it's scaling an application, deriving insights from data, or getting results from AI, Six Feet Up helps you move forward faster.
See what's possible at sixfeetup.com.
18 Mar 2026 10:00pm GMT
