Module F-8·28 min read

Building a content site end-to-end — layouts, dynamic routes, data fetching, generateMetadata for SEO, static vs dynamic rendering decisions, and what silently breaks when you deploy outside Vercel.

F-8 — Your First Real Next.js Application

Who this is for: Developers who have completed F-1 through F-7 and want to see everything connect in a complete, working application before moving into the Practitioner phase. This is a build module — we're constructing a content site end-to-end, making real decisions along the way, and deploying it. No toy examples; a real architecture you'd actually ship.


What We're Building

A content site for a technical blog — the kind of thing you'd build for a personal site, a company engineering blog, or a documentation hub. It has:

  • A homepage with recent posts
  • A blog index page with category filtering
  • Individual blog post pages
  • An RSS feed endpoint
  • Full SEO metadata including Open Graph images
  • Static generation for published posts
  • Incremental static regeneration for the post listing

This covers the full Foundation toolkit: Server Components, file-system routing, dynamic params, data fetching, built-in components, the metadata API, a Route Handler, and deployment. After building this, you have a real reference architecture to adapt.


Project Setup

bash

The project structure we'll work toward:

text

The Data Layer

For this example we'll use a file-system based data layer — posts as Markdown files — which is a common real-world pattern for personal blogs and documentation sites.

ts
ts

Install gray-matter for Markdown frontmatter parsing:

bash

The Root Layout

The root layout sets up global font, navigation, and HTML structure:

tsx

metadataBase tells Next.js the base URL for resolving relative URLs in Open Graph images and other metadata. Set it to your production domain. In development it defaults to localhost:3000.


The Homepage

tsx

This page is statically generated — it fetches data at build time and produces a static HTML file. Because getAllPosts reads from the filesystem with no external dependencies, it runs at build time and the output never changes until you redeploy.


The Blog Index — With Category Filtering

The blog index uses the URL-as-state pattern for filtering — the Server Component reads searchParams, the Client Component manages the URL:

tsx
tsx
tsx

The blog index is a dynamic page (it reads searchParams). But getAllPosts still runs on the server and the result isn't fetched on the client.


The Post Page — Static Generation with Dynamic Metadata

tsx
tsx

Two important things here: generateStaticParams pre-renders every post at build time. generateMetadata and the page component both call getPost(slug) — but because getPost is wrapped in React's cache(), the filesystem is only read once per request despite the two calls.


The RSS Feed — A Route Handler

ts

export const revalidate = 3600 sets ISR at the Route Handler level — Next.js caches the response for one hour and regenerates in the background when it goes stale. RSS readers typically poll every hour, so a one-hour cache is both fresh enough and cheap enough.


Reading the Build Output

After npm run build, look for this kind of output:

text

Notice /blog shows (static) even though it reads searchParams. This is because searchParams itself doesn't trigger dynamic rendering at the page level in the build output — the page shell is static. The filter functionality kicks in at runtime when the URL has a query parameter. This is correct behaviour.

If you see λ on a page you expected to be static, look for dynamic function calls: accessing cookies(), headers(), or passing the request to something that reads them.


The Deployment Decision

Vercel (recommended for getting started)

bash

Vercel detects Next.js automatically, configures everything, and gives you a live URL in under a minute. ISR, streaming, and Edge functions all work out of the box. Free tier handles the load of a personal blog without issue.

Self-hosting on any Node.js host

bash

For containerised deployment, enable standalone output in next.config.ts:

ts

This generates a minimal /.next/standalone folder with just the Node.js server and its dependencies — no node_modules bloat. Build the Docker image from that:

dockerfile

ISR works the same on self-hosted — Next.js manages the regeneration internally without Vercel infrastructure.


What You've Now Built

This application uses every Foundation concept:

ConceptWhere it appears
Server Componentspage.tsx files, PostCard, lib/posts.ts
Client ComponentCategoryFilter.tsx (uses hooks + URL state)
File-system routingapp/ directory structure
Dynamic segment/blog/[slug]
generateStaticParamsPost page — pre-renders at build time
loading.tsxBlog index skeleton
not-found.tsxPost page 404
generateMetadataPost page — per-post OG tags
Route Handler/api/rss — XML response with ISR
next/fontRoot layout — Google Fonts
react cache()lib/posts.ts — deduped filesystem reads
Suspense boundaryBlog index wrapping CategoryFilter
URL-as-stateCategory filter

Where the Practitioner Phase Begins

You've completed the Foundation phase. You can build real Next.js applications. You understand the rendering spectrum, the Server/Client boundary, data fetching patterns, dynamic routing, the built-in components, Route Handlers, and deployment.

