The whole course in one tree
Here is "Microblog", a complete mini-app that uses every unit you finished:
app/ ├── layout.js ← root shell, next/font, global CSS (U2, U8) ├── page.js ← static landing page (U7) ├── blog/ │ ├── layout.js ← blog section shell (U2) │ ├── page.js ← async list, revalidate: 60 → ISR (U4, U7) │ ├── loading.js ← instant skeleton (U4) │ ├── error.js ← failure boundary (U4) │ └── [slug]/ │ └── page.js ← await params, generateStaticParams, │ generateMetadata, notFound() (U5, U7) ├── search/ │ └── page.js ← await searchParams, dynamic (U5, U7) ├── new/ │ └── page.js ← form calling the createPost action (U6) ├── actions.js ← "use server": insert + revalidatePath (U6) ├── api/ │ └── posts/route.js ← public JSON API (U6) └── like-button.js ← "use client" leaf on post pages (U3)
Reading it folder by folder, each line traces back to a unit of this course. Any annotation that still feels foggy points at the unit worth revisiting before you build something of your own.
Two requests, traced
A reader opens /blog/hello-world. The page was prerendered, since its slug came from generateStaticParams, so the server returns finished HTML instantly, complete with real metadata tags for SEO. The only JavaScript shipped is the like-button.js island. That is the pattern name for a small interactive client component embedded in otherwise-static HTML. It hydrates and starts counting clicks while everything around it stays inert.
An author publishes from /new. The form's action is the createPost server action. The form data travels to the server, the post is inserted, and revalidatePath("/blog") invalidates the cached list so the next visit to /blog shows the new post. Meanwhile a mobile app fetches GET /api/posts from the route handler, the public door.
Every arrow in those two paragraphs is something you can now build.
Why only the like button ships JavaScript
In Microblog, the reader's browser downloads JavaScript for like-button.js but not for the post page itself.
Only client components ship JavaScript. The post page is a server component, so it contributes finished HTML and nothing else. The like button opts into the client world with "use client", so its code is bundled, sent, and hydrated in the browser where it can respond to clicks and keep a count.
This is Unit 3's split in action, and it is the shape most good Next.js pages take: a large server-rendered document with a few small interactive islands embedded in it.
routeTable: rebuilding the build output
One last piece of logic, tying Unit 2 to Unit 9. routeTable(files) reproduces the route table next build prints. It keeps only page.js files, converts each path to its URL the way routeFromPath did back in Unit 2, and returns the URLs sorted alphabetically. Notice that layout.js and route.js files must not appear, because neither one is a page.
function routeTable(files) { return files .filter((f) => f.endsWith("/page.js") || f === "app/page.js") .map((f) => { const route = f.slice(3, f.length - "/page.js".length); return route === "" ? "/" : route; }) .sort(); } const files = [ "app/page.js", "app/blog/page.js", "app/blog/[slug]/page.js", "app/api/posts/route.js", "app/about/page.js", "app/layout.js", ]; for (const r of routeTable(files)) console.log(r);
Output
/ /about /blog /blog/[slug]
Reading the pipeline
- The filter runs first, and it needs both conditions. Most pages end in
/page.js, but the homepage is the bare string"app/page.js", which has no slash beforepage.jsonce you account for theappprefix. - The mapping reuses the Unit 2 trick: slice off
"app"(3 characters) from the front and"/page.js"(8 characters) from the end, then map the empty string to"/". .sort()gives the alphabetical ordering, which is why/bloglands before/blog/[slug].app/layout.jsandapp/api/posts/route.jsare both dropped by the filter, matching the real build output where only pages appear as routes.
Classifying a new dashboard route
Suppose you extend Microblog with an author dashboard at /dashboard that greets the signed-in user via a session cookie. Following Unit 7's rule, next build marks this route dynamic.
Reading a session cookie makes the output depend on the request, so the page cannot be prerendered at build time and renders per request instead.
The rest of Microblog stays static or ISR, and that is the beauty of per-page strategies: one personalized, slower page does not drag the whole site down with it. Your landing page still serves from a CDN and your blog still serves prebuilt HTML.
Why it works out that way
- The dynamic triggers from What makes a page dynamic are cookies, headers,
searchParams, and uncached fetches. This page hits the first one. - Strategies are decided per route, not per app, so mixing all three across one project is normal and expected.
You now have the full Next.js mental model: routing from the file system, the server/client split, data fetching with caching and revalidation, mutations through actions and route handlers, per-page rendering strategies, and a git-driven deploy.