Skip to content

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.

Terminal window
npx [email protected] jonathandobson-com \
--ts --no-tailwind --eslint --app --src-dir --import-alias "@/*" \
--react-compiler --cache-components --use-npm --agents-md --empty

The 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 },
};

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):

app/[season]/layout.tsx
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.

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">.

app/[season]/page.tsx
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.

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").

  • Per-season favicon and link-preview image via icon.tsx and opengraph-image.tsx (ImageResponse) in app/[season]/. They take the [season] param, so each needs its own generateStaticParams or it renders on demand.
  • sitemap.ts and robots.ts list only /.
  • Two 404s. dynamicParams doesn’t exist under Cache Components, so /foo reaches the [season] layout. Calling notFound() in the layout fell back to Next’s default page. The internationalization guide’s pattern works: the layout stays renderable, the page calls notFound(), and app/[season]/not-found.tsx renders inside the layout. Deeper unmatched paths (/a/b) use app/global-not-found.tsx.
  • Typed routes and rewrites. / exists only as a proxy rewrite, so typed routes reject <Link href="/">; the documented escape is href={"/" as Route}.
  • scroll-behavior: smooth needs data-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 children and only toggles classes on it.