Navigation

Learning Website with Django

Chapter 6 ยท Navigation & Breadcrumbs

A visitor needs to know three things at every moment: which site am I on, what is on this site, and where am I within it. Learning Website: Framework & Architecture 5 designed three layers of navigation for that. This chapter builds them in Django, and keeps one rule throughout: every layer is derived from data that already exists, so none of them can drift out of date the way the live site's hand-written menu has.

Run for real, on Django 6.1
The project has 125 tests (all passing). The navigation was checked against all 4,403 imported pages and photographed with headless Chrome. The one thing that could not be checked is a true phone-width layout; the chapter says so where it matters.

Three Layers, Three Sources

LayerAnswersDerived from
1. Global barWhich site am I on, and what are the others?The site map (SITES); the current site is marked
2. Site menuWhat is on this site?The folders in the site map that really have pages in the database
3. Breadcrumb, previous/nextWhere am I?The page's path, its course and its chapter number

All three are exposed to every template by the context processor from Chapter 5, so a template only has to loop over global_bar, site_menu and breadcrumbs.

Layer 1: The Global Bar

It lists the eight sites in the site map's order, the same on every page, with the current one marked aria-current (so a screen reader says so as well as the colour). Each address comes from one setting, SITE_URL_TEMPLATE:

# config/settings/prod.py SITE_URL_TEMPLATE = "https://{site}.osztromok.com" # config/settings/dev.py SITE_URL_TEMPLATE = "http://{site}.localhost:" + os.environ.get("LW_DEV_PORT", "8000")

Changing the domain, or the development port, is one line, and the bar follows. It scrolls sideways on a narrow screen instead of wrapping into several rows.

Layer 2: The Site Menu

The menu is the site's folders, in the site map's order, that have pages. It reads the database once and is kept in Django's cache for an hour; the importer clears it when it finishes, so a new import shows at once. After the first request, the menu costs no database query, which a test proves with assertNumQueries(0).

Derived menus cannot drift
The live site's hand-written menu was missing 13 of its 49 top-level folders (Learning Website: Framework & Architecture 5). The derived menus were checked against the real pages: all 49 folders that have pages appear in a site menu, none appears in two (except the sidebar, which each site shows only its own part of), and no folder in the site map is empty.

Readable labels

A menu entry is a folder name made readable. The live site's helper capitalised every word, giving “Data Science And Ml” and “Cloud And Devops”. The new one lower-cases small joining words, upper-cases acronyms, and has a short list of names with their own capitals:

Folder nameLive siteHere
data-science-and-mlData Science And MlData Science and ML
cloud-and-devopsCloud And DevopsCloud and DevOps
maths-for-programmersMaths For ProgrammersMaths for Programmers
ios-developmentIos DevelopmentiOS Development

The sidebar needs routes too

Building the menu exposed a gap from Chapter 2: its URL configuration was made from the folders in the site map, so /sidebar/... was a 404 on every site. The sidebar belongs to the site its subject is routed to (sidebar/linux to systems, sidebar/football to humanities), and /sidebar/ is that site's own index. The pattern keeps the sidebar index as a separate alternative (sidebar/?) so that /sidebar/football/x/ can never match on the systems site. Tests check both directions.

Layer 3: Where You Are

The breadcrumb

The trail is built from the page's path: Site › folder › folder › this page. A course folder is labelled with the course's real name (“Hungarian Basic Conversation 3”, one query for all the folders on the path), any other folder with its readable name, and the page you are on is plain text, not a link.

Previous and next

They follow the chapter number, so chapter 10 comes after 9, and a page with no course or number simply has neither link. The links carry rel="prev" and rel="next".

Every folder is a page

A breadcrumb that links to a folder is a promise that the folder page exists. So the page view serves either a page or a folder: a folder lists its sub-folders (with page counts) and its own pages, and a course folder lists its chapters in numeric order under the course's name. An address that is neither is a 404, and another site's folder is a 404 on this one.

def page(request, site, page_path): bare = page_path.strip("/") found = Page.objects.filter(path=bare + ".html", site=site).first() if found is not None: ...render the page, with breadcrumbs and previous/next... content = listing(site, bare) # a folder? if content is None: raise Http404("No such page or folder") ...render the listing...

Links Between Sites

The global bar links across sites with absolute addresses. For links inside page content, the resolver from the framework course is included (resolve_link): a same-site link stays root-relative, a cross-site link becomes an absolute address on the right site, and a path that belongs to no site raises an error rather than being guessed. It is not applied to page bodies today because the content has no real cross-site links (Learning Website: Framework & Architecture 11); it is ready for the first one.

What It Costs, and What Was Not Checked

  • Queries: a chapter page costs 4 database queries and a folder listing 4 (the page, the course names for the breadcrumb, the two neighbours). The menus add none, because they are cached.
  • Looked at: screenshots of the Hungarian course listing and of the Linux folder show the global bar with the current site in the accent colour, the site menu with the active subject underlined, the breadcrumb, and the twelve chapters numbered in order.
  • Not verified: phone width. A headless screenshot at 390 pixels did not look like a true phone layout (it seemed to be laid out about 500 pixels wide and cropped, so one menu item looked cut off), so the narrow layout has not been checked. Test it with a real browser's device toolbar.

Hands-On Exercises

Exercise 1

Build the global bar and the cached site menu from the site map and the database, write readable folder labels (small words, acronyms, product names), add the link resolver, and make the importer clear the cached menus.

๐Ÿ“„ View solution
Exercise 2

Add breadcrumbs, previous and next links and folder listings, make the page view serve a page or a folder, route the sidebar to the right site, and test all of it, including numeric chapter order and the sidebar boundaries.

๐Ÿ“„ View solution
Exercise 3

Check the navigation against the real imported pages: which folders appear in which menu, whether any folder is missing or in two menus, and how many queries a page costs. Look at it in a browser, and write down what you could not verify.

๐Ÿ“„ View solution

Chapter 6 Quick Reference

  • Three layers, all derived: the global bar (site map), the site menu (folders with pages), the breadcrumb and previous/next (path, course, chapter number)
  • Site addresses come from one setting, SITE_URL_TEMPLATE; the current site and active subject use aria-current
  • The menu is cached for an hour and cleared by the importer; a repeat request makes no query for it
  • Labels: small words lower case, acronyms upper case, product names their own capitals ("Data Science and ML", "Cloud and DevOps")
  • Course folders are labelled with the course name in the breadcrumb; the page you are on is not a link
  • Previous/next use the chapter number; a page with no number has neither
  • Every folder is a page (sub-folders with counts, then the pages); an unknown folder, or another site's, is a 404
  • The sidebar routes to the site its subject belongs to; /sidebar/ is that site's own index; the index is a separate pattern so subjects cannot leak
  • resolve_link turns cross-site links absolute and raises for a path in no site
  • All 49 real folders appear in a menu; a page costs 4 queries; phone width still needs a real-browser check