Templates and Static Files
Introduction
Django’s Django Template Language (DTL) renders HTML server-side. Views pass context dicts; templates use {{ variables }}, {% tags %}, inheritance, and static asset tags. This chapter replaces plain HttpResponse strings from Views with render(), builds a base.html layout, and wires CSS through STATIC_URL.
Prerequisites
- Views: FBV and CBV
- Basic HTML — HTML track
TEMPLATESwithAPP_DIRS: True(default in settings)
Template Directory Layout
App-namespaced templates (recommended):
Code explanation:
- Path
blog/post_list.htmlavoids clashes with other apps’post_list.html - Static files under
blog/static/blog/mirror the namespace pattern
Optional project-level folders (from chapter 04):
templates/base_site.html
static/css/global.cssEnable in settings.py:
TEMPLATES = [
{
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
...
},
]
STATICFILES_DIRS = [BASE_DIR / "static"]render() in Views
from django.shortcuts import render
def home(request):
context = {
"title": "Hello Django",
"posts": [], # placeholder until models exist
}
return render(request, "blog/home.html", context)render() loads the template, merges context, returns HttpResponse.
Class-based views set template_name = "blog/post_list.html" and build context automatically.
DTL Syntax
| Syntax | Purpose |
|---|---|
{{ variable }} | Output escaped HTML |
{% tag %} | Logic (if, for, url, block) |
{# comment #} | Template comment |
Example blog/templates/blog/home.html:
Auto-Escaping and |safe
Django escapes {{ user_input }} by default—mitigates XSS.
{{ bio }} {# escaped #}
{{ bio|safe }} {# raw HTML — only for trusted content #}Warning
Never |safe on User Content
Comments or posts from users must stay escaped unless you sanitize server-side.
Template Inheritance
blog/templates/blog/base.html:
Child template:
{% extends "blog/base.html" %}
{% block title %}Posts{% endblock %}
{% block content %}
<h1>Posts</h1>
{% endblock %}include Partial
blog/templates/blog/partials/nav.html:
<nav>
<a href="{% url 'home' %}">Home</a>
<a href="{% url 'post-list' %}">Posts</a>
</nav>Requires named URLs from chapter 06.
Static Files in Development
settings.py:
STATIC_URL = "static/"In templates:
{% load static %}
<link rel="stylesheet" href="{% static 'blog/css/style.css' %}">blog/static/blog/css/style.css:
body {
font-family: system-ui, sans-serif;
max-width: 720px;
margin: 2rem auto;
padding: 0 1rem;
}runserver serves static files automatically when django.contrib.staticfiles is in INSTALLED_APPS. Production uses collectstatic — chapter 16.
Common Tags
| Tag | Example |
|---|---|
{% if %} | {% if user.is_authenticated %}...{% endif %} |
{% for %} | {% for x in items %}...{% endfor %} |
{% url %} | {% url 'post-detail' pk=1 %} |
{% csrf_token %} | Inside <form method="post"> — Forms chapter |
{% block %} | Inheritance slots |
{% extends %} | Parent template |
{% include %} | Reusable fragment |
Common Filters
{{ created|date:"Y-m-d H:i" }}
{{ body|truncatewords:30 }}
{{ nickname|default:"Anonymous" }}
{{ title|lower }}
{{ price|floatformat:2 }}Chain filters: {{ name|default:""|truncatewords:5 }}.
Context Processors
Default processors add request, user, messages to every template without passing them in each view:
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},Use {{ user.username }} in base.html when auth is enabled.
Wire post_list View
def post_list(request):
posts = [
{"id": 1, "title": "First post"},
{"id": 2, "title": "Second post"},
]
return render(request, "blog/post_list.html", {"posts": posts})blog/templates/blog/post_list.html:
{% extends "blog/base.html" %}
{% block title %}Posts{% endblock %}
{% block content %}
<h1>Posts</h1>
<ul>
{% for post in posts %}
<li><a href="{% url 'post-detail' pk=post.id %}">{{ post.title }}</a></li>
{% endfor %}
</ul>
{% endblock %}Replace the dict list with ORM QuerySet in chapter 09.
Custom Template Tags (Awareness)
For logic too heavy for templates, use custom template tags in blog/templatetags/blog_extras.py. Most tutorials defer this until needed—filters and {% if %} cover common cases.
Comparison with Flask Jinja2
| Feature | Django DTL | Flask Jinja2 |
|---|---|---|
| Syntax | Similar {{ }}, {% %} | Same family |
| Inheritance | {% extends %} | Same |
| Static helper | {% static %} | url_for('static', ...) |
See Flask Jinja2 for parallel examples.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Wrong template path | TemplateDoesNotExist | Use blog/post_list.html with app namespace |
Forgot {% load static %} | Empty or broken CSS URL | Load tag once per template (or in base) |
Missing {% block content %} in child | Blank page | Match parent block names |
| Hard-coded URLs | Breaks on URL change | {% url 'name' %} |
Post-Chapter Checklist
- Templates live under
blog/templates/blog/ -
base.html+{% block content %}inheritance works - CSS loads via
{% static 'blog/css/style.css' %} - Views use
render()with context dicts - You use
{% url %},{% for %},{% if %}, filters
FAQ
Can I use Jinja2 instead of DTL?
Yes—swap BACKEND in TEMPLATES. This track uses built-in DTL.
Template not found but file exists?
Check app in INSTALLED_APPS, folder name templates/blog/, and spelling of template string.
STATIC_URL vs full path?
{% static %} prepends STATIC_URL and handles cache-busting in production with ManifestStaticFilesStorage.
Pass variables to include?
{% include "partial.html" with item=post %}
Debug template context?
django.template.context_processors.debug adds debug when DEBUG=True.