React (Vite) to Next.js (App Router) Migration Guide
For a long time, my portfolio was my pride and joy. Built with React and Vite, it was fast, sleek, and did exactly what I needed it to do—or so I thought. But as the web evolved into 2026, I started noticing the cracks in the foundation of my Single Page Application (SPA).
I realized that while Vite is an incredible tool for development speed, a pure React SPA has inherent limitations that were holding my professional brand back. Here is the story of how—and why—I migrated the Kamran Portfolio to Next.js 16 and the App Router.
The Turning Point: Why "Standard" React Wasn't Enough
I loved the "snappiness" of React, but I couldn't ignore the technical debt of the SPA architecture. I hit three major walls:
- The SEO Ghost Town: Because my site was rendered entirely in the browser, search engines often saw a blank
<div>before the JavaScript kicked in. Even withreact-helmet-async, I was fighting an uphill battle to get my blog posts indexed correctly. - The "Loading Spinner" Fatigue: Every time a user visited a dynamic page (like a specific project or blog post), they had to wait for the JavaScript to bundle, then wait again for an API call to Supabase. This "waterfall" effect felt sluggish compared to modern SSR (Server-Side Rendering) standards.
- Bundle Bloat: I was shipping my entire routing logic, heavy animation libraries, and SEO helpers to the client, even if they were only looking at a simple "About" page.
I needed a framework that could handle the heavy lifting on the server while keeping the interactivity I loved. It was time for Next.js.
Phase 1: The Great Purge
The first step was the hardest: saying goodbye to Vite. It felt like moving out of a house I’d lived in for years. I started by stripping out the old engine.
npm uninstall vite @vitejs/plugin-react-swc vitest vite-plugin-sitemap
In its place, I brought in the Next.js core. I opted for the latest version to take advantage of the matured Turbopack engine and the refined App Router patterns.
Phase 2: Translating the Language of Environments
One of the most tedious parts of the migration was the "Find and Replace" saga. Vite and Next.js speak different dialects when it comes to environment variables.
I had to rename every instance of VITE_SUPABASE_URL to NEXT_PUBLIC_SUPABASE_URL. More importantly, I had to swap out import.meta.env for process.env. It felt like rewriting my site’s DNA, but it was necessary for Next.js to securely handle those keys between the server and the client.
I also had to teach TypeScript how to behave in this new world. By updating my tsconfig.json to include the Next.js plugin and setting jsx to preserve, I ensured the compiler wouldn't get confused by the App Router’s unique structure.
Phase 3: Architecting the App Router
This is where the magic happened. In my Vite app, I had a massive App.tsx file filled with <Route> components. In Next.js, I deleted that file entirely.
I embraced the File-Based Routing system. I created a src/app/ directory and started mapping out my world:
- My home page became
page.tsx. - My blog became a folder:
blog/page.tsx. - My dynamic posts became
blog/[slug]/page.tsx.
The Layout Revolution
Instead of wrapping every page in a navigation component manually, I used layout.tsx. This acts as a permanent shell. I moved my global CSS and my TanStack Query providers here, ensuring that my navigation stayed "sticky" and my state persisted without unnecessary re-renders.
Phase 4: The "Use Client" Bridge
Coming from a Vite/SPA background, almost all my components used useEffect or useState. In the Next.js App Router, every component is a Server Component by default.
I had a choice: rewrite everything, or bridge the gap. For my interactive elements and Framer Motion animations, I added the "use client"; directive at the top of the files. This allowed me to keep my interactive logic while still reaping the benefits of server-rendered shells for the rest of the site.
I also performed a massive "Search and Destroy" on <img> tags. I replaced them with the next/image component.
Why? Because Next.js automatically optimizes my Unsplash and Supabase images into
.webpformat on the fly. No more layout shifts, and much faster load times.
Phase 5: SEO and the Automatic Sitemap
In my old React app, I had a custom script that would run after the build to scrape my routes and generate a sitemap. It was fragile and often broke.
With Next.js, I implemented a native sitemap.ts file. This file is alive—it’s asynchronous. It talks directly to my Supabase database during the build process, loops through my blog posts, and generates a perfect XML sitemap automatically.
Coupled with the Metadata API, I finally achieved "Social Media Perfection." When I share a link now, the Open Graph images and descriptions are generated on the server. No more empty preview cards.
The Result: A Faster, Smarter Portfolio
Looking back at the migration, it wasn't just about changing a framework; it was about changing my philosophy of the web.
By moving away from the limitations of a standard React SPA, I’ve given my portfolio:
- Instant Load Times: Thanks to Server Components and Image Optimization.
- Search Engine Dominance: My blog posts are now fully readable by crawlers the millisecond they land on the page.
- Developer Joy: Next.js 16 and Turbopack make my local development feel even faster than Vite ever did.
If you’re still clinging to a Vite SPA for a content-heavy site, take the leap. The migration is a challenge, but the performance gains are worth every "use client" directive you’ll write.
Share this post