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.

Rule of thumb: always write the app name twice in the path — once for the folder Django scans, once for the namespace. It feels repetitive until the day it saves you from an accidental overwrite.

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.

Note: This is correct, real Django configuration — but this website's own project doesn't run a Django server, so you won't see 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.

Next up: Static files are fixed, developer-authored assets. But what about files your users upload — a profile picture, a project cover image? That's Media Files & Uploads, next.