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 notes.models import Note
from next.pages import context
from next.urls import DUrl
@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.
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 two sources, in the same way staticfiles resolves static files.
- 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/.- 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.
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 resolve to the same Django path the system check next.E015 reports the conflict, whether they come from one tree or several.
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
NEXT_FRAMEWORK = {
"DEFAULT_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 fromDEFAULT_COMPONENT_BACKENDS.
NEXT_FRAMEWORK = {
"DEFAULT_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 DEFAULT_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 = {
"DEFAULT_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. The Django URL resolver checks them in order. The first match wins. Both backends emit the same signals and follow the same naming rules.
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() clears the resolver cache and rebuilds every backend, 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.
The call walks every page tree configured in DEFAULT_PAGE_BACKENDS, so a burst of model writes can dominate the request latency.
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 resolve to the same Django path, whether they come from one tree or several (next.E015).check_duplicate_url_parametersfails when one route repeats a captured parameter name (next.E028).
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.
Register the backend in DEFAULT_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.