File router#
The file router scans your project for page.py and template.djx files and generates Django URL patterns from the directory tree.
This page covers every route shape the router accepts, how URL parameters are captured and typed, how URL names are computed, and how to mount several routers side by side.
Overview#
A directory under a configured page root becomes a URL segment.
A directory with a page.py becomes a navigable URL.
A directory with only a template.djx becomes a virtual route that renders static markup.
A bracketed segment such as [slug] is captured as a URL parameter and exposed to the page through the dependency resolver.
The router does not need an entry in urls.py per page.
Adding a directory adds a URL.
Renaming a directory renames the URL and its computed URL name.
Removing a directory removes the URL.
Route shapes#
The router recognises four directory shapes.
- Plain segment.
A directory name without brackets becomes a static URL segment.
routes/blog/page.pyanswers/blog/.- Captured segment.
A directory wrapped in single brackets becomes a captured URL parameter.
routes/posts/[slug]/page.pyanswers/posts/<str:slug>/.- Typed captured segment.
A captured directory with a converter prefix sets the Django path converter.
routes/posts/[int:post_id]/page.pyanswers/posts/<int:post_id>/.- Wildcard segment.
A directory wrapped in double brackets becomes a
pathconverter that swallows multiple URL segments.routes/api/[[suffix]]/page.pyanswers/api/<path:suffix>/.
The following layout shows the four shapes together.
routes/
page.py /
blog/
page.py /blog/
posts/
[slug]/
page.py /posts/<str:slug>/
[int:post_id]/
page.py /posts/<int:post_id>/
api/
[[suffix]]/
page.py /api/<path:suffix>/
Captured parameters#
The bracket syntax accepts every Django path converter.
Bracket |
Generated converter |
When to use |
|---|---|---|
|
|
Any non empty value with no slash. |
|
|
Non-negative integers, including zero. |
|
|
URL slugs of ASCII letters, digits, hyphens, and underscores. |
|
|
Canonical UUID strings. |
|
|
Wildcard that matches one or more segments including slashes. |
A bracket label is passed to Django verbatim, so any converter registered with django.urls.register_converter() works in [label:name].
The parser handles three bracket forms.
The typed captured segment is the captured form with a converter prefix, as covered in URL router.
The [[name]] wildcard requires at least one character.
A request to the parent path with no trailing segment, such as /api/ for an api/[[suffix]]/ route, does not match, because the Django path converter never captures an empty string.
Captured values reach Python through markers.
DUrl[T] parses the captured value into the requested type and provides it to context functions and action handlers.
Hyphens in directory names are normalised to underscores in the generated URL parameter and URL name.
A routes/[my-id]/page.py route becomes the Django parameter <str:my_id>, the resolver provides it as my_id, and the URL name registers as next:page_my_id.
Name your directories without hyphens when you want the parameter name and the directory name to match exactly.
from next import context
from next.urls import DUrl
from notes.models import Note
@context("note")
def fetch_note(post_id: DUrl[int]) -> Note:
return Note.objects.get(pk=post_id)
DUrl[int] reads the captured segment whose name matches the parameter, so post_id resolves the [int:post_id] segment and the marker coerces it to int.
See Dependency injection for the full set of DUrl forms and the coercion table.
Virtual routes#
A directory that contains a template.djx without a page.py still becomes a URL.
The router renders the template directly without invoking a Python page module.
routes/
about/
template.djx /about/
legal/
privacy/
template.djx /legal/privacy/
Virtual routes are useful for marketing pages, static content, and quick mockups.
A virtual route can still receive layout wrapping from any ancestor layout.djx.
Every view the router generates, virtual routes included, carries a next_page_path attribute naming the page source.
The form dispatcher reads it when it resolves a posted origin back to the page that should re-render after a validation failure, see Validation and re-render.
URL names#
Every page receives a URL name in the next namespace.
The name is computed from the URL path with the leading slash removed and each segment separated by an underscore.
Captured segments contribute their parameter name without the brackets.
File |
URL name |
|---|---|
|
|
|
|
|
|
|
|
|
|
The trailing underscore on the root page name is intentional.
reverse('next:page_') resolves the root page.
A typed captured segment keeps its converter label in the URL name.
[int:post_id] becomes posts_int_post_id, not posts_post_id, because the name is computed from the raw segment text.
The page_ prefix comes from the URL_NAME_TEMPLATE setting.
Its default is page_{name}, where {name} is the underscore-joined path computed above.
Set it to change the prefix for every file-routed page at once.
NEXT_FRAMEWORK = {
"URL_NAME_TEMPLATE": "route_{name}",
}
With this value routes/blog/page.py registers as next:route_blog.
The placeholder {name} must appear in the template so each page still gets a distinct name.
Reverse them through the standard {% url %} tag or with page_reverse.
See URL reversing for the Python side.
Page roots#
The router resolves routes from app directories and project directories, in the same way staticfiles resolves static files, with one fallback shape.
- App directories.
When
APP_DIRSisTruethe router scans each installed application for a directory namedPAGES_DIR.PAGES_DIRis required on every backend entry, and thenext.E024system check fails when the key is missing. In the tutorial it is set topages, so the router scansnotes/pages/. Applications are resolved through Django’s application registry, so anINSTALLED_APPSentry written as anAppConfigpath such asnotes.apps.NotesConfigscans the samenotes/pages/directory as the plainnotesentry, and both are reported asApp 'notes'. The scanned directory is the one the registry reports for the application, so anAppConfigthat declares its ownpathmoves the scan with it, and an application shipped as a namespace package is scanned like any other. Django’s own applications andnextitself are never scanned, so the packages they ship cannot contribute routes.A directory reachable both as an application tree and through
DIRSis routed twice, once under each mount, which thenext.E015system check reports as a URL pattern conflict. The page checks report each file once, however many mounts reach it.- Project directories.
The
DIRSlist adds absolute or project-relative paths to the scan. The router walks each directory in order and registers everypage.pyandtemplate.djxit finds.- Fallback root.
With
APP_DIRSset toFalseand no usable path entry inDIRS, the router falls back to scanningBASE_DIR / PAGES_DIR. This shape serves a project that keeps every page in a single tree without a page-bearing application.
NEXT_FRAMEWORK = {
"PAGE_BACKENDS": [
{
"BACKEND": "next.urls.FileRouterBackend",
"APP_DIRS": False,
"DIRS": [],
"PAGES_DIR": "pages",
"OPTIONS": {"context_processors": []},
}
]
}
With this configuration the router serves every page under <BASE_DIR>/pages/.
You can use both sources at once.
URL patterns are built in this order, first from application directories then from each entry in DIRS.
If two routes convert to exactly the same Django path string the system check next.E015 reports the conflict, whether they come from one tree or several.
The comparison is string equality after bracket conversion, so semantic overlap between typed converters stays silent, and posts/[int:id] next to posts/[id] is not reported.
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
NEXT_FRAMEWORK = {
"PAGE_BACKENDS": [
{
"BACKEND": "next.urls.FileRouterBackend",
"APP_DIRS": True,
"DIRS": [str(BASE_DIR / "chrome")],
"PAGES_DIR": "routes",
"OPTIONS": {
"context_processors": [
"myapp.context_processors.global_context",
],
},
}
]
}
The OPTIONS block accepts a list of Django context processor paths.
Each processor contributes values to every template that the router renders.
DIRS entry types#
Each entry in DIRS is classified by next.utils.classify_dirs_entries before the router uses it.
- Path entry.
An absolute path, or a relative path that resolves to an existing directory under
settings.BASE_DIR. The router walks this directory as an additional page root alongside the application directories.- Segment entry.
A plain string such as
"api"or"_internal"that does not resolve to an existing directory. The router adds it to the set of directory names it skips during the file walk, preventing those directories from becoming URL segments. This is an alternative to the automatic_componentsskip that comes fromCOMPONENT_BACKENDS.
NEXT_FRAMEWORK = {
"PAGE_BACKENDS": [
{
"BACKEND": "next.urls.FileRouterBackend",
"DIRS": ["_drafts"],
"APP_DIRS": True,
"PAGES_DIR": "routes",
"OPTIONS": {"context_processors": []},
}
]
}
In the example above, any directory named _drafts under any application’s page root is silently skipped.
No URL is registered for it and the file walk does not descend into it.
Components folder skipping#
The router shares its file walk with the components backend.
The name set in the first COMPONENT_BACKENDS entry under COMPONENTS_DIR becomes a directory that the router does not enter.
The default is _components.
Only that exact name is skipped, not every directory that starts with an underscore.
Multiple backends#
The settings list accepts more than one backend.
Each backend can read from a different directory, register a different PAGES_DIR, or use a custom subclass.
NEXT_FRAMEWORK = {
"PAGE_BACKENDS": [
{
"BACKEND": "next.urls.FileRouterBackend",
"DIRS": [],
"APP_DIRS": True,
"PAGES_DIR": "routes",
"OPTIONS": {"context_processors": []},
},
{
"BACKEND": "next.urls.FileRouterBackend",
"DIRS": [],
"APP_DIRS": True,
"PAGES_DIR": "admin_routes",
"OPTIONS": {"context_processors": []},
}
]
}
Two backends produce two independent sets of URLs. Resolution preserves the concatenated list order. The first match wins. Both backends emit the same signals and follow the same naming rules.
Resolution performance#
URL resolution scales with the depth of the request path, not with the number of registered pages.
A static route resolves through one dictionary lookup, and a parameterised route resolves through a walk over a trie of path segments that yields a handful of candidates.
The final match runs through standard Django pattern resolution, so converters and first-match-wins ordering behave as in plain Django.
Setting URL_RESOLVER in NEXT_FRAMEWORK to "django.urls.resolvers.URLResolver" restores the plain linear scan.
See URL router for the algorithm and Settings for the setting.
Common patterns#
Single page application root#
A single routes/page.py registers the empty path /.
The router treats it as the default URL for the project.
Static content section#
Use virtual routes for marketing pages and legal copy.
The directory holds only a template.djx, no Python required.
Per project page tree#
Place a layout and one page.py under chrome/ and add chrome to DIRS.
The result is a project-level shell that wraps every application page.
See Multi-project setup for the full pattern.
Hot reload#
A backend that reads from a database or other dynamic source needs to rebuild its pattern list when the data changes.
router_manager.reload() rebuilds every backend and clears the Django URL resolver cache, and the call is idempotent.
Each invocation emits a router_reloaded signal with the manager class as sender, so long lived processes can listen for it to refresh cached URL references.
Rebuilding drops the cached patterns, so the next request walks every page tree configured in PAGE_BACKENDS again.
A burst of model writes that each triggers a reload can dominate that request.
Receivers should debounce or batch invocations when one logical change triggers many model signals at once.
See Reload routes from code for the model-signal receiver that triggers the reload.
System checks#
The router contributes Django system checks that validate the configuration at startup.
check_next_pages_configurationvalidates theNEXT_FRAMEWORKstructure and each backend entry.check_pages_structurevalidates directory naming, captured parameter syntax, and the presence ofpage.pyortemplate.djx.check_page_functionsreports next.E012 when a directory has neither a render function nor a template.check_pages_structureandcheck_page_functionscome fromnext.pagesand appear here because they validate the same page tree the router scans.check_url_patternsreports two routes that convert to exactly the same Django path string, whether they come from one tree or several (next.E015). The comparison is string equality after bracket conversion, soposts/[int:id]next toposts/[id]is not reported. While collecting patterns it also reports a route that repeats a captured parameter name (next.E028), listing every conflicting name, so parameter duplicates surface from every scanned tree.check_reverse_name_collisionsfails when two distinct routes collapse to the same reverse URL name after separator normalisation, such asfoo-barnext tofoo_bar(next.E039).
Run them through uv run python manage.py check.
A clean exit confirms that every page resolves and every name is unique.
Extension points#
Three surfaces let you replace or augment the router.
next.urls.backends.RouterBackendis the abstract contract for any source of URL patterns.next.urls.backends.FileRouterBackendis the default file-based implementation.next.urls.backends.RouterFactory.register_backendmaps a dotted path to a custom backend.
Subclass FileRouterBackend to add additional patterns or augment URL names without writing a backend from scratch.
See Extending for a worked example.
Database driven routes#
A hybrid backend combines file routes with routes built from database rows.
Subclass FileRouterBackend and override generate_urls to call super().generate_urls() for the file routes, then append one named pattern per row.
The base method returns a fresh list on every call, so appending to it never mutates the cached file routes.
Register the backend in PAGE_BACKENDS and call router_manager.reload() from a model signal so the row-derived patterns rebuild when the data changes.
See Write a router backend for the full worked recipe.
See also#
See also
URL reversing for building URLs in Python and templates. Add a page for a one page recipe. Reload routes from code for hot reload mechanics. URL router for the parser, dispatcher, and signal flow. URLs reference for the public API.