Beetje Coderen

Les 4 · Module 1 · Flask-basis

Statische bestanden en een nette layout

Geef de kassa een eigen stijl: CSS, afbeeldingen en JavaScript serveren uit static/, met url_for, cache-busting en een responsive basislayout.

โฑ 60–75 minuten leestijd + oefenen

Illustratie: een browservenster met gekleurde vlakken naast een stapel documenten, de stijlbestanden van een webpagina

๐ŸŽฏ Na deze les kun je

  • CSS, afbeeldingen en scripts serveren uit de map static/
  • met url_for('static', filename=…) naar statische bestanden linken
  • een responsive basislayout bouwen met een header, menu en hoofdinhoud
  • uitleggen waarom browsers statische bestanden cachen en hoe je dat omzeilt bij een wijziging

Je kassa heeft nu templates, maar ziet eruit als 1995: zwarte Times New Roman op wit. In deze les geef je de app een eigen stylesheet, een logo en een klein script. Zulke bestanden veranderen niet per verzoek en heten daarom statische bestanden (static files). Flask serveert ze voor je uit de map static/.

De map static

kassa-web/
โ”œโ”€โ”€ app.py
โ”œโ”€โ”€ static/
โ”‚   โ”œโ”€โ”€ css/kassa.css
โ”‚   โ”œโ”€โ”€ img/logo.svg
โ”‚   โ””โ”€โ”€ js/kassa.js
โ””โ”€โ”€ templates/

Alles in static/ is bereikbaar onder /static/โ€ฆ. Zet een bestand static/css/kassa.css neer en open http://127.0.0.1:5000/static/css/kassa.css: je ziet de CSS. Je hoeft geen route te schrijven.

Linken met url_for('static', โ€ฆ)

Schrijf in base.html nooit /static/css/kassa.css met de hand, maar:

<link rel="stylesheet" href="{{ url_for('static', filename='css/kassa.css') }}">
<script src="{{ url_for('static', filename='js/kassa.js') }}" defer></script>

Draait je app later onder een subpad (bijvoorbeeld /kassa/) of komen statische bestanden van een CDN, dan hoef je alleen de configuratie aan te passen, niet elke template.

Een basislayout met CSS

Werk templates/base.html bij:

<!doctype html>
<html lang="nl">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{% block titel %}Kassa{% endblock %} ยท kassa-web</title>
  <link rel="stylesheet" href="{{ url_for('static', filename='css/kassa.css') }}">
</head>
<body>
  <header class="kop">
    <img src="{{ url_for('static', filename='img/logo.svg') }}" alt="" width="40" height="40">
    <nav>
      <a href="{{ url_for('home') }}">Start</a>
      <a href="{{ url_for('toevoegen') }}">Toevoegen</a>
      <a href="{{ url_for('bon_tonen') }}">Bon</a>
    </nav>
  </header>
  <main class="inhoud">{% block inhoud %}{% endblock %}</main>
</body>
</html>

En static/css/kassa.css:

