My Vite Portfolio Is Dead. Long Live Next.js.
I was knee-deep in getting Oplinque out into the world when I decided to rebuild my portfolio. Somewhere between debugging the app and thinking about how to explain it, I gave myself another project. Great timing, Kevin.
The old site started as a Vite app. It worked and it loaded quickly. But I kept imagining a blog, project stories, and a slightly unreasonable canvas full of draggable nodes. I thought the migration might take a weekend. Two weeks later I was still tweaking CSS, with more late-night sessions and coffee than I’d planned.

The point where “just a portfolio” stopped being useful
A project card can tell you what I built. It does a worse job of explaining why I made a decision, what I considered, or what I would do differently. I wanted dedicated pages for work and a place to write longer notes without sending visitors to another site.
I also wanted someone arriving from search to land directly on a useful page. Google can render JavaScript, so the old claim that a client-rendered site is invisible to Google is too simple. Still, clear routes, descriptive page content, and metadata give me more control over what each page says and how it is shared. That mattered more as the site grew.
I wanted writing inside the site
The blog is made of MDX pages. I can write mostly in Markdown, then use a component when the idea needs a little structure. A callout can separate a caveat from the main story. A code block can show the exact command. A comparison can put two tradeoffs next to each other.
The folder structure matters because I want to tell a fuller story than a card and a link can hold. It should make adding or changing an article feel ordinary, even when that article needs more than plain text.
The first MDX callout I rendered felt like a little win. I could put a real component inside a post without turning the whole article into a custom page. It is a small thing, but that is exactly the sort of detail that made the rebuild feel worth the trouble.
The writing routes use Nextra for MDX. The portfolio and Spatial routes have their own components. Shared navigation stays shared; route-specific pieces live close to the route that owns them. That structure is less exciting than a new animation, but it makes editing the site much less mysterious.
The part where everything broke
My original plan was roughly: create the new app, move the components, wire up the routes, ship. That plan did not survive contact with the code. Dependencies needed attention, styles shifted, and my instincts from older Next.js projects kept reaching for files that do not belong in the App Router.
The hydration errors were the most frustrating part. I was tempted to put suppressHydrationWarning around whatever complained and move on. That would have hidden the symptom without answering why the server and browser had produced different markup. I had to slow down and decide which pieces belonged on the server, which needed a client boundary, and which values should wait until the browser mounted.
// Tempting, but not a substitute for finding the mismatch:
<div suppressHydrationWarning>{children}</div>That snippet is a reminder of the temptation, not a migration recipe. Once I treated each mismatch as a specific rendering problem, the App Router started making more sense. The page stopped feeling like a pile of moved components and started feeling like a site with clear ownership.
Search needed its own answer
There are two different search jobs here. The Writing page filters its small list of articles immediately. The sitewide command palette searches a generated content index so it can reach projects, experience, services, and articles from one place. The generated index matters because I do not want to maintain a second handwritten description of every page.
That is also a good example of why I kept the interface quiet. Search is something a visitor may open repeatedly. It should appear on the keyboard shortcut, accept a query, and get out of the way. A dramatic entrance would make the site feel slower every time I used it.
And then there is Spatial
The normal portfolio is linear: read the introduction, scan the work, visit a project. That is the route I expect most people to use. The Spatial view is a different lens on the same material. It uses React Flow to arrange profile, experience, project, and skill nodes on a canvas you can explore.
Is an interactive map the fastest way to evaluate a candidate? Probably not for every visitor. I kept it because it shows a side of how I think about interfaces and information. The ordinary pages still have direct links and readable content, so the experiment does not have to carry the whole site.
What I would watch if I did it again
First, I would map the content before changing the framework. Which pages need their own URL? Which data is shared? Which interactions really need client-side code? Answering those questions early is more useful than starting with a fresh project template and hoping the architecture sorts itself out.
Second, I would keep checking the rendered output. A nice component tree in my editor does not guarantee a readable article, a working keyboard flow, or a useful search snippet. I want to see the page as someone else sees it, at phone width and desktop width, in both themes.
Finally, I would document the messy bits while they happen. I remember the late nights, the hydration errors, and the CSS that took longer than the route migration. I wish I had kept a tighter log of the exact fixes. I can tell the story honestly without turning those memories into a made-up performance benchmark. The portfolio repository is there for anyone who wants to inspect the current implementation.
The move from Vite to Next.js gave this portfolio room to grow. The interesting part is what I do with that room: clearer project stories, writing worth reading, and an interactive view that earns its place.
Sources & references
2 sources- Portfolio source
Current routes and implementation.
- Google Search SEO starter guide
Guidance on useful content, page titles, and discovery.