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

Template Directory Layout

App-namespaced templates (recommended):

Code explanation:

  • Path blog/post_list.html avoids clashes with other apps’ post_list.html
  • Static files under blog/static/blog/ mirror the namespace pattern

Optional project-level folders (from chapter 04):

text
templates/base_site.html
static/css/global.css

Enable in settings.py:

python
TEMPLATES = [
    {
        "DIRS": [BASE_DIR / "templates"],
        "APP_DIRS": True,
        ...
    },
]
STATICFILES_DIRS = [BASE_DIR / "static"]

render() in Views

python
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

SyntaxPurpose
{{ 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.

django
{{ 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:

django
{% extends "blog/base.html" %}
 
{% block title %}Posts{% endblock %}
 
{% block content %}
  <h1>Posts</h1>
{% endblock %}

include Partial

blog/templates/blog/partials/nav.html:

django
<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:

python
STATIC_URL = "static/"

In templates:

django
{% load static %}
<link rel="stylesheet" href="{% static 'blog/css/style.css' %}">

blog/static/blog/css/style.css:

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 collectstaticchapter 16.

Common Tags

TagExample
{% 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

django
{{ 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:

python
"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

python
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:

django
{% 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

FeatureDjango DTLFlask Jinja2
SyntaxSimilar {{ }}, {% %}Same family
Inheritance{% extends %}Same
Static helper{% static %}url_for('static', ...)

See Flask Jinja2 for parallel examples.

Common Mistakes

MistakeSymptomFix
Wrong template pathTemplateDoesNotExistUse blog/post_list.html with app namespace
Forgot {% load static %}Empty or broken CSS URLLoad tag once per template (or in base)
Missing {% block content %} in childBlank pageMatch parent block names
Hard-coded URLsBreaks 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.