:root { --accent: #2a9d8f; --tekst: #264653; }
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; color: var(--tekst); }
.kop { display: flex; align-items: center; gap: 1rem; padding: .75rem 1rem; background: var(--accent); }
.kop nav { display: flex; gap: 1rem; flex-wrap: wrap; }
.kop a { color: white; text-decoration: none; }
.inhoud { max-width: 40rem; margin: 2rem auto; padding: 0 1rem; }
table { width: 100%; border-collapse: collapse; }
td { padding: .4rem; border-bottom: 1px solid #ddd; }
td:last-child { text-align: right; font-variant-numeric: tabular-nums; }
.duur { background: #fff3cd; }
.fout { color: #9b2226; font-weight: bold; }

Voorspel eerst: je verandert --accent in #e76f51, slaat op en laadt de pagina opnieuw. Zie je meteen de nieuwe kleur?

Uitkomst

Meestal wel met de debug-server, maar niet altijd. Browsers cachen statische bestanden. Flask stuurt er een Last-Modified-header en een ETag bij, zodat de browser kan vragen "is dit nog hetzelfde?" (antwoord 304 Not Modified). Zie je de oude kleur, doe dan een harde herlaadactie (Ctrl+Shift+R). In productie los je dit op met cache-busting, zie hieronder.

Cache-busting

In productie wil je dat browsers CSS lang bewaren (sneller), maar wel direct een nieuwe versie ophalen na een wijziging. Het simpelste trucje: zet een versie in de URL.

import os

@app.context_processor
def statische_versie():
    def static_v(pad):
        stempel = int(os.path.getmtime(os.path.join(app.static_folder, pad)))
        return url_for("static", filename=pad, v=stempel)
    return {"static_v": static_v}

In de template: href="{{ static_v('css/kassa.css') }}" โ†’ /static/css/kassa.css?v=1760380000. Verandert het bestand, dan verandert de URL, en haalt de browser hem opnieuw op.

Een context processor voegt variabelen of functies toe aan elke template. Je komt hem later weer tegen voor de ingelogde gebruiker.

Een klein beetje JavaScript

static/js/kassa.js vraagt bevestiging voor het leegmaken van de bon:

document.addEventListener("submit", (e) => {
  if (e.target.matches("[data-bevestig]") && !confirm(e.target.dataset.bevestig)) {
    e.preventDefault();
  }
});

En in bon.html: <form method="post" action="โ€ฆ" data-bevestig="Weet je het zeker?">. Merk op: geen onclick="โ€ฆ" in de HTML. Losse scripts uit een bestand werken ook als je later een strenge Content-Security-Policy instelt (les 14).

Typische fouten

  • 404 op /static/css/kassa.css: het bestand staat in templates/ of de map heet statics.
  • Afbeelding kapot in een submap-pagina: je schreef src="static/img/logo.svg" zonder / aan het begin. Met url_for gebeurt dit niet.
  • Wijziging niet zichtbaar: cache. Harde herlaadactie of cache-busting.

Oefenen

  1. Maak static/img/logo.svg: een eenvoudige cirkel met een munt-kleur, en toon hem in de kop.
  2. Voeg een favicon toe (static/favicon.ico of .svg) en link hem in base.html.
  3. Gebruik static_v voor de CSS en controleer met curl -I dat de URL een ?v= heeft en het bestand 200 geeft.
  4. Maak de menu-link van de huidige pagina vet. Tip: request.endpoint is ook in templates beschikbaar.
Modelantwoord oefening 1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
  <circle cx="20" cy="20" r="18" fill="#e9c46a" stroke="#264653" stroke-width="3"/>
  <circle cx="20" cy="20" r="9" fill="none" stroke="#264653" stroke-width="2"/>
</svg>

alt="" in base.html is hier juist: het logo is decoratief en het menu ernaast zegt al waar je bent.

Modelantwoord oefening 2
<link rel="icon" href="{{ url_for('static', filename='favicon.svg') }}" type="image/svg+xml">

Zonder link vraagt de browser zelf /favicon.ico op; die geeft dan een 404 in je log. Wil je dat stil hebben, zet dan ook een favicon.ico in static/ en een route /favicon.ico die er met redirect naar wijst.

Modelantwoord oefening 3
curl -s http://127.0.0.1:5000/ | grep kassa.css
curl -I "http://127.0.0.1:5000/static/css/kassa.css?v=1760380000"

De eerste regel toont href="/static/css/kassa.css?v=โ€ฆ". Flask negeert de query-parameter bij het serveren: het bestand komt gewoon terug met 200 OK.

Modelantwoord oefening 4
{% macro menulink(endpoint, label) %}
  <a href="{{ url_for(endpoint) }}"{% if request.endpoint == endpoint %} aria-current="page"{% endif %}>{{ label }}</a>
{% endmacro %}

Met CSS .kop a[aria-current] { font-weight: bold; }. aria-current helpt ook schermlezers, beter dan alleen een class.

Lokaal oefenen

Geen live lab: open de site in je browser en gebruik de ontwikkelaarstools (F12 โ†’ Netwerk) om te zien welke bestanden geladen worden, met welke statuscode (200 of 304).

Samenvatting

  • static/ wordt automatisch geserveerd onder /static/; link met url_for('static', filename=โ€ฆ).
  • Een basislayout met CSS-variabelen en flexbox geeft alle pagina's รฉรฉn stijl.
  • Browsers cachen statische bestanden; met een versie in de URL dwing je een nieuwe versie af.
  • Volgende les: formulieren echt goed verwerken en valideren.

๐Ÿ“ Quiz: statische bestanden en layout

5 vragen ยท 70% nodig om te slagen

1. Onder welke URL is static/css/kassa.css bereikbaar?
2. Waarom gebruik je url_for('static', filename=…) in plaats van een vast pad?
3. Wat betekent een 304 Not Modified op een CSS-bestand?
4. Wat doet een context processor?
5. Waarom zet je JavaScript in een los bestand in plaats van onclick-attributen?