Skip to content

Linking Static Files (CSS/JS)

Static files are assets like:

  • CSS
  • JavaScript
  • images

By default:

  • static/ folder next to your app/package

Example:

text
myapp/
  app.py
  static/
    styles.css
  templates/
    base.html

In base.html:

html
<link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
html
<script src="{{ url_for('static', filename='app.js') }}"></script>
  • handles URL prefixes (reverse proxy setups)
  • avoids hardcoded /static/...
  • works cleanly with Blueprints and app factories

In production, Nginx or a CDN often serves static files more efficiently.

But the URL patterns should still work the same.

A hard-coded /static/style.css breaks the moment the app is mounted under a prefix, or static_url_path changes, or you move to a CDN. url_for derives the path from the app’s configuration:

diagram Diagram mermaid

Measured:

callresult
url_for('static', filename='style.css')/static/style.css
url_for('static', filename='css/site.css')/static/css/site.css
url_for('static', filename='style.css', v=7)/static/style.css?v=7
url_for('static', filename='s.css', _external=True)http://localhost/static/s.css
base.html
<link rel="stylesheet" href="{{ url_for('static', filename='css/site.css') }}">
<script src="{{ url_for('static', filename='js/app.js') }}" defer></script>
<img src="{{ url_for('static', filename='img/logo.svg') }}" alt="Logo">

Note that the static endpoint exists without you registering it — creating the Flask object adds the rule /static/<path:filename> to url_map.

Cache busting is what the extra argument is for

Section titled “Cache busting is what the extra argument is for”

Browsers cache static files aggressively, which is what you want until you ship a change and users keep the old CSS. Any extra keyword becomes a query parameter:

cache_bust.py
url_for("static", filename="style.css", v=7)
# /static/style.css?v=7

Change the value and the URL changes, so the browser treats it as a new resource. Rather than editing a number by hand, derive it from something that changes with the file:

auto_version.py
import os
 
@app.context_processor
def static_version():
    def versioned(filename):
        path = os.path.join(app.static_folder, filename)
        stamp = int(os.path.getmtime(path))
        return url_for("static", filename=filename, v=stamp)
    return {"versioned": versioned}
base.html
<link rel="stylesheet" href="{{ versioned('css/site.css') }}">

Now the URL changes exactly when the file does, and never otherwise.

sketch Why a hard-coded path breaks p5.js
url_for derives the path from static_url_path and any mount prefix. A literal string does not.
pch.quizTag pch.quizDefaultTitle
  1. Why prefer url_for('static', filename='css/site.css') to a literal /static/css/site.css?

    pch.quizShowAnswer

    B — it derives the path from static_url_path and any mount prefix, so it stays correct when the app is deployed under a sub-path — A hard-coded path is right only for one deployment. Mount the app under /app or change static_url_path and it 404s — after working perfectly in development.

  2. What does url_for('static', filename='style.css', v=7) produce?

    pch.quizShowAnswer

    B — /static/style.css?v=7 — Unrecognised keywords become query parameters. Changing the value changes the URL, so the browser fetches a fresh copy — the standard cache-busting trick.

  3. Which route exists in url_map without you writing it?

    pch.quizShowAnswer

    B — /static/<path:filename> — Creating the Flask object registers the static endpoint, which is why url_for('static', ...) works immediately.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading