The Next.js 16 app
The static page became a Next.js 16.4.5 App Router app. The interesting part is the seasonal theme: the site shows a holiday design from November 4 to December 31 and a standard one otherwise — and both are fully static.
Scaffold
Section titled “Scaffold” --ts --no-tailwind --eslint --app --src-dir --import-alias "@/*" \ --react-compiler --cache-components --use-npm --agents-md --emptyThe flags turn on Cache Components (Partial Prerendering and 'use cache')
and the React Compiler from the start; create-next-app 16.4 also writes
partialPrefetching: true.
next.config.ts adds:
const nextConfig: NextConfig = { cacheComponents: true, partialPrefetching: true, reactCompiler: true, typedRoutes: true, // stable in 16; `experimental.typedRoutes` is the old spelling output: "standalone", // small Docker image poweredByHeader: false, experimental: { globalNotFound: true },};Seasons as a root parameter
Section titled “Seasons as a root parameter”The root layout lives under app/[season]/. That makes season a root
parameter, readable from any Server Component with next/root-params
(new in 16.3):
import { season } from "next/root-params";
export async function generateStaticParams() { return [{ season: "standard" }, { season: "holiday" }];}
export default async function RootLayout({ children }: LayoutProps<"/[season]">) { const raw = await season(); const current = isSeason(raw) ? raw : "standard"; return <html lang="en" data-season={current}>…</html>;}Because data-season is in the prerendered HTML, there is no client script
and no flash of the wrong theme. Holiday-only pieces (snow, lights) are client
components rendered only when season is holiday.
Picking the season: proxy.ts
Section titled “Picking the season: proxy.ts”Next.js 16 renamed Middleware to Proxy. A tiny src/proxy.ts rewrites
/ to /standard or /holiday based on the date — the address bar still
says /, and both targets are prerendered, so the choice costs a string
compare:
export function proxy(request: NextRequest) { const url = request.nextUrl.clone(); url.pathname = `/${seasonFor(new Date())}`; return NextResponse.rewrite(url);}export const config = { matcher: "/" };seasonFor is a pure function (November 4 – December 31, UTC) with unit tests
for the boundaries. /standard and /holiday stay reachable as previews, and
both set <link rel="canonical" href="https://jonathandobson.com">.
Keeping it static, provably
Section titled “Keeping it static, provably”export const ensureStatic = "navigation";With Cache Components on, ensureStatic makes the build fail if anything
on the page starts rendering per request. The contact form doesn’t break it —
its Server Action runs on POST, not during render.
Data that changes: 'use cache'
Section titled “Data that changes: 'use cache'”The npm section shows live package versions without a client-side fetch:
async function latestVersions(names: string[]) { "use cache"; cacheLife("days"); cacheTag("npm-packages"); // fetch https://registry.npmjs.org/<name>/latest with a 5 s timeout, // falling back to recorded versions so a registry outage never fails a build}The footer year is cached the same way. Reading the current time during a
prerender must be cached or deferred under Cache Components, so
new Date().getUTCFullYear() lives inside a 'use cache' function with
cacheLife("days").
Metadata, icons and the 404
Section titled “Metadata, icons and the 404”- Per-season favicon and link-preview image via
icon.tsxandopengraph-image.tsx(ImageResponse) inapp/[season]/. They take the[season]param, so each needs its owngenerateStaticParamsor it renders on demand. sitemap.tsandrobots.tslist only/.- Two 404s.
dynamicParamsdoesn’t exist under Cache Components, so/fooreaches the[season]layout. CallingnotFound()in the layout fell back to Next’s default page. The internationalization guide’s pattern works: the layout stays renderable, the page callsnotFound(), andapp/[season]/not-found.tsxrenders inside the layout. Deeper unmatched paths (/a/b) useapp/global-not-found.tsx. - Typed routes and rewrites.
/exists only as a proxy rewrite, so typed routes reject<Link href="/">; the documented escape ishref={"/" as Route}.
Smaller things that bite
Section titled “Smaller things that bite”scroll-behavior: smoothneedsdata-scroll-behavior="smooth"on<html>in 16, or Next stops honouring it during navigation.- Fonts come from
next/font/google, self-hosted at build time — the page makes no requests to Google. - The React Compiler means no hand-written
useMemo/useCallback. - Client components are leaves: the 3D stack receives the server-rendered layer
list as
childrenand only toggles classes on it.