Skip to content

Django Templates and Static Files

Django templates let you show dynamic data (data that changes based on users or database content) inside HTML pages. This chapter covers templates, static files, app setup, and how to add Tailwind CSS for styling. By the end, you will know how to build reusable page layouts, serve CSS and image files, and style your project with Tailwind.

Django comes with its own way to write templates called Django Template Language (DTL). It looks very similar to Jinja2 (another popular template tool), so if you’ve seen one, the other will feel familiar. DTL is the default and the one you should use for most Django projects.

Stick with DTL unless you have a strong reason to switch. If you prefer Jinja2, Django does support it as an option, but for beginners, DTL is the way to go.

By default, Django looks for template files in two places:

  • A templates/ folder inside each app (when APP_DIRS=True)
  • Any extra folders you list in settings.py under TEMPLATES[0]["DIRS"]

To add a project-level templates/ folder (one that’s shared across all apps), update settings.py like this:

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"], # project-level templates
"APP_DIRS": True, # include app-level templates
"OPTIONS": {
# context processors, etc.
},
},
]

In a Django template, you use:

  • {{ variable }} - to display a value
  • {% tag %} - to run logic like loops or conditions
Hello {{ name }}!

If the view sends name = "Kumar Shail", the page will show: Hello Kumar Shail!

if - show something only when a condition is true:

Section titled “if - show something only when a condition is true:”
{% if name %}
Hello, {{ name }}!
{% endif %}

for - loop over a list and show each item:

Section titled “for - loop over a list and show each item:”
{% for item in items %}
{{ item }}
{% endfor %}

block / endblock - mark a section in a base template that child templates can replace:

Section titled “block / endblock - mark a section in a base template that child templates can replace:”
<title>{% block title %}My Website{% endblock title %}</title>

extends - use a base template as the starting point for a page:

Section titled “extends - use a base template as the starting point for a page:”
{% extends "base.html" %}

include - pull in another template file, great for things like headers or footers:

Section titled “include - pull in another template file, great for things like headers or footers:”
{% include "partials/header.html" %}
Section titled “load static / static - link to static files like CSS, JS, or images:”
{% load static %}
<link rel="stylesheet" href="{% static 'css/style.css' %}">
Section titled “url - generate a link to a named URL without hardcoding the path:”
<a href="{% url 'all_items' %}">All Items</a>

Template inheritance lets you write the common layout (header, footer, navigation) once in a base template, and then have other pages just fill in their own content. This way you don’t repeat the same HTML on every page.

templates/base.html (example):

{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<title>{% block title %}My Project{% endblock title %}</title>
</head>
<body>
<nav><!-- common navbar --></nav>
<main>{% block content %}{% endblock content %}</main>
<footer><!-- common footer --></footer>
</body>
</html>

The {% block %} sections are the parts that child templates can replace. Everything else (like the <nav> and <footer>) stays the same on every page.

Child template (core/templates/core/all_items.html):

{% extends "base.html" %}
{% block title %}All Items{% endblock title %}
{% block content %}
<h1>All Items</h1>
<p>Welcome {{ name }}</p>
{% for item in names %}
<p>{{ item }}</p>
{% endfor %}
{% endblock content %}

This child template starts with {% extends "base.html" %} which means “use base.html as the shell, and fill in my blocks”. It only defines what goes inside title and content - the rest comes from the base.

To show a template to a user, you use render() in your view. You pass it the request, the template file name, and any data (called context) you want to display in the template.

core/views.py:

from django.shortcuts import render
def all_items(request):
context = {
"name": "Kumar Shail",
"names": ["Tea", "Coffee", "Juice"],
}
return render(request, "core/all_items.html", context)

core/urls.py:

from django.urls import path
from . import views
urlpatterns = [
path("", views.all_items, name="all_items"),
]

Include the app’s URLs in the main project urls.py:

from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("items/", include("core.urls")),
]

Now visit http://localhost:8000/items/ and you’ll see the page rendered with your data.

Serving a Template Directly from a URL (No View Needed)

Section titled “Serving a Template Directly from a URL (No View Needed)”

