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.

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
| Expectation | Reality |
|---|---|
| Turbopack would need configuration | Zero config — it's just the default now |
| Old libraries would mostly work | React 19 peer-deps broke several; I removed them instead |
| Fonts would be the easy part | next/font was easy and deleted a render-blocking request |
| The 404 page would be trivial | It 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
- Switch the package manager first (I moved to pnpm) so every later step is reproducible.
- Upgrade Next + React + TypeScript in one commit, fix compile errors only.
- Move to the App Router while the site still looks identical.
- 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.


