Skip to content

Concepts

This page explains the core mental model behind dj-layouts: what each piece is, how they relate, and how they differ from standard Django template inheritance.

The problem with {% block %}

Classic Django template inheritance looks like this:

base.html        (owns the page structure, has {% block %} holes)
  └── page.html  (fills the holes with content)

This works fine until you want the sidebar to show something dynamic — user info, recent posts, contextual navigation. You end up passing sidebar data from every view that uses base.html, or you reach for template tags that query the database in the template layer.

Panels solve this by moving each region into its own view.

The dj-layouts mental model

Layout class        (owns the page structure, declares which panels to render)
  ├── content view  (your main view — returns its HTML partial)
  ├── sidebar view  (independent view — returns sidebar HTML)
  └── header view   (independent view — returns header HTML)

Each panel is a fully independent Django view. It has its own URL, its own tests, its own caching surface (future). The Layout class wires them together without any of them importing each other.

Core concepts

Layout

A Layout is a Python class (subclass of dj_layouts.Layout) that declares:

  • template — the layout template path (required)
  • Panel attributesPanel(...) instances assigned as class attributes
  • layout_context_defaults — default variables available in the template and to the content view
  • Overridable methods: get_layout_context(), get_template(), on_panel_error()
from dj_layouts import Layout, Panel

class DefaultLayout(Layout):
    template = "myapp/layout.html"
    layout_context_defaults = {"site_name": "My App"}

    sidebar = Panel("myapp:sidebar")
    footer  = Panel("myapp:footer")

Subclassing Layout automatically registers the class under the key "myapp.DefaultLayout". Registration happens at class definition time, and layouts.py modules in every installed app are autodiscovered on startup.

Panel

A Panel is a configuration object that describes how to render one named region in the layout. It is not a view itself — it's the wiring between the Layout and the view that produces the content.

Panel("myapp:sidebar")          # URL name → reversed and called as a view
Panel("myapp:item", context={"pk": 5})  # with extra kwargs
Panel("<p>Static HTML</p>")     # literal string → returned as-is
Panel(url_name="home")          # explicit URL name (no ":" needed)
Panel(None)                     # empty — template fallback is used

See Panels for the full source-type reference.

Content view

The "content" panel is special: it's always the output of your main view — the one decorated with @layout. Every other panel is a separate view called by the layout engine.

@layout("myapp.DefaultLayout")
def homepage(request):
    return HttpResponse("<h1>Welcome!</h1>")

The content view's HTML becomes {% panel "content" %} in the layout template.

Panel view

Any Django view can be a panel view. There are no special base classes or mixins required. A panel view just returns an HttpResponse (or a string) containing its HTML fragment.

def sidebar(request):
    items = MenuItem.objects.all()
    return render(request, "myapp/sidebar.html", {"items": items})

Panel views receive a cloned, GET-only request. request.user, request.session, and cookies are preserved, but POST data is cleared and the view never goes through middleware again. See Security for implications.

Use @panel_only to prevent a panel view from being called directly:

@panel_only
def sidebar(request):
    ...

Layout context

Layout context is a shared dict that flows from the Layout to the content view and is readable (but not writable) in panel views. Use it for data that multiple panels need — the current user's display name, active navigation item, page title, etc.

class DefaultLayout(Layout):
    template = "myapp/layout.html"
    layout_context_defaults = {"site_name": "My App"}

    def get_layout_context(self, request):
        return {"current_user_name": request.user.get_full_name()}

The content view can also write to request.layout_context before the layout template renders:

@layout("myapp.DefaultLayout")
def homepage(request):
    request.layout_context["page_title"] = "Home"
    return HttpResponse(...)

See Layout Context for the full merge order and read/write rules.

Render queues

Render queues let panel views enqueue scripts and stylesheets that are then rendered once in the layout template, deduplicated, and in a guaranteed order (content view first, then panels in definition order).

class DefaultLayout(Layout):
    template = "myapp/layout.html"
    scripts = ScriptQueue()
    styles  = StyleQueue()

See Render Queues for the full API.

Comparison with standard approaches

Classic {% block %} Template tags dj-layouts
Page structure In base template In base template In Layout class
Sidebar data Passed from every view Queried in template tag Sidebar is its own view
Views coupled? Tightly (base ↔ child) Somewhat (tag lives in template) Not at all
Testability Test full page Test tag in isolation Test each view in isolation
Concurrent rendering No No Yes (ASGI + @async_layout)
HTMX partials Manual duplication Manual Built-in — views are already partials
Error isolation One error breaks the page One error breaks the page Per-panel error handling

Request lifecycle

Here is what happens when a request hits a @layout-decorated view:

Browser → Django → @layout wrapper
    1. Sets request.layout_role = "main"
    2. Sets request.is_layout_partial = False
    3. Builds layout_context, sets request.layout_context
    4. Calls your content view → gets HTML partial
    5. For each Panel in the Layout:
         a. Clones request (method=GET, POST cleared, layout_role="panel")
         b. Freezes layout_context on the clone
         c. Calls the panel's source view/callable/literal
    6. (Under @async_layout: steps 5a–5c run concurrently via asyncio.gather)
    7. Renders the layout template with all panel outputs
    8. Returns full-page HttpResponse