2025, Oct 21 17:00
Fixing Wagtail i18n redirects at DEBUG=False: make 404.html safe to render and restore locale-prefixed routing
Troubleshooting Wagtail i18n in production: DEBUG=False triggers Resolver404 when 404.html uses page. Make 404 template safe to restore locale redirects.
Wagtail i18n can feel rock-solid in development and suddenly bite in production. A typical case: with DEBUG = True, a bilingual site happily auto-redirects the root URL to the correct locale prefix, /en/ or /fr/. Switch to DEBUG = False, hit https://example.com without the prefix, and you get a server error with Resolver404. Manually adding /en/ still works; even /admin fails without the trailing slash, while /admin/ is fine.
Minimal setup that reproduces the behavior
The project uses standard middleware and i18n settings, plus Wagtail’s i18n features.
MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "django.middleware.security.SecurityMiddleware",
    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
]
USE_I18N = True
USE_L10N = True
WAGTAIL_I18N_ENABLED = True
USE_TZ = True
WAGTAILSIMPLETRANSLATION_SYNC_PAGE_TREE = True
URL configuration relies on i18n_patterns with the usual Wagtail routing:
from django.urls import path, include
from wagtail import urls as wagtail_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
# renamed symbol below to show intent without changing behavior
from search import views as search_handlers
urlpatterns = [
    path("admin/", include(wagtailadmin_urls)),
    path("documents/", include(wagtaildocs_urls)),
    path("search/", search_handlers.lookup, name="search"),
] + i18n_patterns(
    path("search/", search_handlers.lookup, name="search"),
    path("", include(wagtail_urls)),
)
What’s really going on
Redirects in this setup are triggered after Wagtail captures 404 responses. That mechanism is key for things like adding the locale prefix automatically. However, the 404 response must actually render. If your 404.html extends a base template that expects context variables that aren’t defined during the pre-redirect phase, rendering the 404 fails, the 404 is never generated, and the redirect never fires. In the case at hand, base.html uses page, but that variable isn’t defined before the redirect step. With DEBUG = True Django’s error pages mask the problem; with DEBUG = False the template error surfaces as a server error instead of a clean 404 plus redirect.
The fix
Make sure 404.html can render without assuming page exists. Wrap any sections that rely on the page object in a conditional so the template is safe during the redirect capture.
Example 404.html before:
{% extends "base.html" %}
{% block content %}
  <h1>Not found</h1>
  <h2>{{ page.title }}</h2>  {# breaks when `page` is undefined #}
{% endblock %}
Example 404.html after:
{% extends "base.html" %}
{% block content %}
  <h1>Not found</h1>
  {% if page %}
    <h2>{{ page.title }}</h2>
    {# any other fragments that depend on `page` #}
  {% endif %}
{% endblock %}
With that guard in place, the 404 page renders cleanly, Wagtail can complete its redirect handling, and the root URL correctly lands on the localized home, for example /en/.
Why this matters
On multilingual Wagtail sites, the initial locale resolution often relies on 404 capture plus redirects. If your error template can’t render in the minimal context used during that flow, you effectively disable the redirect path in production. The result is hard-to-explain behavior differences between DEBUG = True and DEBUG = False, broken root URL visits, and oddities like /admin failing without a trailing slash while /admin/ still works.
Takeaways
Keep 404.html resilient. Don’t assume page or other context variables are present before Wagtail finishes its redirect logic. Wrap page-dependent sections in {% if page %} blocks or otherwise make those sections conditional. Once the 404 page can render without that context, the redirect chain runs as intended and your bilingual routing behaves the same in development and production.