Getting Started
This guide walks you from a fresh Django project to a working layout with a content view and a sidebar panel.
Requirements
- Python 3.11+
- Django 4.2+
Installation
pip install dj-layouts
Add to INSTALLED_APPS
# settings.py
INSTALLED_APPS = [
...
"dj_layouts",
]
That's all the configuration required. Autodiscovery of layouts.py modules starts automatically via AppConfig.ready().
Step 1 — Create a Layout class
Create myapp/layouts.py. Any Layout subclass in a layouts.py file is automatically discovered and registered.
# myapp/layouts.py
from dj_layouts import Layout, Panel
class DefaultLayout(Layout):
template = "myapp/layout.html"
# Declare panels as class attributes.
# "myapp:sidebar" is a namespaced URL name — resolved via reverse().
sidebar = Panel("myapp:sidebar")
Layout subclasses must declare a template attribute; you get a TypeError at class definition time if you forget.
Step 2 — Write the layout template
{# myapp/templates/myapp/layout.html #}
{% load layouts %}
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>{{ page_title|default:"My Site" }}</title>
</head>
<body>
<header><h1>My Site</h1></header>
<div class="layout">
<aside>
{# Fallback content shown when the sidebar panel is empty or missing #}
{% panel "sidebar" %}<p>No sidebar available.</p>{% endpanel %}
</aside>
<main>
{# "content" is the main view's output — always available #}
{% panel "content" %}{% endpanel %}
</main>
</div>
</body>
</html>
{% panel "name" %}...{% endpanel %} renders the named panel's HTML if it produced output, otherwise renders the fallback content between the tags.
Step 3 — Write your views
# myapp/views.py
from django.http import HttpResponse
from dj_layouts import layout, panel_only
@layout("myapp.DefaultLayout")
def homepage(request):
"""Main content view — renders inside DefaultLayout."""
return HttpResponse("<h1>Welcome!</h1><p>Hello, world.</p>")
@panel_only
def sidebar(request):
"""Panel view — only callable as a panel (returns 403 otherwise)."""
return HttpResponse("<nav><a href='/'>Home</a></nav>")
@layout("myapp.DefaultLayout") — the dotted string is resolved lazily from the registry, which avoids circular imports between views.py and layouts.py.
@panel_only — marks a view as panel-only. Calling it directly (e.g. via the browser) returns 403.
Step 4 — Wire up URLs
# myapp/urls.py
from django.urls import path
from . import views
app_name = "myapp"
urlpatterns = [
path("", views.homepage, name="home"),
path("_panels/sidebar/", views.sidebar, name="sidebar"),
]
!!! tip "Panel URL conventions"
Prefixing panel URLs with _panels/ is just a convention. There's nothing magic about the prefix. Panel views still appear in urlpatterns so Django can resolve them — they just return 403 when hit directly.
Step 5 — Try it
Start your dev server and visit /. You'll see:
- The layout template rendered as a full page
- The sidebar panel's HTML in the
<aside>slot - Your homepage content in the
<main>slot
If you visit /_panels/sidebar/ directly you'll get a 403 Forbidden — because it's decorated with @panel_only.
Next steps
- Use
@async_layoutfor concurrent panel rendering on ASGI — see Async. - Add layout context (page title, current user data, etc.) — see Layout Context.
- Add a render queue for per-panel scripts/styles — see Render Queues.
- Understand the full Panel API — see Panels.