Styling & Templates

Personal Catalogue: Django & PostgreSQL

Chapter 8 · Styling & Templates

The functional core is done: real schema, a real admin, real public list/detail pages, a real add-item form, tag filtering, and search. This chapter deliberately comes after all of that, not before — matching this project's own real deadline-driven priority order from Chapter 1, function before polish.

Django's Static Files System

CSS, images, and any other static asset live in a dedicated app-level directory, wired up through a small, one-time settings change:

# catalogue_site/settings.py STATIC_URL = 'static/' STATICFILES_DIRS = [BASE_DIR / 'catalogue' / 'static']

Create catalogue/static/catalogue/style.css — the extra catalogue/ nesting inside static/ is a real, deliberate Django convention (namespacing static files by app), avoiding a real file-name collision if a second app in this project ever also defines a style.css.

A Real Base Template With Actual Styling

<!-- catalogue/templates/catalogue/base.html --> {% load static %} <!DOCTYPE html> <html> <head> <title>{% block title %}My Catalogue{% endblock %}</title> <link rel="stylesheet" href="{% static 'catalogue/style.css' %}"> </head> <body> <nav> <a href="{% url 'item_list' %}">My Catalogue</a> <a href="{% url 'item_add' %}">Add Item</a> <a href="{% url 'tag_index' %}">Browse Tags</a> </nav> <main>{% block content %}{% endblock %}</main> </body> </html>

{% load static %} must appear before any {% static %} tag is used in the template — a real, easy-to-forget requirement, since forgetting it produces a confusing TemplateSyntaxError pointing at the {% static %} line itself rather than at the missing {% load %} that actually caused it.

A Reusable Item Card Partial

Both the list page and the tag-filtered list page render items the same way — a real, genuine case for Django's own template composition, rather than maintaining the same markup twice:

<!-- catalogue/templates/catalogue/_item_card.html --> <article class="item-card"> <h3><a href="{% url 'item_detail' item.pk %}">{{ item.title }}</a></h3> <p>{{ item.item_type }}{% if item.creator %} &middot; {{ item.creator }}{% endif %}</p> {% for tag in item.tags.all %} <a class="tag-pill" href="?tag={{ tag.name }}">#{{ tag.name }}</a> {% endfor %} </article>

And in item_list.html:

{% for item in items %} {% include 'catalogue/_item_card.html' %} {% endfor %}
Why {% include %} Instead of Repeating the Markup
A leading underscore on _item_card.html is a real, common Django community convention — not a Django feature itself — marking a template as a partial, meant to be included rather than rendered directly as a page. Pulling this markup into its own file means a future styling change to how an item card looks only needs to happen in one place, rather than being kept in sync across the list page and anywhere else an item might eventually be rendered the same way.

Real Dark-Theme Styling

/* catalogue/static/catalogue/style.css */ body { background: #0d1117; color: #c9d1d9; font-family: system-ui, sans-serif; margin: 0; } nav { background: #161b22; padding: 1rem 1.5rem; display: flex; gap: 1.5rem; } nav a { color: #7fd6b4; text-decoration: none; } main { max-width: 800px; margin: 0 auto; padding: 1.5rem; } .item-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; margin-bottom: 1rem; } .tag-pill { display: inline-block; background: #0c1f18; color: #7fd6b4; border: 1px solid #14523f; border-radius: 999px; padding: 2px 10px; margin-right: 4px; font-size: 0.8rem; text-decoration: none; }
STATICFILES_DIRS Isn't Where Files Live in Production
STATICFILES_DIRS is where Django's own development server looks for static files while DEBUG = True — genuinely convenient locally, but Django's development server is explicitly documented as unsuitable for serving static files in production. A real production deployment instead runs python manage.py collectstatic, which copies every static file from every app into one single folder — STATIC_ROOT, a separate setting not yet defined in this project — for a real web server (or a service like WhiteNoise) to serve directly. This distinction doesn't matter yet, but it matters directly once Chapter 9 covers real deployment.

Trying It Out

Reload the list page and confirm the nav bar, dark theme, and pill-styled tags all render correctly, and that clicking a tag pill still filters exactly as it did before this chapter — styling changed nothing about the underlying filtering logic from Chapter 6.

Hands-On Exercises

Exercise 1

Build style.css, the updated base.html, and the _item_card.html partial, then update item_list.html to use {% include %} instead of its own inline item markup.

📄 View solution
Exercise 2

Deliberately remove {% load static %} from the top of base.html, reload the page, and record the exact error Django produces — then explain why the error points at the {% static %} line rather than at the missing {% load %} tag itself.

📄 View solution
Exercise 3

Explain, in your own words, why STATICFILES_DIRS being sufficient in development doesn't mean it's sufficient in production, and what collectstatic and STATIC_ROOT actually do differently.

📄 View solution

Chapter 8 Quick Reference

  • Static filesSTATICFILES_DIRS plus {% load static %} and {% static %} wire up CSS in development
  • _item_card.html — a reusable partial included via {% include %}, keeping item-card markup defined in exactly one place
  • Real gotcha — forgetting {% load static %} produces an error pointing at the wrong line
  • Real production distinctionSTATICFILES_DIRS serves files in development only; collectstatic and STATIC_ROOT handle real production serving, covered directly in Chapter 9
  • Next chapter: Deployment — taking this project from a local dev server to something actually live