Why I Started Building My Own Portfolio (Again)
Every developer eventually reaches the point where they open their portfolio, wince at the design they thought was brilliant three years ago, and decide it's time for a complete rewrite. This was mine.
I'd been using a static site generator for a while — nothing wrong with it — but I wanted something that felt more alive. Something I could ship features to on a weekend and actually enjoy maintaining.
Why Next.js 15?
The short answer: the App Router finally clicked for me. When React 18 introduced concurrent features, and Next.js layered the server component model on top, I kept bouncing between "this is brilliant" and "wait, why is my state gone?" until something just... made sense.
A few things that sold me for this specific project:
- Server Components by default — most of my portfolio pages are read-heavy. No client bundle needed for fetching and rendering static content.
- Route groups — I have a
(portfolio)group for the public site and anadminsection with its own layout and auth middleware, all cleanly separated in the same repo. - Built-in image optimisation —
next/imagedoes the heavy lifting so I don't have to think about responsive images.
The Stack I Landed On
Here's what the project runs on:
- Next.js 15 with the App Router
- TypeScript — non-negotiable at this point
- Tailwind CSS v3 for styling, with a handful of CSS design tokens for dark/light mode
- Framer Motion 12 for scroll animations
- Supabase for the database (messages, projects, blog posts)
- Tiptap as the rich-text editor for writing posts
The Part That Surprised Me Most
Async params in Next.js 15. When they made params and searchParams return Promises, I hit a wall of type errors that took an embarrassingly long time to trace back to a single root cause. Now every dynamic page looks like this:
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ...
}It's fine once you know. But the migration guide really buries that detail.
Dark Mode Without a Headache
I was dreading the usual dark mode dance — toggling classes, fighting Tailwind's dark: variants in deeply nested components. Instead I went with CSS custom properties:
:root {
--bg-primary: #f7f5f0;
--text-primary: #111009;
--accent: #c8820a;
}
html.dark {
--bg-primary: #0a0a0a;
--text-primary: #f5f5f5;
--accent: #f0a500;
}Every component just uses var(--accent) and it works in both modes automatically. No dark:text-yellow-400 sprinkled everywhere.
The best design system is the one you'll actually maintain.
What I'd Do Differently
A few honest reflections after shipping this:
- Start with the CMS earlier. I built all the UI first and the admin panel last. I kept hard-coding content while thinking "I'll make this dynamic later." Later always arrives.
- Plan the image pipeline before you need it. Wiring up Cloudinary for upload + delivery after the fact is messier than just sorting it on day one.
- Keep the JS bundle ruthlessly small. Every third-party package that ships to the client is a tax on your visitors. I audited mine twice and removed things I thought I needed.
What's Next
The blog itself is new — this is the first post. I plan to write regularly about frontend patterns, performance, and the random rabbit holes that absorb my weekends. If that sounds interesting, check back soon.




