How a request becomes a page¶
django CMS builds on Django’s request-to-response cycle — URL routing, middleware, template rendering, and the cache framework are all standard Django. This page explains how the CMS extends that cycle: which Django parts it hooks into, what it adds on top, and how the two layers interact when a request for a CMS-managed page arrives. Read it when you need to debug a page that is not showing, understand why a cache is not invalidating, or build a mental model of how all the pieces fit together.
If you have not yet read How django CMS is composed and Content objects: the grouper / content pattern, do that first. The concepts introduced there — pages, content objects, placeholders, plugins, apphooks, and the grouper / content split — are the vocabulary this page assumes.
The big picture¶
Browser: GET /de/events/2026-summit/
│
▼
1. Middleware preamble
ApphookReload → CurrentPage → Language → Toolbar
│
▼
2. URL resolution
slug → PageUrl table → Page object (or 404 / welcome)
│
▼
3. Language determination
URL prefix → session → cookie → Accept-Language → LANGUAGE_CODE
│
▼
4. Content resolution
Page + language → which PageContent row? (published? fallback?)
│
▼
5. Page cache gate
Hit? → return cached response immediately
Miss? → proceed to render, cache result for next time
│
▼
6. Template selection
PageContent.template → "base.html"
│
▼
7. Placeholder rendering (per {% placeholder %} tag)
Cache check → load plugin tree → render plugins
│
▼
8. Menu building (if template uses {% show_menu %})
│
▼
9. Response
1. The middleware preamble¶
Before the CMS view runs, Django’s middleware stack fires in order.
ApphookReloadMiddlewareEnsures the URL configuration is up-to-date when apphooks have been added or removed. A stale URLconf would cause
NoReverseMatcherrors in menu templates.CurrentPageMiddlewareAttaches
request.current_pageas a lazy object. The page is resolved once on first access and stored on the request asrequest._current_page_cache— an in-process attribute that prevents resolving the same page twice during a single request.
LanguageCookieMiddleware (optional — add it to
MIDDLEWARE to enable)
Persists the user’s language preference in a cookie so that subsequent visits default to the same language.
ToolbarMiddlewareAttaches the toolbar if the user is staff and the request is in edit mode (
?editor?toolbar_on). The toolbar’s presence disables CMS-level caching for the request.
2. URL resolution¶
The request arrives at cms.views.details() via the CMS URLconf.
The view calls get_page_from_request(), which
matches the URL slug against the PageUrl table.
The PageUrl model stores one row per (page, language) pair,
each with its own path field. This means the German URL
/de/ueber-uns/ and the English URL /en/about-us/ are stored
independently and resolve to the same Page through different
PageContent rows.
If no page is found:
The CMS checks whether the request falls inside an apphook’s URL space (via
applications_page_check). If it does, the apphook’s parent page is treated asrequest.current_page.If the URL is
/and no pages exist at all, the welcome screen is rendered.Otherwise, a 404 is raised.
A PageUrl lookup happens on most requests. To avoid hitting the
database every time, the result is cached in Django’s cache backend —
keyed by (page_lookup, language, site_id), with a duration set by
CMS_CACHE_DURATIONS ['content'] (default 60 seconds).
The cache is invalidated alongside the global page cache when a page’s
slug changes or the page is moved in the tree.
3. Language determination¶
By the time the CMS view runs, Django’s LocaleMiddleware has
already resolved the active language. The resolution chain, in order:
language prefix in the URL (
/de/when usingi18n_patterns())language stored in the user’s session
language stored in a cookie (from
LanguageCookieMiddleware)browser’s
Accept-Languageheaderthe project’s
LANGUAGE_CODE
The resolved language is available as request.LANGUAGE_CODE.
CMS_LANGUAGES overlays additional policy per language:
fallbacks, redirect_on_fallback, hide_untranslated, and
public. These are consulted in the next step, not here. Language
preference (this step) and content availability (the next step)
are separate concerns.
4. Content resolution¶
With the Page and the language known, the CMS needs to determine
which PageContent row to serve.
A Page instance carries a page_content_cache dictionary —
mapping language → PageContent — that is populated once per
request by _get_page_content_cache(). It loads all available
PageContent rows for the page in a single query, so subsequent
lookups during the same request (including those from template tags
and the toolbar) hit this dictionary rather than the database.
The lookup logic:
Direct hit. A
PageContentrow exists in the requested language. Serve it.Fallback hit. No row in the requested language, but the language’s
fallbackslist (inCMS_LANGUAGES) names a language that does have one. Ifredirect_on_fallbackisTrue(the default), the browser is redirected to the fallback language’s URL. IfFalse, the fallback’s content is served under the original URL.No fallback. 404.
When djangocms-versioning is installed, PageContent.objects
(the default manager) filters to the published row per language.
PageContent.admin_manager returns every row and is used in admin
code paths only. See Publishing for the full version-state
model.
5. The page cache gate¶
Before any rendering begins, the CMS checks whether a cached copy of the entire page already exists. This is the most impactful cache in the system: a hit avoids template loading, placeholder rendering, plugin rendering, and menu building entirely.
The check runs only when CMS_PAGE_CACHE is True
(default), the user is anonymous, the toolbar is not in edit mode, and
no placeholder on the page uses the legacy cache = False flag (see
`Step 7`_).
If the conditions are met, get_page_cache(request) queries
Django’s cache backend for a key composed from the site ID, language,
and a hash of the request path. A hit returns (content, headers,
expires_datetime) — the CMS reconstructs an HttpResponse,
recalculates a max-age header from the stored expiry, and returns
immediately. No further processing.
The page cache uses a versioning strategy: a global integer is stored
under CMS_PAGE_CACHE_VERSION_KEY. Each cache write includes the
current version. invalidate_cms_page_cache() increments the
version — all previous entries become unreachable and expire
naturally. The version key is re-written with a fresh timeout on every
cache write, ensuring it always outlives the entries it protects.
The cache duration is the shortest of CMS_CACHE_DURATIONS['content']
and the TTL returned by each rendered placeholder’s
get_cache_expiration().
If any placeholder returns EXPIRE_NOW (0), the page is not cached
at all.
When the page cache is skipped — for authenticated users, toolbar edit mode, or plugins that opt out — the remaining cache layers (placeholder, menu) are also skipped for that request.
On a cache miss, the CMS proceeds to render the page (Steps 6–8).
After rendering, set_page_cache() stores the result: it gathers
all placeholders that were rendered, computes the effective TTL as the
shortest across all placeholders, collects the union of Vary headers,
and writes (content, headers, expires_datetime) to the cache. The
absolute expiry timestamp is stored so that future reads can
recalculate max-age without recomputing each placeholder’s TTL.
6. Template selection¶
The PageContent.template field names the Django template to use
(e.g. "base.html"). Template resolution follows Django’s standard
loader chain: the CMS template directory, then the project’s template
directories, then any additional loaders configured in
TEMPLATES.
The template defines which placeholders exist through {% placeholder
"name" %} tags. These are the rendering anchor points for the next
step.
7. Placeholder rendering¶
For each {% placeholder %} tag in the template, the CMS resolves
the corresponding Placeholder
object — scoped to the current PageContent — and renders it.
Before loading plugins, the CMS checks the placeholder cache. This
cache stores the fully rendered HTML for a single placeholder, keyed
by placeholder ID, language, site, timezone, and any Vary headers the
placeholder’s plugins declare. It uses its own per-placeholder version
integer — separate from the global page cache version — so
invalidating one placeholder does not affect others. The check runs
when CMS_PLACEHOLDER_CACHE is True (default), the
toolbar is not in edit mode, and caching is enabled on the placeholder
and in the template tag (both defaults).
A cache hit returns the pre-rendered HTML directly. On a miss, the CMS loads the plugin tree and renders it.
Plugin tree loading. Plugins form a tree through a self-referential
foreign key: each CMSPlugin row has a parent field pointing to
its container plugin (or NULL for plugins placed directly into a
placeholder). The tree is assembled by querying children at each
level, ordered by a position field.
The root plugins (those inserted directly into the placeholder) are
determined by the template and slot; valid root plugin classes for a
given (template, slot) are cached in-process in
PluginPool.root_plugin_cache, avoiding recomputation on every
render.
Plugin rendering. The tree is walked depth-first. For each plugin,
CMSPluginBase.render()
returns an HTML fragment. Context flows from parent to child through
the CMS_PLUGIN_CONTEXT_PROCESSORS pipeline.
Plugin-level cache control. Each plugin class can influence the placeholder cache through two methods:
get_cache_expiration(request, instance, placeholder)Returns a TTL in seconds for this plugin instance. The placeholder uses the shortest TTL across all its plugins. Override this to vary cache duration by content type — a shorter TTL for a “latest news” plugin than for static footer content.
get_vary_cache_on(request, instance, placeholder)Returns a list of HTTP header names to vary on — for example
['User-Agent']for a plugin that renders differently on mobile.
The legacy cache = False attribute on a CMSPluginBase subclass
causes the plugin to return EXPIRE_NOW, which disables the page
cache for any page containing that plugin. Prefer overriding
get_cache_expiration() with a short TTL instead.
After rendering, the result is stored in the placeholder cache with a
duration of min(CMS_CACHE_DURATIONS['content'], placeholder_ttl).
The cache is invalidated — by incrementing the per-placeholder
version — whenever any plugin inside it is saved, moved, or deleted.
During rendering, plugins may also perform permission checks — for
example, whether the current user can see a restricted plugin. These
checks use the permission cache, which stores allowed page IDs per
user and per action (change_page, publish_page, etc.) in
Django’s cache backend. It is invalidated when the user’s groups
change or when any PagePermission or GlobalPagePermission is
saved or deleted. See Permissions for the full model.
9. Response¶
The rendered page — whether from cache or freshly built — is returned
as a Django HttpResponse. If the page cache layer wrote the
response, the headers include Cache-Control: max-age=...,
Expires, and Vary, set from the values computed during the
cache write in Step 5.
The toolbar, if active, wraps each plugin in edit markers and injects structure-board data and admin URLs. This happens after rendering and does not affect the cache — edit-mode requests bypass all CMS-level caching and are never cached themselves.
Cache layers at a glance¶
Layer |
Storage |
Keyed by |
Duration |
Invalidated by |
|---|---|---|---|---|
|
Process memory |
— |
Request lifetime |
— |
|
Process memory |
language |
Request lifetime |
— |
Page URL cache |
Django cache |
(page, lang, site) |
|
Version bump on slug/move |
Page cache |
Django cache |
(site, lang, path hash) |
|
|
Placeholder cache |
Django cache |
(ph, lang, site, tz, vary) |
|
|
Permission cache |
Django cache |
(user, action) |
|
Version bump on group/perm change |
Menu cache |
Django cache |
(site, lang) |
|
|
Plugin pool cache |
Process memory |
(template, slot) |
Process lifetime |
— |
Where to go next¶
How django CMS is composed — the three building blocks (content objects, plugins, apphooks) that the lifecycle orchestrates.
Content objects: the grouper / content pattern — the grouper / content split that underpins content resolution (Step 4).
Publishing — how version states affect which content rows are visible and what “published” means for caching.
Serving content in multiple languages — the language-determination and fallback rules that drive Steps 3 and 4.
How the menu system works — the generators and modifiers behind Step 8.
Permissions — the permission model that the permission cache protects.
Configuring django CMS — the authoritative reference for
CMS_PAGE_CACHE,CMS_PLACEHOLDER_CACHE,CMS_CACHE_DURATIONS, and related settings.Plugins — the
CMSPluginBaseAPI, includingget_cache_expiration()andget_vary_cache_on().