Skip to content

Last updated: June 2026

URL Structure

The URL Tree

Django's URL routing is a tree. config/urls.py is the root; each app's urls.py is a branch. A request walks the tree until a pattern matches.

config/urls.py  (root)
├── robots.txt               → robots_txt view
├── sitemap.xml              → sitemap view
├── health/                  → returns "ok" (load-balancer check)
├── stratos/                 ← Django admin (renamed from admin/)
├── i18n/                    ← language switcher POST endpoint
├── [i18n_patterns]
│   ├── /  →                 apps.core.urls
│   ├── projects/  →         apps.projects.urls   # commented until Phase 2
│   ├── blog/  →             apps.blog.urls       # commented until Phase 3
│   └── .../  →              # other apps
└── __debug__/               ← debug toolbar (dev only, import inside if DEBUG)

Root URL Configuration

# config/urls.py
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin
from django.urls import path, include

from apps.core.views import robots_txt
from apps.core.sitemaps import StaticSitemap

sitemaps = {"static": StaticSitemap}

handler400 = "apps.core.views.error_400"
handler403 = "apps.core.views.error_403"
handler404 = "apps.core.views.error_404"
handler429 = "apps.core.views.error_429"
handler500 = "apps.core.views.error_500"

urlpatterns = [
    path("robots.txt", robots_txt, name="robots_txt"),
    path("sitemap.xml", sitemap, {"sitemaps": sitemaps}, name="sitemap"),
    path("health/", lambda _: HttpResponse(b"ok"), name="health"),
    path("stratos/", admin.site.urls),              # renamed from admin/
    path("i18n/", include("django.conf.urls.i18n")),
]

urlpatterns += i18n_patterns(
    path("", include("apps.core.urls")),
    # path('projects/', include('apps.projects.urls')),  # Phase 2
    # path('blog/', include('apps.blog.urls')),          # Phase 3
    prefix_default_language=True,
)

if settings.DEBUG:
    from debug_toolbar.toolbar import debug_toolbar_urls
    urlpatterns += debug_toolbar_urls()
    # Import is INSIDE the if block — critical.
    # A top-level import crashes prod because debug_toolbar is not installed there.

    # Error page previews
    urlpatterns += [
        path("__errors/400/", lambda r: render(r, "400.html", status=400)),
        path("__errors/403/", lambda r: render(r, "403.html", status=403)),
        path("__errors/404/", lambda r: render(r, "404.html", status=404)),
        path("__errors/410/", lambda r: render(r, "410.html", status=410)),
        path("__errors/500/", lambda r: render(r, "500.html", status=500)),
    ]

prefix_default_language=True

URL Language With flag Without flag
/about/ English ✅ works Redirects
/en/about/ English ✅ also works ✅ works
/it/about/ Italian ✅ works ✅ works

With prefix_default_language=True, every language gets the prefix for consistency. Non-prefixed links get redirected.


App-level URL Patterns

Each app defines its own urls.py. The app_name establishes a namespace.

# apps/core/urls.py
from django.urls import path
from . import views

app_name = 'core'

urlpatterns = [
    path('', views.home, name='home'),
    path('about/', views.about, name='about'),
    path('work/', views.work, name='work'),
    path('contact/', views.contact, name='contact'),
    ...
]

URL Namespacing in Templates

The namespace prevents collisions if two apps both define name='index':

<!-- Without namespace — ambiguous if multiple apps define 'home' -->
<a href="{% url 'home' %}">Home</a>

<!-- With namespace — unambiguous -->
<a href="{% url 'core:home' %}">Home</a>

Always use namespaced URLs in templates.


Error Handlers

Handler View Status
handler400 apps.core.views.error_400 Bad request
handler403 apps.core.views.error_403 Permission denied
handler404 apps.core.views.error_404 Not found
handler429 apps.core.views.error_429 Rate limited
handler500 apps.core.views.error_500 Server error

Each renders a branded error page extending 4xx_base.html.