learning-website-nextjs1-4 Exercise 2: Make Every Page When the Site Is Built ================================================================================ Next.js can make a page when the site is built (static generation) from a list of addresses. Here the list comes from the content folder: one address for every page of the site. The visitor then receives a finished HTML file. Save as apps/languages/lib/site-content.ts: import { loadPages, prepareFragment, readContentFile, type Page } from "@lw/content"; const SITE = "languages" as const; /** The content folder. There is no default: a build that quietly reads the wrong folder is worse than one that stops. */ export function contentRoot(): string { const root = process.env["LW_CONTENT_ROOT"]; if (!root) throw new Error("Set LW_CONTENT_ROOT to the content folder before building."); return root; } /** The URL segments of a page: hungary/x/y.html is ["hungary", "x", "y"]. */ export function urlSegments(page: Page): string[] { return page.path.slice(0, -".html".length).split("/"); } let cache: Map | undefined; /** Every page of this site, by its URL. Read once per build process; any problem file stops the build. */ export function sitePages(): Map { if (cache === undefined) { const { pages, errors } = loadPages(contentRoot(), { site: SITE }); if (errors.length > 0) { throw new Error(`${errors.length} content files could not be read, for example ${errors[0]?.path}: ${errors[0]?.message}`); } cache = new Map(pages.map((page) => [urlSegments(page).join("/"), page])); } return cache; } /** Next may hand over percent-encoded segments for non-ASCII addresses; a segment that cannot be decoded is kept as it is. */ function decodeSegment(segment: string): string { try { return decodeURIComponent(segment); } catch { return segment; } } export function pageFor(segments: readonly string[]): Page | undefined { return sitePages().get(segments.map(decodeSegment).join("/")); } /** The HTML to show for a page. Fragments are the site's OWN files: never pass visitor-supplied text through here. */ export function fragmentFor(page: Page): string { return prepareFragment(readContentFile(contentRoot(), page.path), page.path, page.site); } Save as apps/languages/app/[...path]/page.tsx: import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { Fragment } from "../../components/Fragment"; import { fragmentFor, pageFor, sitePages, urlSegments } from "../../lib/site-content"; // every page of the site is made when the site is built; any other address is a 404 export const dynamicParams = false; export function generateStaticParams() { return [...sitePages().values()].map((page) => ({ path: urlSegments(page) })); } export async function generateMetadata({ params }: { params: Promise<{ path: string[] }> }): Promise { const page = pageFor((await params).path); return page ? { title: `${page.title} | Philip's Learning Notes`, description: page.summary || undefined } : {}; } export default async function ContentPage({ params }: { params: Promise<{ path: string[] }> }) { const page = pageFor((await params).path); if (!page) notFound(); const html = fragmentFor(page); return (
{/* a page with its own

shows it; every other page gets one made from its title */} {/

{page.title}

}
); } Save as apps/languages/components/Fragment.tsx: import { RunScripts } from "./RunScripts"; /** * Show an HTML fragment from a content file. * * dangerouslySetInnerHTML is safe HERE only because the fragment is one of the site's own files, read at build time. * It must never be given text a visitor typed. */ export function Fragment({ html, id, rerunScripts = true }: { html: string; id: string; rerunScripts?: boolean }) { return ( <>
{rerunScripts ? : null} ); } How it works: - generateStaticParams returns the address of every page, as a list of path segments ("hungary", "hungarian-basic-3", ...). - dynamicParams = false: an address that was not made at build time is a 404, never made on request. - The page looks up its entry, prepares its fragment from the file, and shows it. The title and description come from the banner (generateMetadata). A page with its own

shows it; every other page gets one made from its title. - LW_CONTENT_ROOT has no default: a build that quietly reads the wrong folder is worse than one that stops. - Any content file the site cannot read stops the build with the file's name. (This site: none.) - dangerouslySetInnerHTML is the React name for "put this text in as HTML". It is safe HERE only because the text is one of the site's own files, read at build time. It must never be given text a visitor typed. Add "@lw/content" to the app's dependencies and to transpilePackages in next.config.ts. Built on the real content (the languages site): LW_CONTENT_ROOT= npm run build -w @lw/languages Generating static pages using 7 workers (567/567) in 4.1s ● /culture/japan/japanese-film-and-television/japanese_film_and_television_1_1 ... [+558 more paths] real 14.6 s 567 HTML files in all: the 561 pages, plus the home page, the 404 page, the test bench of Exercise 3 and some pages Next makes for itself (I did not list those separately); 59 MB. Served with "next start" and fetched: /hungary/hungarian-basic-3/hungarian_basic_conversation_3_1 200 97,176 bytes h1 "Buying Clothes: Sizes, Fit & Returns" /germany/german-basic-1/german_basic_conversation_1_1 200 64,946 bytes h1 "Greetings & Goodbyes" /japan/hiragana-1/hiragana_1_1 200 50,689 bytes h1 "Meet Hiragana" /japan/japanese-language/reference-materials/hiragana/hiragana_%E3%81%82 200 14,068 bytes h1 "Hiragana — あ" /hungary/not-a-page 404 /lab (the test bench, in a build made without LW_LAB=1) 404 the Hungarian lesson has 38 lang="hu" marks; title "Buying Clothes: Sizes, Fit & Returns | Philip's Learning Notes"; the description is the banner's Topic line. Non-ASCII addresses work: the hiragana page for あ was made at build time and found by its percent-encoded address. (My first guess at a URL for the kanji pages, /resources/japanese/kanji/..., was a 404: those live outside the content folder.) What was NOT done: no navigation, menus, styles or theme (Chapters 5 and 6); no PDFs (Chapter 8); and "next dev" was not used. WHY THIS WORKS AS AN ANSWER --------------------------- It builds the real thing at the real size and reports counts, time and bytes, including the awkward addresses.