Layouts#

A layout is a layout.djx file in any directory under a page root. The framework wraps every page below it through string substitution into a {% block template %} placeholder. This page covers how layouts are discovered, how the layout chain composes, how to publish layout-level context, and how multiple page backends produce independent layout trees.

Overview#

A page is wrapped by every layout.djx between the page directory and the page root. The closest layout is the innermost wrapper. The root layout is the outermost wrapper. The page body is substituted into a {% block template %}{% endblock template %} placeholder that each layout must contain.

Layouts compose through string substitution, not through Django template inheritance. There is no {% extends %} directive and no parent template identifier. The composition is purely structural, driven by directory placement.

Layout discovery#

The framework walks every ancestor directory upward from the page directory, bounded at 64 levels. Every directory along the way is inspected for a layout.djx file. The collected layouts are composed in order, with the closest layout wrapping the page body and the farthest layout wrapping everything else.

The walk does not stop at the page root. A layout.djx in a directory above the page root, such as the application package directory, also joins the chain and wraps every page below it. Keep layout files inside the page tree unless that wrapping is intended.

layout discovery#
notes/pages/
  layout.djx                outer wrapper for every page
  notes/
    layout.djx              inner wrapper for /notes/* pages
    [id]/
      layout.djx            innermost wrapper for /notes/<id>/* pages
      template.djx
      page.py

A request to /notes/42/ composes three layouts. The innermost wraps the page body. The middle layout wraps the result. The outermost layout wraps the result again.

Layout-only directories#

A page directory that contains a sibling layout.djx but no body source still renders. The body slot is the empty string and the layout chain composes around it. Use this shape for landing routes whose whole markup belongs in the layout shell. next.E012 does not fire in this case.

Layout template contract#

Every layout must contain a {% block template %} placeholder where the body of the wrapped content is substituted. The closing tag can be written either way. {% block template %}{% endblock template %} and {% block template %}{% endblock %} are both recognised.

notes/pages/layout.djx#
<!doctype html>
<html>
  <head>
    <title>{{ site_name }}</title>
    {% collect_styles %}
  </head>
  <body>
    <header>{{ site_name }}</header>
    <main>
      {% block template %}{% endblock template %}
    </main>
    {% collect_scripts %}
  </body>
</html>

Without the placeholder that layout is skipped during composition, so its own markup vanishes from the output while the page body and every inner layout still render, with no error raised. The check_layout_templates system check emits next.W001 for a layout.djx sitting next to a discovered page when it lacks a {% block template %} block. The check scans every page root each router serves, so next.W001 surfaces for layouts under any page-bearing application, not only one of them. A layout in an intermediate segment directory without a sibling page is not covered, so uv run python manage.py check does not catch every broken layout.

Layouts can declare layout-level CSS and JS through the static collector tags shown above. The tags also live in inner layouts when you want a section-scoped style sheet.

Publishing context from a layout segment#

A segment directory that contains a layout.djx can also have a sibling page.py. Use it to publish values that the layout markup needs and, with inherit_context=True, to make those values available to every descendant page.

notes/pages/page.py#
from next import context
from notes.models import Note

@context("site_name", inherit_context=True)
def site_name() -> str:
    return "Notes"

@context("note_count", inherit_context=True)
def note_count() -> int:
    return Note.objects.count()

The inherit_context=True flag publishes the value to every descendant page, whereas without the flag the value is available only when notes/pages/page.py itself handles the request.

Context functions in a segment page.py take dependencies the same way any page does. The resolver shares its cache across the entire layout chain and the leaf page, so a value resolved at an ancestor segment is not recomputed further down.

Nested layout patterns#

Section layout#

Add a layout inside a section to share a sub navigation across every page under that section.

notes/pages/admin/layout.djx#
<section class="admin">
  <nav class="admin-nav">
    <a href="{% url 'next:page_admin' %}">Overview</a>
    <a href="{% url 'next:page_admin_stats' %}">Stats</a>
  </nav>
  {% block template %}{% endblock template %}
</section>

Empty pass through#

Sometimes a directory needs a visual-only layout boundary for context but no extra HTML. Use an empty layout that contains only the placeholder.

notes/pages/api/layout.djx#
{% block template %}{% endblock template %}

A sibling page.py in the same directory can publish inherited context for every page under /api/ without that layout injecting any visible markup.

Section specific static assets#

Co-locate layout.css next to layout.djx to ship styles that apply only to pages under that directory. The static collector emits the file only when a request reaches a page below that layout.

Multiple backends and layout roots#

Two backends that scan different directories produce two separate layout chains. The separation comes from the directories, not from the backends, because layout discovery is purely filesystem-based.

config/settings.py#
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": []},
        }
    ]
}

The first backend reads notes/pages/ and uses notes/pages/layout.djx as its root. The second backend reads notes/admin_routes/ and uses notes/admin_routes/layout.djx as its root. The two chains stay separate only because the two directories share no ancestor that carries a layout.djx. A layout in a shared ancestor directory, such as the notes package directory itself, wraps pages from both backends. Root layouts collected from DIRS are also global. The framework gathers them from every PAGE_BACKENDS entry and appends them to the chain of every page, regardless of which backend discovered the page.

Project level root layout#

Place a root layout outside of any application by adding a project directory to DIRS so a single layout.djx wraps every page across every installed app. See Multi-project setup for the full settings example and the layered-projects pattern.

Cross cutting behaviour#

Context processors#

Context processors are applied after every @context callable finishes but before any template (including the layout chain) renders. They contribute variables that every layout and every page template can use. Because they run last, a processor that returns the same key as a @context function overwrites that value. Processor-to-processor duplicates are resolved by dotted path, keeping the first occurrence. See Context under Resolution order for the full merge order and the first TEMPLATES entry rule. A processor that raises TypeError, ValueError, AttributeError, or KeyError is logged and swallowed by default. Set STRICT_CONTEXT = True to re-raise those exceptions instead. Any other exception type propagates regardless of the setting. Semantics live in Settings.

Static collector#

The static collector runs once per request. Both layouts and the page contribute to the same collection slot. A {% collect_styles %} tag inside an inner layout still emits every collected stylesheet, not only the ones declared in that layout.

Page rendered signal#

The page_rendered signal fires once per request after the layout chain produces the final HTML. Subscribe to observe rendering duration, the number of collected styles and scripts, and the keys present in the template scope. See Signals for the full payload.

Common pitfalls#

Layout markup is missing from the output.

A {% block template %} placeholder is required, closed with either {% endblock %} or {% endblock template %}. Without it the framework drops that layout from the chain and renders the page body without its wrapper.

Inherited context not visible to a sub-page.

The inherit_context=True flag is required on the ancestor page.py that declares the value. Without it the value is not injected into descendant routes.

Two roots produce one composed page.

A page composes its layout chain from its own ancestor directories. A page lives under exactly one backend, even when the file path is also reachable from another backend.

See also#

See also

Pages for the page body sources and the layout placeholder. Context for context inheritance rules. Multi-project setup for project-level layout roots. Page discovery for the composition pipeline.