Views & Templates: Django's MVT Model

Website Rebuild with Django

Chapter 4 · Views & Templates: Django's MVT Model

Chapter 2 built the Model; Chapter 3 wired the routing that finds one. This chapter is the other two letters of MVT — the View and the Template — and Django's own answer to what Website Rebuild with Next.js 4 solved with React components. The mechanism is genuinely different: no JSX, no component props, and — deliberately — no arbitrary code running inside a template file at all.

Template Inheritance: extends and block

<!-- content/templates/content/base.html --> <!DOCTYPE html> <html lang="en"> <head> <title>{% block title %}Osztromok.com{% endblock %}</title> </head> <body class="dark-theme"> <nav>{% block breadcrumb %}{% endblock %}</nav> <main>{% block content %}{% endblock %}</main> </body> </html>
<!-- content/templates/content/page_detail.html --> {% extends "content/base.html" %} {% block title %}{{ page.title }}{% endblock %} {% block content %} <h1>{{ page.title }}</h1> <div>{{ page.body|safe }}</div> {% endblock %}

base.html defines the page's overall shell — the same job Next.js Rebuild 4's own shared Layout component did — with named {% block %} slots left open. page_detail.html extends it and only fills in the specific blocks it actually needs; everything else in the shell is inherited unchanged.

The Django Template Language: Deliberately Not a Programming Language

{{ variable }} outputs a value, auto-escaped by default — the same automatic XSS protection JSX gives Next.js, just via a different mechanism. {% if %}/{% for %} and a limited set of filters (|truncatewords, |date, and the rest) cover most real templating needs. What DTL deliberately doesn't have is arbitrary function calls, real expressions, or general-purpose logic — no way to write a genuine loop-inside-a-loop tree walk, or call an arbitrary Python function with arguments, directly inside a template file.

Building the Breadcrumb: Logic in the View, Not the Template

def page_detail(request, full_path): full_path = full_path.rstrip('/') page = get_object_or_404(Page, full_path=full_path) breadcrumb = [] node = page while node: breadcrumb.insert(0, node) node = node.parent return render(request, 'content/page_detail.html', { 'page': page, 'breadcrumb': breadcrumb, })

Walking node.parent upward — Chapter 2's own adjacency list, not the materialized full_path Chapter 3 used for the lookup — happens entirely in Python, producing an already-flat list before the template ever sees it. The template's own job is trivial by comparison:

{% block breadcrumb %} {% for crumb in breadcrumb %} <a href="/{{ crumb.full_path }}/">{{ crumb.title }}</a> / {% endfor %} {% endblock %}
The central fact this chapter is built on
This split — a real, arbitrary Python loop in the view; a simple, flat {% for %} in the template — is Django's own "thin templates" philosophy made concrete. Every genuinely tricky part of the breadcrumb (walking an unknown number of levels upward, in the correct order) lives in ordinary, testable Python, exactly where Chapter 2's own model design intended the adjacency list to be used. The template only ever iterates a list that already exists — it was never capable of doing that walk itself, and that's a deliberate design choice, not a missing feature: a template language that could run arbitrary logic is also one where a template-injection-style bug becomes possible in the first place.
|safe is only appropriate because of where page.body actually comes from
{{ page.body }} alone would auto-escape any HTML inside page.body, turning real markup into visible &lt;p&gt; text on the page — not what's wanted, since body genuinely stores HTML. |safe disables that escaping, and it's the right call here specifically because page.body is written exclusively by the authenticated site admin (Chapter 9), never submitted directly by an anonymous visitor. Applying |safe to anything a visitor could actually type into a form would reopen exactly the XSS risk Django's own auto-escaping exists to close.

Context Processors: Site-Wide Data Without Repeating Yourself

# content/context_processors.py def site_settings(request): return {'current_year': 2026} # settings.py — added to TEMPLATES['OPTIONS']['context_processors'] # 'content.context_processors.site_settings',

Once registered, {{ current_year }} is available in every template, site-wide, with no view ever needing to pass it explicitly — the same "shared, always-available data" goal a Next.js layout component solves by simply always rendering, just reached through Django's own request-scoped context mechanism instead.

Django's Templates vs. Next.js's React Components

Next.jsDjango
Composition unitA React component — real JavaScript, JSX, props, childrenA template file — {% %}/{{ }} tags only
Shared layoutA shared Layout component wrapping page contentTemplate inheritance — {% extends %} + named {% block %} slots
Logic inside the view layerFull JavaScript, directly inside the componentDeliberately restricted — real logic belongs in the view (Python), not the template

Hands-On Exercises

Exercise 1

Explain why the breadcrumb is built by walking page.parent in the view (Python) rather than attempting that same walk directly inside the template with DTL tags. What does this illustrate about Django's own "thin templates" philosophy?

📄 View solution
Exercise 2

Explain why {{ page.body|safe }} is used instead of plain {{ page.body }}, and why this specific use of |safe is considered acceptable rather than a security risk.

📄 View solution
Exercise 3

Explain what a context processor does, using the current_year example, and why it's a better fit here than passing current_year explicitly from every individual view function.

📄 View solution

Chapter 4 Quick Reference

  • {% extends %} / {% block %} — template inheritance; Django's own answer to a shared Next.js layout component
  • DTL is deliberately limited{{ }}/{% %} tags and filters only, no arbitrary code execution
  • Breadcrumb built in the view — walking page.parent in Python, handed to the template as an already-flat list
  • "Thin templates" — real logic belongs in views/models, not templates; a deliberate separation-of-concerns choice
  • |safe — disables auto-escaping; only appropriate for trusted, admin-authored content, never for visitor-submitted input
  • Context processors — make data available to every template automatically, without passing it from every view
  • Next chapter: Styling — Dark Theme