The Practitioner phase assumes this foundation and builds on it for production applications at scale. P-1 goes deep on caching — not just what the options are (you saw those in F-4) but how to architect your data access layer around Next.js's cache model to maximise performance while maintaining data freshness. P-2 covers Server Actions properly — how they're compiled, what the security implications are, and how to use useActionState and optimistic UI to build forms that feel instant.

The gap between Foundation and Practitioner is the gap between "this works" and "this is ready for production traffic." P-1 is where that gap starts to close.


What Silently Breaks Outside Vercel

The five-minute Vercel deploy is real. The problem is that "it works on Vercel" creates a false ceiling — you ship, the app works, and then six months later someone asks about self-hosting, AWS, or Docker and discovers that several features they've been relying on are Vercel-only.

Know these before you build on them.

next/image Optimisation Requires a Running Server

next/image proxies image optimisation through /_next/image?url=...&w=...&q=.... That endpoint exists on the Next.js server. If you deploy with output: 'export' (static HTML export), that server does not exist. Every <Image> in your app breaks with a 404.

What happens: The browser requests /_next/image?url=.... There's no server to respond. The image doesn't load.

Fix options:

  1. Use output: 'export' with unoptimized: true in next.config.ts — images are served as-is, no optimisation
  2. Use a third-party image CDN (Cloudinary, imgix) and configure a custom loader
  3. Don't use output: 'export' — run the Node.js server
ts

Server Actions Don't Exist in Static Exports

output: 'export' generates a folder of .html files. There is no server. Server Actions POST to /_next/action — that endpoint does not exist in a static export.

What happens: Calling a Server Action throws a network error. Forms that use Server Actions silently fail.

Fix: Move mutations to a separate API — a Route Handler on a separate server, or a completely separate backend service. Static exports cannot have server-side mutation logic.

ISR Needs a Cache Handler on Multi-Instance Deployments

On Vercel, ISR just works. On a self-hosted Node.js server or Kubernetes, ISR uses the local filesystem as a cache. If you have two instances (two pods, two EC2 instances), each has its own filesystem. revalidatePath('/products') on Pod A doesn't affect Pod B. Users get inconsistent data depending on which pod serves their request.

What happens: After a revalidation, 50% of users still see stale content (those hitting the other pod).

Fix: Set up a shared Redis cache handler so all pods share one cache. This is covered in depth in A-3 and A-14.

after() Needs Adapter Support

The after() API — which runs callbacks after the response is sent — requires the runtime to support deferred execution. Vercel supports it natively. On self-hosted Node.js, it works because the process stays alive. On some serverless platforms that terminate the function immediately after the response is sent, after() callbacks are silently dropped.

What breaks: Analytics events, audit log writes, and cache warming that you've put in after() callbacks stop firing.

Fix: Verify your platform supports waitUntil semantics before relying on after(). For Cloudflare Workers, use ctx.waitUntil(). For bare serverless, fire-and-forget is unreliable — use a proper job queue (BullMQ, Inngest).

Edge Config, request.geo, and Vercel Analytics Are Vercel-Only

Vercel Edge Config (sub-millisecond key-value store), request.geo (geo data on the request object in Middleware), and @vercel/analytics are Vercel-specific. They don't exist on self-hosted deployments.

What breaks: Any Middleware logic that reads request.geo returns undefined. Any Edge Config reads throw. Analytics events are silently dropped.

Fix alternatives:

  • request.geo → Read CF-IPCountry header (Cloudflare) or X-Vercel-IP-Country equivalent from your CDN
  • Edge Config → Redis with a read-through cache, or hardcoded config for simpler cases
  • Vercel Analytics → Plausible, PostHog, or self-hosted Umami

The Static Export Feature Graveyard

Full list of what output: 'export' removes:

FeatureWorks in export?
Server Components (read-only)✅ (rendered at build time)
Client Components
next/image optimisation❌ (use unoptimized: true)
Server Actions
Route Handlers
ISR / revalidate
Middleware
cookies() / headers()
Dynamic routes without generateStaticParams
i18n routing

If your application needs any of these, you cannot use output: 'export'. Run the Node.js server.


Knowledge Check

When inspecting the Next.js build output, what does the (Static) symbol signify for a route like /blog that uses searchParams?


What happens to next/image components when you deploy a Next.js application using output: 'export' (static HTML export) without additional configuration?


Why might Incremental Static Regeneration (ISR) lead to inconsistent data across users on a multi-instance self-hosted deployment (e.g., Kubernetes)?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.