20 Mar 2026

feedDjango 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.

Waitlist

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):

>>> 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

feedDjango 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

feedDjango community aggregator: Community blog posts

PyCon US 2026 - Elaine Wong & Jon Banafato

🔗 Links

🎥 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