Layout and Navigation

Learning Website with Next.js

Chapter 6 ยท Layout & Navigation

A visitor needs three answers on every page: which site is this, where in it am I, and where can I go next. This chapter builds them in the Next.js App Router with layouts, and works out every one of them from the pages themselves, so none can drift from the content. As elsewhere in this course, the result is compared with the Django project's on the real content, and then clicked through in a real browser, which found a bug the comparison could not.

Run for real, on 4,417 pages and 545 folders
50 tests pass; about 9,400 navigation answers were compared with the Django project's (0 differences) and three planted mistakes were all caught; the languages site was built (601 pages, 13 s) and clicked through in headless Chrome. The web development app shows its menu but has no content routes yet, so its menu links lead nowhere.

Three Layers, All Derived

LayerWhat it showsWorked out from
1. The bar of sitesAll eight sites, the current one markedThe site map (so the addresses differ between development and production by themselves)
2. The site menuThis site's subjects that really have pagesThe pages' first folder, in the site map's order
3. The breadcrumb and chapter linksSite, folder, course, page; previous and next chapterThe page's path, its course's name, and chapter numbers

The logic is a package of plain functions, @lw/navigation, with no React in it, so it can be tested without a browser. A few of its rules:

  • Labels: data-science-and-ml becomes “Data Science and ML” and cloud-and-devops “Cloud and DevOps”: small joining words stay lower case, a list of acronyms is upper-cased, and a few product names have fixed spellings.
  • Breadcrumbs: a course folder is labelled with the course's name, not its folder name; the page you are on is not a link.
  • Previous and next: by chapter number, so chapter 10 follows 9, not 1.
  • Folder listings: sub-folders with page counts, then the folder's own pages in chapter order. The folder pages are the site's index, and nothing was written to make them.
  • Links between sites: resolveLink keeps a link inside the site, gives a link into another site that site's absolute address, leaves external links, anchors and .txt files alone, and throws for a path no site owns: a broken link should fail, not be guessed.
What went wrong, and how to avoid it (1)
The mistake: my first version of the index class declared a field in its constructor (constructor(readonly site: SiteName, ...)). Node runs TypeScript by stripping the types, and refused it: “parameter property is not supported in strip-only mode”. The takeaway: write TypeScript that is only types plus plain JavaScript (declare the field on its own line), and the same files run in Node, in the tests and in Next.

Is It the Same as Django's?

A script in the Django project writes what its navigation says for the real content; the TypeScript version is asked the same questions:

ComparedCountDifferent
Site menus80
Previous and next chapter of every page4,4170
Breadcrumb of every page4,4170
Listing of every folder (sub-folders, counts, pages, course name, breadcrumb)5450

Can the comparison fail? Three deliberate breakages, each undone afterwards:

Planted mistakeWhat the comparison reported
Small words (“and”, “of”) no longer kept small5 menus, 1,158 breadcrumbs and 197 folder lists different
Previous chapter chosen with <= instead of <3,907 previous/next different
Chapters sorted as text (10 before 2)3,115 previous/next different

Layouts

In the App Router a layout wraps the pages below it and is not drawn again when the visitor moves between them, which is what a site frame wants. Two are used:

  • The root layout (app/layout.tsx): the page frame, the bar of sites and the site's menu, the same on every page.
  • A nested layout (app/[...path]/layout.tsx) for content pages: it makes the breadcrumb. It is not redrawn when the visitor moves, but it is handed the address's segments, so the breadcrumb can be made from them.
export default async function ContentLayout({ children, params }) { const { path } = await params; const page = content.pageFor(path); const crumbs = page ? nav.breadcrumbs(/* the page's folder */, page.title) : nav.breadcrumbs(decodeURIComponent(path.join("/"))); return <><Breadcrumbs crumbs={crumbs} />{children}</>; }

One route handles two kinds of address: a page, or a folder (a listing). generateStaticParams lists both, a page wins if a page and a folder share an address, and anything else is a 404. Links inside the site use Next's Link (a click loads only what is new); the bar of sites uses plain links because they go to other hosts. The menu is a client component for one reason only: the root layout is not redrawn when the visitor moves, so something has to ask the browser where we are now (usePathname) to mark the current subject. Everything else sends no script.

