Templates and Design

Learning Website with Django

Chapter 5 · Templates & the Shared Design System

The database now holds every page; the routing knows which site a request is for. This chapter makes the pages appear: a base template, a reusable theme app that carries the shared design, and a real page view that replaces the placeholders from Chapter 2. The design follows the plan in Learning Website: Framework & Architecture 4: tokens, shared components that sit under each lesson's own styles, and a safe order for loading them. Here it becomes working code, and it was looked at in a real browser.

Run for real, on Django 6.1
The project has 92 tests (all passing). Pages from the imported content were served by the development server and photographed with headless Chrome. The Chapter 2 placeholder tests had to be rewritten, which is covered below.

The Theme App

Everything visual lives in one app, apps/theme, so a change to the design is a change in one place and every site gets it:

apps/theme/ ├── context_processors.py # tells every template which site it is on ├── templates/theme/ │ ├── base.html # the page: head, site bar, main, footer │ ├── page.html # a stored page inside base.html │ └── home.html └── static/theme/ ├── tokens.css # the ONLY place a colour is decided ├── base.css # typography, layout, links ├── components.css # tip-box, code-block, tables, exercises (shared defaults) └── copy-code.js # copyCodeBlock(), and scripts inside a page body

One Element Restyles the Page

The base template puts three facts about the site on the <html> element:

<html lang="en-GB" data-site="languages" data-family="language" style="--accent: #a78bfa">
  • data-family picks the neutral colours. The language courses use one set and the technical courses another (the two sets measured in the framework course), so tokens.css defines each set once under [data-family="..."].
  • style="--accent: ..." sets the site's own colour, a CSS variable that links, the site bar's underline, table headings and the shared boxes all read.
  • The values come from one dictionary in the site map, SITE_STYLE, so a ninth site is one more line and no template changes.

The four language colours (Hungarian green, German gold, Japanese red, French blue) are set on the lesson's own wrapper, in tokens.css. A Hungarian chapter therefore keeps its green inside a site whose chrome is violet, which is what the screenshot of the languages site shows.

Shared Defaults Under Old Styles

Every existing page carries its own scoped CSS. If the shared styles beat it, old pages change; if they lose, old pages stay exactly as they are and new pages use the shared look. The cascade gives you the second for free, provided the shared rules stay less specific than a fragment's own:

RuleSpecificityWhere it comes from
.tip-boxone classcomponents.css (shared default)
.devsetup1-lesson .tip-boxtwo classesthe fragment's own style: wins
A tieequalthe later rule wins, and the fragment's <style> is in the body, after the links

Two tests keep that promise: the shared CSS never contains !important, and no shared selector uses an id or more than two parts. The screenshot of the systems site shows the result: the Debian chapter keeps its pink headings, green tip box and yellow warning box, inside a site with a salmon accent.

Accents You Can Read

A colour that is lovely and unreadable is a bug. Each accent has a test that checks its contrast against the surface it sits on and against the page background, to the 4.5 that WCAG level AA asks for normal text:

SiteAccentOn surfaceOn background
languages#a78bfa6.186.93
webdevelopment#38bdf88.078.81
programming#44b78b6.917.54
systems#f0938a7.628.31
ai#fbbf2410.3611.30
humanities#fb923c7.438.34
lifeskills#6ee7b711.0312.38
creative#f472b66.537.12

Every site's accent passes, and no two are the same. These are the site accents; the Japanese lesson red is the lessons' own colour and is still only 3.86 (Learning Website: Framework & Architecture 4), which a lighter text variant in tokens.css would fix.

The Page View

def page(request, site, page_path): path = page_path.rstrip("/") + ".html" # address -> file path page = get_object_or_404(Page, path=path, site=site) # another site's page: 404 show_h1 = "<h1" not in page.fragment.lower() return render(request, "theme/page.html", {"page": page, "show_h1": show_h1})
  • The address is the path. /hungary/x/chapter/ is the file hungary/x/chapter.html. There is no other lookup key.
  • The site check is in the query. A row whose stored site disagrees with the address is a 404, even though the route matched.
  • The heading rule is the live site's. A page without its own <h1> gets one made from its title (course chapters have no real heading of their own); a page with one does not get a second.
  • Escaping is on by default. A page's title is escaped; its stored body is marked safe in exactly one place (Chapter 4). A test stores a title of <b>x</b> and a body of <b>y</b> and checks that the first is shown as text and the second as markup.
A test of placeholder output is a test of the placeholder
Replacing the Chapter 2 views broke 11 tests, because they asked the placeholder what it returned. They were rewritten to ask the real question: “does this site's URL configuration have a route for this path?” uses django.urls.resolve, and “which site answered?” looks for data-site in the page. A routed-but-missing page and an unrouted path are both 404 now, so a status code can no longer tell the two apart.

Looking at It

The tests cannot say whether a page looks right. Serving the imported content and photographing it with headless Chrome (mapping the site names to your own machine inside the browser, so no hosts file is needed) showed:

  • Languages: a violet-underlined site bar; the chapter title as a large heading, made from the banner; the course name in the lesson's own green; dialog cards with purple speaker letters.
  • Systems: the same layout with a salmon underline; the Debian chapter in its own pink, with its green tip box, yellow warning and table.

Only desktop width was looked at, and a third screenshot (programming) was taken but not examined. A phone width and the print preview remain to check (Learning Website: Framework & Architecture 12).

Hands-On Exercises

Exercise 1

Build the theme app: a SITE_STYLE dictionary, a context processor, the three stylesheets (tokens, base, components), the copy-code script, and the base, page and home templates. Explain how one element restyles a page and why a fragment's own CSS still wins.

📄 View solution
Exercise 2

Replace the placeholder views with a real page view, rewrite the tests that depended on the placeholders, and add tests for contrast, the shared CSS rules, stylesheet order, escaping, the heading rule and the 404 cases.

📄 View solution
Exercise 3

Serve the imported content with the development server, fetch a stylesheet with curl, and photograph one page from two different sites with headless Chrome. Look at the images and write down what you see, and what you did not check.

📄 View solution

Chapter 5 Quick Reference

  • One theme app holds the whole design: tokens.css, base.css, components.css, copy-code.js and the templates
  • The <html> element carries data-site, data-family and style="--accent: ..."; the values come from SITE_STYLE
  • Load order: tokens, base, components, then the page body (with the fragment's own <style>)
  • Shared rules stay at one class, with no ids and no !important, so a fragment's own two-part rule wins
  • Language colours (.hu-lesson and the others) live on the lesson wrapper, so they survive a different site accent
  • Every accent is tested at 4.5 or more on its surface and on the page background
  • The page view: address to path, site checked in the query, one <h1> only if the body has none, title escaped, body marked safe once
  • A test that asserts on placeholder output breaks when the placeholder goes: test routing with resolve(), and the site with data-site
  • Look at the pages in a real browser; headless Chrome with --host-resolver-rules needs no hosts file