Static Files
Every real web page needs more than HTML — it needs CSS to look right, JavaScript to behave right, and usually a few images or icons. In Django, these files are called static files: CSS stylesheets, JavaScript files, fonts, and images that are part of your codebase. You write them, you commit them to version control, and they don't change while your app is running. That's the key distinction to hold onto right away — a stylesheet you wrote for TaskFlow is a static file, but a profile picture a TaskFlow user uploads is something else entirely: a media file. We'll draw that line sharply in the next lesson; for now, this page is entirely about the files that ship with your app.
The STATIC_URL Setting
Every Django project defines a setting called STATIC_URL in settings.py.
It doesn't point at a folder on disk — it's a URL prefix. It tells Django (and your templates)
what web address static files should be requested from.
# taskflow_project/settings.py
STATIC_URL = '/static/'
With this setting in place, a request to /static/taskflow/style.css is understood
by Django to mean "the static file named taskflow/style.css" — regardless of where
that file physically lives on disk. That mapping from URL to actual file location is handled
for you, which is exactly what makes the next section possible.
The Per-App static/ Folder Convention
Django's static files system is built around apps. Each app in your project is expected to
keep its own static files inside a folder literally named static/, sitting right
next to that app's models.py and views.py. Inside that folder,
convention says you should create another folder named after the app itself, and put
your actual files inside that. For our taskflow app, the layout looks like this:
taskflow/
__init__.py
models.py
views.py
static/
taskflow/
style.css
app.js
img/
logo.png
At first glance that nested taskflow/static/taskflow/ path looks redundant — why
name the folder after the app twice? The answer is namespacing. When Django
collects static files from every installed app into one shared location (more on that below),
it does so by folder name. If both the taskflow app and some other app in the same
project each had a file named static/style.css, one would silently overwrite the
other during collection. By nesting each app's files under a folder that matches the app's own
name — taskflow/static/taskflow/style.css — every app effectively gets its own
namespace, and two apps can each ship a style.css without ever colliding.
Loading Static Files in a Template
You never hardcode a static file's URL in a template. Instead, Django gives you a template tag
that builds the correct URL for you, using the STATIC_URL setting under the hood.
Two things are required: load the static tag library at the very top of the
template, then use the {% static %} tag wherever you need a static file's URL.
{% load static %}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="{% static 'taskflow/style.css' %}">
</head>
<body>
...
</body>
</html>
Notice the argument to {% static %} is 'taskflow/style.css' — the
path inside the app's static/ folder, including that namespacing folder,
not an absolute filesystem path and not a raw URL. Django resolves it to
/static/taskflow/style.css at render time, using whatever STATIC_URL
is set to. If you ever change STATIC_URL later, every template using
{% static %} keeps working without edits — that indirection is the whole point of
using the tag instead of writing the URL by hand.
Full Worked Example: Linking a Stylesheet in base.html
Let's wire this up for real inside TaskFlow's shared base template, the one every other TaskFlow page extends.
{# taskflow/templates/taskflow/base.html #}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}TaskFlow{% endblock %}</title>
<link rel="stylesheet" href="{% static 'taskflow/style.css' %}">
</head>
<body>
<header>
<img src="{% static 'taskflow/img/logo.png' %}" alt="TaskFlow logo">
<h1>TaskFlow</h1>
</header>
{% block content %}{% endblock %}
<script src="{% static 'taskflow/app.js' %}"></script>
</body>
</html>
Every page that extends base.html — the task list, the project detail page, the
login form — automatically picks up that stylesheet, logo, and script, because they're loaded
once in the shared base template rather than repeated on every page.
STATICFILES_DIRS: Static Files Outside Any App
Not every static file naturally belongs to a single app. A project-wide favicon, a shared
marketing stylesheet, or third-party JavaScript libraries you're vendoring locally don't fit
neatly inside taskflow/static/. For those, Django lets you list extra folders in
the STATICFILES_DIRS setting, and it will search them in addition to every app's
own static/ folder.
# taskflow_project/settings.py
import os
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
With that in place, a project-level folder such as taskflow_project/static/favicon.ico
becomes reachable through {% static 'favicon.ico' %}, exactly the same way an
app's static files are.
collectstatic: Gathering Everything Into One Place
While you're developing, Django is happy to hunt through every app's static/
folder plus every folder listed in STATICFILES_DIRS every time a static file is
requested. That's convenient, but it doesn't scale to production, where you typically want a
single, real web server (or a service like a CDN) handing out static files directly — and that
server needs one folder to point at, not a dozen scattered ones spread across your installed
apps.
That's what the collectstatic management command is for. It walks every app's
static files (and everything in STATICFILES_DIRS) and copies all of it into a
single destination folder defined by the STATIC_ROOT setting.
# taskflow_project/settings.py
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
python manage.py collectstatic
Run that, and Django reports something like 132 static files copied to
'/path/to/taskflow_project/staticfiles'. Afterward, that one staticfiles/
folder contains a flattened copy of every app's static assets — taskflow/style.css,
accounts/style.css, whatever else your installed apps contribute — all merged
together under their namespacing folders, ready to be handed to a production web server. We'll
put this folder to use for real in Serving Files
in Production.
collectstatic actually execute on this page. Try it against your own project as you follow along.Why This "Just Works" in Development but Not in Production
If you've followed along in earlier lessons and never touched STATICFILES_DIRS or
collectstatic, static files probably still loaded fine on your development server.
That's not an accident — when DEBUG = True, Django's built-in development server
automatically serves static files for you, searching every app's static/ folder on
the fly, with zero extra configuration.
The catch: that automatic behavior is only active while DEBUG = True,
and Django's own documentation is explicit that this convenience is meant for development only —
it is not built for the performance or security demands of serving real traffic. Once
DEBUG = False (as it should be in any real deployment), that automatic serving
switches off, and you're responsible for getting static files served some other way — typically
using the STATIC_ROOT folder built by collectstatic. That's exactly
where we're headed next, once we've covered media files.