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.
Three Layers, All Derived
| Layer | What it shows | Worked out from |
|---|---|---|
| 1. The bar of sites | All eight sites, the current one marked | The site map (so the addresses differ between development and production by themselves) |
| 2. The site menu | This site's subjects that really have pages | The pages' first folder, in the site map's order |
| 3. The breadcrumb and chapter links | Site, folder, course, page; previous and next chapter | The 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-mlbecomes “Data Science and ML” andcloud-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:
resolveLinkkeeps a link inside the site, gives a link into another site that site's absolute address, leaves external links, anchors and.txtfiles alone, and throws for a path no site owns: a broken link should fail, not be guessed.
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:
| Compared | Count | Different |
|---|---|---|
| Site menus | 8 | 0 |
| Previous and next chapter of every page | 4,417 | 0 |
| Breadcrumb of every page | 4,417 | 0 |
| Listing of every folder (sub-folders, counts, pages, course name, breadcrumb) | 545 | 0 |
Can the comparison fail? Three deliberate breakages, each undone afterwards:
| Planted mistake | What the comparison reported |
|---|---|
| Small words (“and”, “of”) no longer kept small | 5 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.
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 |
|---|---|
| Build | 601 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 sites | Eight links; Languages marked current, the others on their own ports |
| Site menu | France, Germany, Hungary (current), Japan, Culture |
| Breadcrumb | Languages › Hungary › Hungarian Basic Conversation 3 › Work & Careers: Applying for a Job & Interviews |
/hungary | A 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.
| Step | Address afterwards | Reloaded? | Menu marked | First run | After the fix |
|---|---|---|---|---|---|
| Open chapter 5 directly | …_3_5 | (start) | Hungary | ok | ok |
| Click Next | …_3_6 | no | Hungary | ok | ok |
| Click Previous | …_3_5 | no | Hungary | ok | ok |
| Click the course in the breadcrumb | /hungary/hungarian-basic-3 | no | Hungary | ok | ok |
| Click Japan in the menu | /japan | no | none in the first run; Japan after the fix | bug | ok |
| Browser Back | /hungary/hungarian-basic-3 | no | Hungary | ok | ok |
/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.
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
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 solutionBuild 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 solutionDrive 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 solutionChapter 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, fromparams - 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
Linkinside 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