Building a Developer Portfolio with Next.js and Tailwind
Most developer portfolios are over-engineered or underwhelming. I wanted something in between — fast, well-designed, and easy to maintain. This site is the result. Here's how I approached the key decisions.
Why Next.js Over a Static Generator
I considered Hugo, Astro, and even a plain HTML site. Next.js won for a few reasons:
React component model. I wanted interactive elements — animated cards, filter tabs on the projects page, a contact form — without sprinkling vanilla JavaScript into templates. React's component model makes this natural.
MDX for blog posts. Writing in Markdown but embedding custom React components when needed is the sweet spot. I can drop in a code block with syntax highlighting, a callout component, or an interactive demo without switching context.
App Router. Server components for pages that don't need client JavaScript (blog posts, the about page), client components where interactivity is needed (project filters, the contact form). The boundary is explicit and the output is fast.
The Motion System
Every page has subtle fade-in animations as content enters the viewport. Rather than reach for a heavy animation library, I built three small components on top of Framer Motion:
<FadeIn delay={0.1} direction="up">
<h2>Section Title</h2>
</FadeIn>
<StaggerChildren>
<StaggerItem>Card 1</StaggerItem>
<StaggerItem>Card 2</StaggerItem>
</StaggerChildren>
FadeIn uses IntersectionObserver to trigger when elements scroll into view, with a Safari fallback that manually checks getBoundingClientRect after a short delay. Safari mobile occasionally fails to fire the initial observer callback during SPA navigations — a bug I only caught through device testing.
StaggerChildren and StaggerItem use Framer Motion's variant propagation to cascade animations through a list with a 100ms delay between items. The result is a subtle wave effect that makes lists feel intentional rather than static.
Design Decisions
Dark mode first. I designed in dark mode and adapted for light. Most developer portfolios look better dark, and it forced me to think about contrast and hierarchy before reaching for colour.
Minimal colour. The palette is almost entirely neutral — greys, whites, and a single blue accent used sparingly. Colour is reserved for interactive states (hover, focus) and status indicators. This keeps the focus on content.
Consistent spacing. Every page uses the same container pattern — max-w-7xl with responsive padding. Section separators use Separator components with consistent vertical margins. It's boring, but boring spacing is invisible, and that's the point.
The Page Header Component
One pattern I'm pleased with is the shared PageHeader component. Every interior page (About, Blog, Projects, Contact) uses it:
<PageHeader
title="About Me"
subtitle="A bit about who I am and what I do."
>
<ProfileImage />
</PageHeader>
It renders a full-bleed background section with consistent padding and typography. The optional children prop lets pages like About add a profile image alongside the title. This kind of small abstraction keeps the codebase consistent without adding complexity.
Content Management
Blog posts live as .mdx files in a content/blog/ directory. At build time, gray-matter parses the frontmatter, reading-time calculates the reading time, and the posts are sorted by date. No CMS, no database, no build-time API calls. The content is co-located with the code and version-controlled in Git.
For syntax highlighting, I use Shiki with GitHub's light and dark themes. The highlighting runs at build time via rehype-shiki, so there's no client-side JavaScript for code blocks. The HTML arrives pre-highlighted and switches themes via CSS custom properties.
What I'd Do Differently
Start with the blog layout. I built the homepage and projects page first, then retrofitted the blog. Starting with the content-heavy pages would have established typography and spacing patterns earlier.
Design the dark and light themes simultaneously. Adapting dark-first designs to light mode occasionally produced washed-out results. Designing both in parallel would have caught those issues earlier.
The site is intentionally simple. No CMS, no analytics dashboard, no complex build pipeline. It's a Next.js app with Tailwind, deployed to Vercel, and it builds in under 10 seconds. That simplicity is a feature.