Django Module 2 – URLs, views, templates and static files
Static files in development and production
Organise Django static files by app, link them with the static tag, collect hashed copies with collectstatic, and serve them with WhiteNoise, nginx or a CDN.
What you will learn
- Organise app and project static files and reference them with the static tag
- Run collectstatic and explain hashed file names, the manifest and the cache headers they allow
- Choose between WhiteNoise, a web server and a CDN for serving static files in production
- Separate user uploads, the media files, from static files
Before you start
What you’ll learn · Before you start
What you will learn
- Organise app and project static files and reference them with the static tag
- Run collectstatic and explain hashed file names, the manifest and the cache headers they allow
- Choose between WhiteNoise, a web server and a CDN for serving static files in production
- Separate user uploads, the media files, from static files
Before you start
On this page
Every page of Nivas loads the same stylesheet, and its browser copy should last for years, yet change on the day a release changes it. Django’s answer is a file name that changes with the content: collectstatic copied 133 files for Nivas and gave each a name with a hash of what it holds, and a server can then tell browsers to keep them for ten years.
This lesson organises the static files of Nivas, links them with the static tag, serves them while you develop,
collects them into hashed copies for production, and serves those with WhiteNoise, then compares that with a web
server and a CDN. Last, it keeps residents’ uploads, the media files, apart from all of them.
Each app keeps its static files in a folder of its own name
static files: Files a site serves as they are, the same for every visitor, such as stylesheets, scripts, fonts and the images of the layout. In Django they live in static/ folders and collectstatic gathers them. are the files a site sends as they are: stylesheets, scripts, fonts and
the images of its layout. Nivas has them in two kinds of places. Files of the whole project, css/base.css and its logo,
sit in the project’s static/ folder, which STATICFILES_DIRS names. Files of one app sit in that app’s static/
folder, under an inner folder with the app’s name: the notices app keeps notices/static/notices/notices.css. A
template asks for a file by its name, notices/notices.css, and two finders search for it in a fixed order:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- A name, not a path. A template asks for notices/notices.css with the static tag. Like a template name, it is a name to search for, and the finders try folders in a fixed order.
- The project's folders first. The first finder reads STATICFILES_DIRS, here the project's static/ folder, which holds css/base.css and img/logo.svg but no notices/notices.css.
- Then each app. The second finder tries the static/ folder of each installed app, in the order of INSTALLED_APPS. The admin's folder has only admin/…, so the search goes on.
- Found in the notices app. notices/static/notices/notices.css exists, so the search stops: the first file found under a name is the one Django uses.
- A name without its folder. notices.css alone matches no file, because the app keeps it under an inner notices/ folder. That inner folder is what keeps two apps' files apart: without it, the first app's file would win.
The program below asks the finders about five names and prints, relative to the project, the file each one found:
"""Where the static files of Nivas are found, and the URL the static tag builds for them while you develop.
Run it next to manage.py. The finders look in STATICFILES_DIRS first, then in each installed app's static/ folder.
"""
import os
from pathlib import Path
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
import django # noqa: E402
django.setup()
from django.conf import settings # noqa: E402
from django.contrib.staticfiles import finders # noqa: E402
from django.template import Context, Template # noqa: E402
def where(path):
path = Path(path)
if path.is_relative_to(settings.BASE_DIR):
return str(path.relative_to(settings.BASE_DIR))
return 'django/' + str(path.relative_to(Path(django.__file__).parent))
for name in ['css/base.css', 'img/logo.svg', 'notices/notices.css', 'admin/css/base.css', 'notices.css']:
found = finders.find(name)
print(f'{name:21} -> {where(found) if found else "not found"}')
print('Folders searched for the last name:')
for folder in finders.searched_locations:
print(' ', where(folder))
template = Template("{% load static %}{% static 'css/base.css' %} {% static 'notices/notices.css' %}")
print('The static tag with DEBUG = True:', template.render(Context())) Output
css/base.css -> static/css/base.css
img/logo.svg -> static/img/logo.svg
notices/notices.css -> notices/static/notices/notices.css
admin/css/base.css -> django/contrib/admin/static/admin/css/base.css
notices.css -> not found
Folders searched for the last name:
static
django/contrib/admin/static
notices/static
The static tag with DEBUG = True: /static/css/base.css /static/notices/notices.css
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 find_static.py
The project’s folder came first, then the apps in the order of INSTALLED_APPS: the admin’s, which holds the admin’s
own admin/css/base.css, and the notices app’s. notices.css alone found nothing, because the app keeps it inside
notices/. That inner folder is the point: names from every app share one space, and Django’s guide warns that with
two files of one name, Django uses the first it finds and cannot tell them apart.
The same question can be asked from the command line: python manage.py findstatic notices/notices.css runs these
finders and lists every file the name finds, the winner first; --first prints only the winner.
An app's files directly in its static folder
notices/static/style.css takes the name style.css from every other app’s style.css, and whichever app comes
first in INSTALLED_APPS wins. Put it at notices/static/notices/style.css and ask for notices/style.css.
The static tag builds the URL, so templates never write one
A template never writes /static/… itself. {% load static %} and then {% static 'css/base.css' %} asks Django for
the URL: with the development settings that is STATIC_URL and the name, /static/css/base.css, as the last line of
the run above printed. The base template of Nivas links the project’s stylesheet this way, and the notices list adds
the app’s own in a block:
templates/base.html · nivas/templates/base.html
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Nivas{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/base.css' %}">
{% block extra_head %}{% endblock %}
</head>
<body>
<header><a href="{% url 'notices:list' %}">Nivas</a></header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html> nivas/notices/templates/notices/notice_list.html
{% extends "base.html" %}
{% load nivas_extras static %}
{% partialdef notice-card %}
<li id="notice-{{ notice.pk }}">
<h2>{{ notice.title }}</h2>
{% resident_badge notice.posted_by notice.flat %}
{% if notice.fee_paise %}<p>Fee: {{ notice.fee_paise|paise }}</p>{% endif %}
</li>
{% endpartialdef %}
{% block title %}Notices · {{ block.super }}{% endblock %}
{% block extra_head %}<link rel="stylesheet" href="{% static 'notices/notices.css' %}">{% endblock %}
{% block content %}
<h1>Notices</h1>
{% fee_summary page_obj %}
<ul id="notices">
{% for notice in page_obj %}
{% partial notice-card %}
{% empty %}
<li>No notices match these filters.</li>
{% endfor %}
</ul>
{% endblock %} The tag matters most once the URL stops being that simple. With the production storage of a later section, the same
tag writes /static/css/base.902a4c163d8a.css, and with a CDN it would write the CDN’s address: every template keeps
working without a change, which is the same rule as building links from route names. A stylesheet cannot use the tag,
so base.css finds its logo with a relative url("../img/logo.svg"), and collectstatic takes care of it.
Key idea
A static file has a name and the storage gives it a URL: keep each app’s files under static/<app>/, and build
every URL with the static tag.
Checkpoint
Check: Static folders and the finders · URLs from the static tag
runserver serves static files only while DEBUG is on
While you develop, nothing needs collecting: runserver finds each file in its app’s folder and sends it. But it does
so only while DEBUG is True, and Django’s guide is blunt about why it stops there: the method “is grossly
inefficient and probably insecure, so it is unsuitable for production”. This program asks runserver’s own command which
handler it would use, without starting a server, and sends it a request for the stylesheet:
"""runserver serves static files only while DEBUG is True. This asks runserver's own command for the handler it
would use, without starting a server, and sends it a request for a stylesheet."""
import io
import os
import sys
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
import django # noqa: E402
django.setup()
from django.contrib.staticfiles.management.commands.runserver import Command # noqa: E402
from django.test import override_settings # noqa: E402
def get(handler, path):
environ = {'REQUEST_METHOD': 'GET', 'PATH_INFO': path, 'SERVER_NAME': 'localhost', 'SERVER_PORT': '8000',
'wsgi.url_scheme': 'http', 'wsgi.input': io.BytesIO(), 'wsgi.errors': sys.stderr, 'HTTP_HOST': 'localhost'}
status = []
body = handler(environ, lambda line, headers: status.append(line))
if hasattr(body, 'close'):
body.close()
return status[0]
for debug in [True, False]:
with override_settings(DEBUG=debug, ALLOWED_HOSTS=['localhost']):
handler = Command().get_handler(use_static_handler=True, insecure_serving=False)
print(f'DEBUG = {debug}: runserver uses {type(handler).__name__}')
print(' GET /static/css/base.css ->', get(handler, '/static/css/base.css')) Output
DEBUG = True: runserver uses StaticFilesHandler
GET /static/css/base.css -> 200 OK
DEBUG = False: runserver uses WSGIHandler
GET /static/css/base.css -> 404 Not Found
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 runserver_static.py
With DEBUG = True, runserver wraps the project in StaticFilesHandler, which answers /static/… before the URLconf
is tried, and the stylesheet came back 200. With DEBUG = False, it uses the plain WSGIHandler, the URLconf has no
static route, and the answer is 404. That is why a page loses its styles when you switch DEBUG off on your own
computer, and why production needs another way to serve the files.
collectstatic copies everything and names each file by its content
A production server should not search the folders of every app on each request. python manage.py collectstatic
gathers every static file of the project and its apps into one folder, STATIC_ROOT: The folder into which collectstatic copies the static files of the project and of every app, for the production server to serve. Nobody edits files in it by hand; the next collectstatic writes it again.,
staticfiles/ in Nivas, for the server to send. The storage in the staticfiles key of the STORAGES setting decides
how it copies them. Nivas’s production settings use WhiteNoise’s version of
manifest storage: ManifestStaticFilesStorage and the storages built on it: collectstatic writes a copy of each file whose name holds a hash of its content, and staticfiles.json, the manifest the static tag reads.:
"""Production settings for static files. The deployment module of this track adds the rest: secrets from the
environment, HTTPS and the mailer."""
from .settings import * # noqa: F403
DEBUG = False
ALLOWED_HOSTS = ['nivas.example']
# WhiteNoise serves the collected files from the Django process itself: right after SecurityMiddleware.
MIDDLEWARE = [MIDDLEWARE[0], 'whitenoise.middleware.WhiteNoiseMiddleware', *MIDDLEWARE[1:]] # noqa: F405
STORAGES = {
'default': {'BACKEND': 'django.core.files.storage.FileSystemStorage'},
# Copies with the content's hash in their names, a manifest, and a gzip copy of each file that shrinks.
'staticfiles': {'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage'},
} This program runs collectstatic with those settings and shows what it wrote:
"""collectstatic with the production settings of Nivas: every static file copied into STATIC_ROOT, with a copy whose
name holds a hash of its content, a manifest and a gzip copy. Paths are printed relative to the project.
Run it next to manage.py.
"""
import io
import json
import os
from pathlib import Path
os.environ['DJANGO_SETTINGS_MODULE'] = 'config.production'
import django # noqa: E402
django.setup()
from django.conf import settings # noqa: E402
from django.core.management import call_command # noqa: E402
from django.template import Context, Template # noqa: E402
out = io.StringIO()
call_command('collectstatic', interactive=False, verbosity=1, stdout=out)
print('$ python manage.py collectstatic --noinput --settings=config.production')
print(out.getvalue().strip().replace(str(settings.STATIC_ROOT), 'staticfiles'))
root = Path(settings.STATIC_ROOT)
ours = sorted(p for p in root.rglob('*') if p.is_file() and p.relative_to(root).parts[0] in {'css', 'img', 'notices'})
print('The files of Nivas in staticfiles/:')
for path in ours:
print(' ', path.relative_to(root))
manifest = json.loads((root / 'staticfiles.json').read_text())
print(f'staticfiles.json maps {len(manifest["paths"])} names, among them:')
for name in ['css/base.css', 'img/logo.svg', 'notices/notices.css']:
print(f' {name} -> {manifest["paths"][name]}')
hashed_css = root / manifest['paths']['css/base.css']
print('The url() inside the hashed base.css now names the hashed logo:')
print(' ', next(line.strip() for line in hashed_css.read_text().splitlines() if 'url(' in line))
template = Template("{% load static %}{% static 'css/base.css' %}")
print('The static tag with these settings:', template.render(Context())) Output
$ python manage.py collectstatic --noinput --settings=config.production
133 static files copied to 'staticfiles', 397 post-processed.
The files of Nivas in staticfiles/:
css/base.902a4c163d8a.css
css/base.902a4c163d8a.css.gz
css/base.css
css/base.css.gz
img/logo.13d8e578b301.svg
img/logo.svg
notices/notices.css
notices/notices.css.gz
notices/notices.da2d22682ee6.css
notices/notices.da2d22682ee6.css.gz
staticfiles.json maps 133 names, among them:
css/base.css -> css/base.902a4c163d8a.css
img/logo.svg -> img/logo.13d8e578b301.svg
notices/notices.css -> notices/notices.da2d22682ee6.css
The url() inside the hashed base.css now names the hashed logo:
background: url("../img/logo.13d8e578b301.svg") no-repeat 1rem center / 1.5rem;
The static tag with these settings: /static/css/base.902a4c163d8a.css
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 collect.py
It copied 133 files, the admin’s and the three of Nivas, and post-processed 397: for each file whose content it
read, it wrote a second copy whose name holds an MD5 hash of the content, base.902a4c163d8a.css, and a gzip copy of
each file that compression made smaller. staticfiles.json, the manifest, maps every plain name to its hashed one, and
the hashed base.css names the hashed logo, because the storage rewrote its url(). The static tag now reads the
manifest and writes /static/css/base.902a4c163d8a.css.
The hash is what makes long caching safe. The same content always gives the same name, and changed content always gives a new name, so a browser that keeps a hashed file forever can never show a stale version.
A missing file stops the page
With manifest storage, the static tag raises ValueError for a name missing from the manifest, so a template that
names a file nobody added fails with a server error. Run collectstatic as a release step, before the new code takes
requests, and render the main pages in a test with the production storage.
Checkpoint
Check: Static files while you develop · collectstatic and hashed names
In production, WhiteNoise, a web server or a CDN serves the files
Something has to send the files of STATIC_ROOT. WhiteNoise: A Python package that serves a Django project's collected static files from the Django process itself, with compressed copies and long cache headers for files whose names hold a hash. does it from the Django
process itself: its middleware, right after SecurityMiddleware in the production settings above, answers requests for
/static/… before the rest of Django runs. This program collects the files, as a release would, and requests the
stylesheet the way a browser does:
"""WhiteNoise serving the collected files from the Django process: the headers a browser gets for a hashed file,
for the same file under its plain name, and for a name that does not exist.
Run it next to manage.py. It runs collectstatic first, quietly, as a release step would.
"""
import logging
import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'config.production'
import django # noqa: E402
django.setup()
from django.core.management import call_command # noqa: E402
from django.templatetags.static import static # noqa: E402
from django.test import Client # noqa: E402
call_command('collectstatic', interactive=False, verbosity=0)
logging.getLogger('django.request').setLevel(logging.CRITICAL)
client = Client(HTTP_HOST='nivas.example')
hashed = static('css/base.css')
for path, encoding in [(hashed, 'gzip'), (hashed, None), ('/static/css/base.css', None), ('/static/css/missing.css', None)]:
extra = {'HTTP_ACCEPT_ENCODING': encoding} if encoding else {}
response = client.get(path, **extra)
print(f'GET {path}' + (f' (Accept-Encoding: {encoding})' if encoding else ''))
print(f' {response.status_code} {response.reason_phrase}')
for name in ['Content-Type', 'Cache-Control', 'Content-Encoding', 'Vary']:
if response.has_header(name):
print(f' {name}: {response[name]}') Output
GET /static/css/base.902a4c163d8a.css (Accept-Encoding: gzip)
200 OK
Content-Type: text/css; charset="utf-8"
Cache-Control: max-age=315360000, public, immutable
Content-Encoding: gzip
Vary: Accept-Encoding
GET /static/css/base.902a4c163d8a.css
200 OK
Content-Type: text/css; charset="utf-8"
Cache-Control: max-age=315360000, public, immutable
Vary: Accept-Encoding
GET /static/css/base.css
200 OK
Content-Type: text/css; charset="utf-8"
Cache-Control: max-age=60, public
Vary: Accept-Encoding
GET /static/css/missing.css
404 Not Found
Content-Type: text/html; charset=utf-8
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 whitenoise_headers.py
The hashed name came back with Cache-Control: max-age=315360000, public, immutable: 315,360,000 seconds is ten years,
and immutable, from RFC 8246, says the file will not change, so the browser need not even check. A browser that
accepts gzip got the compressed copy, with Content-Encoding: gzip and Vary: Accept-Encoding for caches. The plain
name got max-age=60, because its content may change under the same name. Step through a release, two visits and the
next release:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Copies with a hash. collectstatic copies every file into STATIC_ROOT, writes a copy whose name holds a hash of its content, css/base.902a4c163d8a.css, a gzip copy beside it, and staticfiles.json, the manifest.
- The page names the hashed file. A page renders {% static 'css/base.css' %}. The storage looks the name up in the manifest, so the link is /static/css/base.902a4c163d8a.css.
- The first visit. The browser asks for the hashed file. WhiteNoise answers it from the Django process, gzip-compressed, with Cache-Control: max-age=315360000, public, immutable.
- Kept for ten years. 315,360,000 seconds is ten years, and immutable says the file will never change under this name, so the browser stores it and does not even ask again.
- The second visit. On the next page the browser finds the same name in its cache and uses it without a request. The plain name, css/base.css, would have been cached for 60 seconds only.
- The next release. A change to base.css changes its content, so collectstatic gives it a new hash and the pages link the new name. The browser has never seen that name and fetches it at once.
WhiteNoise is one of three common ways. Django’s deployment guide describes the other two: a web server such as nginx
or Apache serving STATIC_ROOT itself, which most larger sites do with a server of its own, and a cloud storage
service with a CDN: A content delivery network: servers in many places that keep copies of a site's files and answer each visitor from one near them., to which collectstatic uploads the files through a storage backend. In the
2026 Django Developers Survey, 50 % of respondents served static files with nginx or Apache, 32 % with cloud storage
and a CDN and 31 % with WhiteNoise. Which fits depends on your site:
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Served by | Work to set up | Visitors far away | Load on Django | Fits a managed platform |
|---|---|---|---|---|
| WhiteNoise in Django | Good one middleware line and one storage setting; compression and cache headers included | Mixed every file comes from your own server, unless a CDN is put in front of it | Mixed the Django process sends the files, cheaply, since browsers keep hashed files for years | Good needs nothing outside Python, so it works where you cannot run a web server |
| nginx or Apache | Mixed a web server to run and configure, with its own cache headers and compression | Mixed files come from your own machine, so a reader far away waits for every first visit | Good Django never sees a static request: the web server answers from STATIC_ROOT | Poor needs a server whose web server you control |
| Cloud storage and a CDN | Poor a storage backend for collectstatic, a bucket, a CDN and its settings | Good files come from an edge close to each reader | Good neither Django nor your server sends a static file | Good works anywhere collectstatic can upload to the storage service |
How the answers decide
- “Do you run your own server, with a web server such as nginx in front of Django?” No, a managed platform runs the app: rules out nginx or Apache. A managed platform runs your app process and nothing in front of it that you configure, so there is no web server of yours to serve STATIC_ROOT.
- “Are many of your visitors far from where the site runs, on another continent?” Yes, they are spread worldwide: adds a caveat to WhiteNoise in Django and nginx or Apache. From your own server, every first visit waits for the round trips to it. A CDN in front of WhiteNoise, or cloud storage behind a CDN, serves from an edge near the reader.
- “Is the site small, with one server and no one to look after more infrastructure?” Yes, keep it to the app: adds a caveat to Cloud storage and a CDN. A bucket and a CDN are two more services to set up, secure and pay for; WhiteNoise keeps static files inside the app you already deploy.
In short: WhiteNoise when the app runs on a platform without a web server of your own, or when one server is all you want to run; the web server you already run when you have one; and a CDN, in front of either, when readers are far away.
Checkpoint
Check: Serving static files in production
Uploads are media files, not static files
A resident’s photo of a broken lift is a file too, but not a static one. media files: Files that users upload, such as photos and documents. Django keeps them in MEDIA_ROOT under MEDIA_URL, apart from static files, because they need their own checks and permissions. come
from users while the site runs, so they need size limits, type checks and permissions, and they must never be served
as if they were the site’s own code. Django keeps them in MEDIA_ROOT, under MEDIA_URL, and refuses settings that mix
the two kinds:
"""Uploads are media, not static files. Django refuses settings that would mix the two folders or the two URLs."""
import django
from django.conf import settings
settings.configure(
INSTALLED_APPS=['django.contrib.staticfiles'],
STATIC_URL='static/',
STATIC_ROOT='/srv/nivas/staticfiles',
MEDIA_URL='media/',
MEDIA_ROOT='/srv/nivas/media',
)
django.setup()
from django.contrib.staticfiles.storage import StaticFilesStorage # noqa: E402
from django.core.exceptions import ImproperlyConfigured # noqa: E402
from django.test import override_settings # noqa: E402
print('STATIC_ROOT and MEDIA_ROOT apart:', StaticFilesStorage().location)
for name, changes in [('MEDIA_ROOT = STATIC_ROOT', {'MEDIA_ROOT': '/srv/nivas/staticfiles'}),
('MEDIA_URL = STATIC_URL', {'MEDIA_URL': 'static/'})]:
with override_settings(**changes):
try:
StaticFilesStorage()
except ImproperlyConfigured as error:
print(f'{name}: ImproperlyConfigured: {error}') Output
STATIC_ROOT and MEDIA_ROOT apart: /srv/nivas/staticfiles MEDIA_ROOT = STATIC_ROOT: ImproperlyConfigured: The MEDIA_ROOT and STATIC_ROOT settings must have different values MEDIA_URL = STATIC_URL: ImproperlyConfigured: The MEDIA_URL and STATIC_URL settings must have different values
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 media_vs_static.py
Both mixes stopped with ImproperlyConfigured: the two folders must differ, and so must the two URLs. Keeping them apart
also keeps collectstatic, which rewrites STATIC_ROOT on every release, away from residents’ files. The file uploads
lesson of this track stores and checks uploads.
Checkpoint
Check: Media files are not static files
Practise: the manifest lookup
The static tag’s lookup is small enough to write yourself. Write resolve_static(name, manifest, strict=True): the
hashed name for a name, its query string and fragment kept, and Django’s own error, or the name unchanged, for a
name the manifest does not have. Its six sample tests use the hashes of the recorded run, and they run in your
browser:
Exercise · Easy · Python
Find a file's hashed name in the manifest
With manifest storage, the static tag looks every name up in staticfiles.json, the manifest that collectstatic wrote, and links the hashed copy. Write resolve_static(name, manifest, strict=True), which does that lookup:
manifestis the"paths"part of the manifest: a dictionary from a file's name to its hashed name, such as{'css/base.css': 'css/base.902a4c163d8a.css'}.nameis what a template passes to the static tag, such as'css/base.css'. Return its hashed name.- A name may end in a query string, a fragment or both, as in
'img/icons.svg#lift'. Look up the path alone and keep what follows it:'css/base.css?v=2'gives'css/base.902a4c163d8a.css?v=2'. - A name the manifest does not have raises
ValueErrorwith the message Django uses,Missing staticfiles manifest entry for 'css/missing.css', which names the path without its query or fragment. Withstrict=False, return the name unchanged instead.
Starter code · resolve.py
def resolve_static(name, manifest, strict=True):
"""The hashed name of a static file, from the manifest that collectstatic wrote."""
return manifest[name] The sample tests · test_resolve.py
from resolve import resolve_static
MANIFEST = {
'css/base.css': 'css/base.902a4c163d8a.css',
'img/logo.svg': 'img/logo.13d8e578b301.svg',
'notices/notices.css': 'notices/notices.da2d22682ee6.css',
'admin/css/base.css': 'admin/css/base.6398cd16ee3d.css',
}
def test_a_name_in_the_manifest():
assert resolve_static('css/base.css', MANIFEST) == 'css/base.902a4c163d8a.css'
def test_names_with_the_same_file_name_in_other_folders():
assert resolve_static('admin/css/base.css', MANIFEST) == 'admin/css/base.6398cd16ee3d.css'
assert resolve_static('notices/notices.css', MANIFEST) == 'notices/notices.da2d22682ee6.css'
def test_a_missing_name_raises_with_djangos_message():
try:
resolve_static('css/missing.css', MANIFEST)
except ValueError as error:
assert str(error) == "Missing staticfiles manifest entry for 'css/missing.css'"
else:
raise AssertionError('a missing name must raise ValueError')
def test_a_missing_name_stays_as_it_is_when_not_strict():
assert resolve_static('css/missing.css', MANIFEST, strict=False) == 'css/missing.css'
def test_the_query_string_and_the_fragment_are_kept():
assert resolve_static('css/base.css?v=2', MANIFEST) == 'css/base.902a4c163d8a.css?v=2'
assert resolve_static('img/logo.svg#mark', MANIFEST) == 'img/logo.13d8e578b301.svg#mark'
assert resolve_static('img/logo.svg?x=1#mark', MANIFEST) == 'img/logo.13d8e578b301.svg?x=1#mark'
def test_the_message_names_the_path_alone():
try:
resolve_static('css/missing.css?v=2#top', MANIFEST)
except ValueError as error:
assert str(error) == "Missing staticfiles manifest entry for 'css/missing.css'"
else:
raise AssertionError('a missing name must raise ValueError')
assert resolve_static('css/missing.css?v=2#top', MANIFEST, strict=False) == 'css/missing.css?v=2#top' A hint
urllib.parse.urlsplit(name) splits a name into parts: .path is the file's name, and ._replace(path=…) with urlunsplit() puts a new path back in front of the same query and fragment.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then fourteen questions on static files, from folders and finders to media, the weakest idea first, each explained as soon as you answer:
Practice that adapts to you
Practise this lesson
14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What does collectstatic do, and when do you run it?
python manage.py collectstatic copies the static files of the project’s STATICFILES_DIRS and of every installed
app’s static/ folder into one folder, STATIC_ROOT, from which the production server sends them. When two files have
the same name, the first one the finders find is copied. With manifest storage it also writes copies whose names hold a
hash of their content, and the manifest staticfiles.json. You run it as part of every release, before the new code
starts serving requests, and not while you develop, because runserver serves the app folders directly while DEBUG
is on.
Two design questions of this module start from these files: a release that leaves some users with the old stylesheet, and the choice of how to serve static files as a site grows.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
After a release, some residents see the new pages with the old stylesheet for hours, and for one minute every page with a changed template failed with a server error. Explain both problems and change the release so neither can happen again.
-
Mid to senior
Nivas serves 2 million page views a month from one server in India, and a first visit loads 3 MB of static files. It now signs up societies in Europe and South-East Asia. How would you serve static files now, and what changes as it grows? Use numbers.
Key takeaways
- Keep an app’s static files under
static/<app>/and the project’s in the foldersSTATICFILES_DIRSlists; the finders search those first, then each app inINSTALLED_APPSorder, and the first file found wins. - Build every URL with
{% static %}, which asks the storage, so prefixes, hashed names and CDNs reach every page. runserverserves static files only whileDEBUGisTrue, a method Django’s guide calls unsuitable for production.collectstaticcopies everything intoSTATIC_ROOT; manifest storage adds copies named by a hash of their content andstaticfiles.json, so hashed files can be cached for years and a missing name raisesValueError.- WhiteNoise serves the collected files from Django with ten-year
immutableheaders for hashed names and gzip copies; a web server or a CDN takes the work off Django where you run one or where readers are far away. - Uploads are media files, kept in
MEDIA_ROOTunderMEDIA_URL, which Django refuses to share with the static ones.
Terms in this lesson
- CDN
- A content delivery network: servers in many places that keep copies of a site's files and answer each visitor from one near them.
- manifest storage
- ManifestStaticFilesStorage and the storages built on it: collectstatic writes a copy of each file whose name holds a hash of its content, and staticfiles.json, the manifest the static tag reads.
- media files
- Files that users upload, such as photos and documents. Django keeps them in MEDIA_ROOT under MEDIA_URL, apart from static files, because they need their own checks and permissions.
- static files
- Files a site serves as they are, the same for every visitor, such as stylesheets, scripts, fonts and the images of the layout. In Django they live in static/ folders and collectstatic gathers them.
- STATIC_ROOT
- The folder into which collectstatic copies the static files of the project and of every app, for the production server to serve. Nobody edits files in it by hand; the next collectstatic writes it again.
- WhiteNoise
- A Python package that serves a Django project's collected static files from the Django process itself, with compressed copies and long cache headers for files whose names hold a hash.
References
- How to manage static files (Django Software Foundation)
- The staticfiles app (Django Software Foundation)
- How to deploy static files (Django Software Foundation)
- Settings: STATIC_ROOT, STORAGES, MEDIA_ROOT (Django Software Foundation)
- Using WhiteNoise with Django (WhiteNoise)
- RFC 9111: HTTP Caching (IETF)
- RFC 8246: HTTP Immutable Responses (IETF)
- Django Developers Survey 2026 results (Django Software Foundation and JetBrains)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress