Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Session 4: Static files and base templates

Phase 8 — Flask · Session 4 of 13

What we’re learning today

Two cleanups today. First, static files — CSS, images, JS — go in a static/ folder, served automatically by Flask. Second, base templates — shared HTML (header, footer, navigation) lives in one file; other templates {% extends %} it. By the end your Flask site has clean separation: HTML in templates, CSS in stylesheets, layout shared via inheritance. Production shape.

You’ll need to remember from last time

  • Templatestemplates/ folder, render_template, {{ var }}, {% for %}, {% if %}.
  • Phase 7 CSS — selectors, properties, the box model, Flexbox.
  • HTML structure — semantic tags, navbar patterns.

Part A: Static files

The static/ folder

Just like templates/, Flask expects static files (CSS, images, JS) in a folder called static/ next to your app.py:

my_app/
├── app.py
├── templates/
│   └── home.html
└── static/
    ├── style.css
    └── images/
        └── logo.png

Files in static/ are served at URLs starting with /static/. So:

  • static/style.csshttp://127.0.0.1:5000/static/style.css
  • static/images/logo.pnghttp://127.0.0.1:5000/static/images/logo.png

Linking from a template

You could hardcode the URL:

<link rel="stylesheet" href="/static/style.css">

But the right way uses Flask’s url_for:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

url_for('static', filename='...') generates the correct URL for the static file. Why use it?

  • Works no matter where the app is mounted.
  • Survives deployment to subpaths.
  • Standard practice.

It’s longer to type but worth the habit.

Try it

Create static/style.css:

*, *::before, *::after { box-sizing: border-box; }

body {
    font-family: -apple-system, Arial, sans-serif;
    max-width: 700px;
    margin: 40px auto;
    padding: 20px;
    background: #f4f1eb;
    color: #333;
    line-height: 1.6;
}

h1 { color: #2c3e50; }

nav a {
    margin-right: 16px;
    color: #3498db;
    text-decoration: none;
}

nav a:hover {
    text-decoration: underline;
}

Update templates/home.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{ title }}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <h1>{{ heading }}</h1>
    <p>This page is styled.</p>
</body>
</html>

Save. Run. Reload. The page is styled.

Open DevTools → Network tab. Reload. You see two requests:

  • / (the HTML)
  • /static/style.css (the CSS)

Both served by Flask. Two separate things — that’s how the web works.

Adding an image

Drop an image into static/images/logo.png (any PNG you like).

In your template:

<img src="{{ url_for('static', filename='images/logo.png') }}" alt="Logo">

Same pattern — url_for('static', filename='path/ to/file').

What goes in static?

  • CSS files.
  • JavaScript files (for any client-side interactivity).
  • Images, fonts, downloadable files.

Anything that’s served as-is (not generated by Python).

Checkpoint: Your Flask app serves at least one CSS file from static/ and uses url_for to link it. This is the natural stop point if class is cut short.


Part B: Base templates with inheritance

The problem

Right now, every template has the full <!DOCTYPE html>...</html> boilerplate. Add a new page → copy the boilerplate. Change the navbar → edit every page. Mess.

The solution — {% extends %}

Make a base template that has the shared structure. Each child template extends it and fills in blocks.

templates/base.html:

<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Site{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
            <a href="/projects">Projects</a>
        </nav>
    </header>
    
    <main>
        {% block content %}{% endblock %}
    </main>
    
    <footer>
        <p>&copy; 2026 My Site</p>
    </footer>
</body>
</html>

Notice the blocks{% block title %} and {% block content %}. Child templates fill these in.

templates/home.html:

{% extends "base.html" %}

{% block title %}Home{% endblock %}

{% block content %}
<h1>Welcome!</h1>
<p>This is my Flask site.</p>
<p>Navigation works because the navbar lives in <code>base.html</code>.</p>
{% endblock %}

templates/about.html:

{% extends "base.html" %}

{% block title %}About{% endblock %}

{% block content %}
<h1>About</h1>
<p>I'm learning Flask. This is page 2.</p>
{% endblock %}

Update app.py:

@app.route("/")
def home():
    return render_template("home.html")

@app.route("/about")
def about():
    return render_template("about.html")

@app.route("/projects")
def projects():
    return render_template("projects.html")    # add this template too

Save. Run. Visit each page. Same navbar, same footer, different content. Each page is just the main content — base.html provides the chrome.

If you change the navbar in base.html, every page updates. One source of truth. Real production practice.

How it works

  1. {% extends "base.html" %} — child says “I’m based on this template.”
  2. {% block content %}...{% endblock %} — child replaces the block with new content.
  3. The base’s other content (navbar, footer) stays the same.

Default content in a block:

{% block title %}My Site{% endblock %}

If a child doesn’t define {% block title %}, the default (“My Site”) is used. Otherwise the child’s content replaces it.

Add a third page

Make templates/projects.html:

{% extends "base.html" %}

{% block title %}Projects{% endblock %}

{% block content %}
<h1>My projects</h1>
<ul>
    <li>Pong (Pygame)</li>
    <li>Todo app (customtkinter)</li>
    <li>This Flask site</li>
</ul>
{% endblock %}

Save. Visit /projects (after adding the route). Same navbar, fresh content. Five lines of unique HTML; everything else inherited.

Stretch — url_for for pages too

Hardcoded route URLs (<a href="/about">) get fragile if you renamed routes. url_for works for your own routes too:

<a href="{{ url_for('about') }}">About</a>

url_for('about') calls the function named about (matching your def about():) and returns its URL.

If you renamed def about(): to def about_me():, you’d just update one place; all url_for('about_me') calls update automatically.

Use url_for for your routes too. Real production discipline.

Stretch — multiple blocks

<!-- base.html -->
<head>
    {% block extra_head %}{% endblock %}
</head>

Child can inject head content (extra CSS, scripts):

{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='extra.css') }}">
{% endblock %}

Multiple blocks let children customize specific parts.

Stretch — {% include %}

For reusable chunks (a card, a sidebar) that aren’t the whole page:

{# templates/_card.html — partial #}
<div class="card">
    <h3>{{ title }}</h3>
    <p>{{ description }}</p>
</div>
{# templates/index.html #}
{% include "_card.html" %}
{% include "_card.html" %}
{% include "_card.html" %}

Underscored filenames are convention for “partials” — pieces of templates, not full pages.

Extension — block.super

Append to a parent block instead of replacing:

{% block title %}Special Page — {{ super() }}{% endblock %}

Combines parent and child content. Useful for title prefixes and accumulating CSS/JS.


Wrap-up

Before we leave, share with the room:

  • For everyone — show your site. How many pages share the same base?
  • Did changing one thing in base.html and seeing every page update feel powerful?
  • For the kids who used url_for for routes — does the indirection make sense?

Today you learned:

  • static/ folder — Flask’s convention for CSS, JS, images.
  • url_for('static', filename='...') — generate static URLs.
  • {% extends "base.html" %} — child template inherits from a base.
  • {% block name %}{% endblock %} — fillable spot in a base; child overrides.
  • One source of truth — base layout in one file, content per page.
  • {% include "..." %} — reusable partials.
  • url_for('route_name') — generate URLs from route function names.

Your sites now have the shape of a real production site: clean separation between shared chrome and per-page content. Real Django sites, real Rails sites, real PHP sites — all work the same way.

Next week: forms — accept user input via POST requests. The first time the user changes data on the server.

If you missed this session

Open Thonny.

  1. Create a static/ folder. Put a CSS file in it.

  2. Link from your templates with url_for.

  3. Create templates/base.html with {% block %} tags.

  4. Refactor at least 2 pages to extend the base.

About 30-45 minutes. By the end your site should share a base layout.

Stretch and extension ideas

  • url_for for routes too — clean indirection.
  • Multiple blocks — content, title, extra_head, etc.
  • {% include %} partials — reusable components.
  • block.super() — append to parent block.
  • CSS variables in your stylesheet (Phase 7 callback).
  • Flexbox layouts in your CSS (Phase 7 callback).
  • Mobile-responsive design with @media queries.
  • Error templatestemplates/404.html, registered with @app.errorhandler(404).
  • Favicon — drop static/favicon.ico and add <link rel="icon"> in base.

What’s next

Next week: forms and POST requests. Users submit forms; your Python processes them and responds. The first time data flows from the browser to your server.