For simple pages that don’t need any data from the database, you can skip writing a view entirely and use TemplateView directly in your URLs:

app/urls.py:

from django.urls import path
from django.views.generic import TemplateView
urlpatterns = [
path("", TemplateView.as_view(template_name="core/index.html"), name="index"),
]

This is handy for things like a plain “About” or “Coming Soon” page.

Static files are files that don’t change based on user data - things like CSS stylesheets, JavaScript files, images, and fonts. Django’s built-in development server handles these automatically when DEBUG=True. For production, you need a proper setup.

The easiest way to serve static files in production is WhiteNoise. Install it:

Terminal window
uv add whitenoise

Add WhiteNoise to MIDDLEWARE in settings.py (it must go right after SecurityMiddleware):

MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# other middleware...
]
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
STATIC_ROOT = BASE_DIR / "staticfiles"
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"

Create a sample stylesheet at core/static/core/css/style.css or static/css/style.css:

body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
padding: 1rem;
}

Before deploying, run this command to copy all static files into one folder (STATIC_ROOT) so your web server can find them:

Terminal window
python manage.py collectstatic

WhiteNoise then serves those files efficiently without needing a separate server.

Tailwind CSS is a popular styling tool that lets you style your HTML using small, ready-made class names instead of writing CSS from scratch. This project uses django-tailwind, which connects Tailwind into your Django project smoothly.

The commands below use uv to run things. If you don’t have uv, you can replace uv add with pip install and uv run python manage.py with python manage.py.

Install the required packages:

Terminal window
uv add django-tailwind django-browser-reload

django-browser-reload automatically refreshes your browser when you change a template - no need to manually reload the page while developing.

Add the apps to INSTALLED_APPS in settings.py:

INSTALLED_APPS = [
# other apps...
"tailwind",
"theme", # the Tailwind theme app created by tailwind init
]
# development-only tools
if ENVIRONMENT != PRODUCTION:
INSTALLED_APPS += ["django_browser_reload"]
TAILWIND_APP_NAME = "theme"
INTERNAL_IPS = ["127.0.0.1", "localhost"]
NPM_BIN_PATH = "npm" if os.name != "nt" else r"C:\Program Files\nodejs\npm.cmd"
  • TAILWIND_APP_NAME = "theme" - tells Django which app holds your Tailwind setup.
  • INTERNAL_IPS - tells Django which IP addresses count as “local” (needed for dev tools to work).
  • NPM_BIN_PATH - points Django to the npm tool. The nt check handles Windows vs Mac/Linux.

Add django_browser_reload middleware in development (so auto-reload works):

if ENVIRONMENT != PRODUCTION:
MIDDLEWARE += ["django_browser_reload.middleware.BrowserReloadMiddleware"]

Add the reload URL in development:

from django.conf import settings
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("items/", include("core.urls")),
]
if settings.ENVIRONMENT != settings.PRODUCTION:
urlpatterns += [path("__reload__/,", include("django_browser_reload.urls"))]

Now run these two commands to set up Tailwind. You only need to do this once:

Terminal window
uv run python manage.py tailwind init
uv run python manage.py tailwind install
  • tailwind init - creates the theme app inside your project.
  • tailwind install - downloads the Tailwind CSS tools via npm.

Use Tailwind in your base template like this:

{% load static %}
{% load static tailwind_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
{% tailwind_css %}
<title>{% block title %}My Project{% endblock title %}</title>
</head>
<body class="bg-slate-100 text-slate-900">
{% block content %}{% endblock content %}
</body>
</html>

{% tailwind_css %} automatically injects the Tailwind stylesheet. Then you can use Tailwind classes like bg-slate-100 directly on any HTML element.

When developing, you need to run two terminal windows at the same time:

Terminal window
uv run python manage.py runserver
Terminal window
uv run python manage.py tailwind start

The first starts Django. The second watches your templates for changes and rebuilds the Tailwind CSS file automatically.

When you’re ready to deploy, build the final optimized Tailwind CSS file:

Terminal window
uv run python manage.py tailwind build