All posts
NextJSJun 5, 2026 · 3 min read

Migrating This Site to Next.js 16: Lessons Learned

What actually broke when I moved my portfolio from Next.js 13 Pages Router to Next.js 16 App Router with React 19 — and how I fixed it.

Migrating This Site to Next.js 16: Lessons Learned
On this page

I recently migrated this very site from Next.js 13 (Pages Router) to Next.js 16 with React 19, the App Router, and Turbopack. On paper it's a version bump. In practice it touched every file in the repo. Here's what actually broke and what I'd do differently.

The breaking changes that actually bit me

The official upgrade guide lists dozens of changes, but only a handful caused real work on a small static site.

next lint is gone

Next.js 16 removed the built-in next lint command entirely. You now run ESLint yourself, which also means you need a real ESLint 9 flat config:

import { defineConfig, globalIgnores } from "eslint/config";
import coreWebVitals from "eslint-config-next/core-web-vitals";
import nextTypescript from "eslint-config-next/typescript";
 
export default defineConfig([
  globalIgnores([".next/**", "node_modules/**", "next-env.d.ts"]),
  ...coreWebVitals,
  ...nextTypescript,
]);

One gotcha: ESLint 10 doesn't work with eslint-plugin-react yet. Pin ESLint to ^9 until the ecosystem catches up.

params is a Promise now

Every dynamic route segment receives params as a Promise in Next 16. Forgetting to await it is a type error under strict TypeScript:

type TagPageProps = { params: Promise<{ tag: string }> };
 
export default async function TagPage({ params }: TagPageProps) {
  const { tag } = await params;
  // ...
}

React 19 removed the global JSX namespace

Any JSX.Element annotation stops compiling. The fix is mechanical — use React.ReactNode for children and let return types be inferred — but it's spread across every component.

What surprised me

ExpectationReality
Turbopack would need configurationZero config — it's just the default now
Old libraries would mostly workReact 19 peer-deps broke several; I removed them instead
Fonts would be the easy partnext/font was easy and deleted a render-blocking request
The 404 page would be trivialIt needed a route group to escape the site chrome

The biggest mindset shift: in the App Router, deleting a dependency is often easier than upgrading it. react-scroll didn't support React 19, and replacing it was one line of native browser API:

document.getElementById("learnmore")?.scrollIntoView({
  behavior: "smooth",
  block: "start",
});

My migration order, if I did it again

  1. Switch the package manager first (I moved to pnpm) so every later step is reproducible.
  2. Upgrade Next + React + TypeScript in one commit, fix compile errors only.
  3. Move to the App Router while the site still looks identical.
  4. Only then refactor architecture and rename files.

Doing the framework upgrade and the architecture refactor in the same step is the classic mistake — when something breaks, you can't tell which change caused it.

Was it worth it?

Yes. The build is faster, the fonts are self-hosted, every route is statically prerendered, and strict TypeScript caught two real bugs that had been shipping for years. If your site is small, the migration is a weekend. Budget two.