Real build and fetch (languages site)Result
Build601 pages made (561 pages, the folder listings, the home page, the 404 page and a few of Next's own), 3.0 s of generation, 13.2 s in all
Bar of sitesEight links; Languages marked current, the others on their own ports
Site menuFrance, Germany, Hungary (current), Japan, Culture
BreadcrumbLanguages › Hungary › Hungarian Basic Conversation 3 › Work & Careers: Applying for a Job & Interviews
/hungaryA listing: Hungarian Alphabet 1 (20), Basic 1 (12), Basic 2 (12), Basic 3 (12), Hungarian Lessons (12)

Click Through It

Reading the HTML shows that the links are there, not what happens when they are clicked. A real headless Chrome opened a lesson, put a marker on the page's window, and clicked through the navigation. If the marker survives a click, the page was not reloaded.

StepAddress afterwardsReloaded?Menu markedFirst runAfter the fix
Open chapter 5 directly…_3_5(start)Hungaryokok
Click Next…_3_6noHungaryokok
Click Previous…_3_5noHungaryokok
Click the course in the breadcrumb/hungary/hungarian-basic-3noHungaryokok
Click Japan in the menu/japannonone in the first run; Japan after the fixbugok
Browser Back/hungary/hungarian-basic-3noHungaryokok
What went wrong, and how to avoid it (2)
The mistake: the menu marks an item current when the path starts with the item's address. The addresses end in a slash (/japan/) but Next serves /japan, which does not start with /japan/. Pages inside a subject worked, so looking at lesson pages never showed it; only a click on a subject's own page did. The takeaway: test the case at the edge of a rule (the subject's own page, not only pages below it), and compare addresses without relying on a trailing slash. The fix: compare with the slash removed, and require a slash after the subject (pathname === base || pathname.startsWith(base + "/")), which also stops /ai matching /ai-tools.
What was not done
The web development app has a menu but no content routes, so its menu links (including Sidebar) lead to a 404 until that site's pages are built; only the languages site is complete. resolveLink is tested but not yet applied to links inside lesson pages (Chapter 11). Keyboard use and a screen reader were not tried, and only Chrome was used.

Hands-On Exercises

Exercise 1

Write the navigation as plain, tested functions: labels, the bar of sites, the site menu, breadcrumbs, previous and next chapter, folder listings and cross-site links. Compare every answer with the Django project's on the real content, and plant three mistakes to show the comparison catches them.

๐Ÿ“„ View solution
Exercise 2

Build the navigation components and a root layout plus a nested layout that makes the breadcrumb from the address. Make one route serve both pages and folder listings, keep the menu as the only client component, build the languages site and fetch it.

๐Ÿ“„ View solution
Exercise 3

Drive a real headless Chrome through the navigation and check that each click avoids a page reload and that the menu follows. Record any bug you find, fix it, and say what you did not test.

๐Ÿ“„ View solution

Chapter 6 Quick Reference

  • Three layers, all derived: the bar of sites (site map), the site menu (the pages' first folders), the breadcrumb and chapter links (path, course name, chapter numbers)
  • @lw/navigation: folderLabel, globalBar, NavIndex (menu, breadcrumbs, neighbours, listing), resolveLink
  • Course folders are labelled with the course's name; previous and next go by chapter number
  • Root layout: frame, bar of sites, menu. Nested layout [...path]/layout.tsx: the breadcrumb, from params
  • A layout is not redrawn when the visitor moves; only the menu is a client component, to mark the current subject with usePathname
  • One route for pages and folder listings; a page wins over a folder of the same address; anything else 404
  • Link inside the site (no reload); plain links to other sites
  • Identical to Django on 8 menus, 4,417 prev/next, 4,417 breadcrumbs, 545 folder lists; three planted mistakes all caught
  • Found in Chrome: the menu did not mark a subject's own page (trailing slash); fixed
  • Node's type stripping refuses constructor parameter properties: declare fields on their own line