My Portfolio
Personal portfolio built with Next.js 16, Tailwind v4, and MDX case studies.
Overview
This site is a personal portfolio built to be fast, readable, and easy to maintain. Case studies are written in MDX and rendered server-side. The design is deliberately minimal — no animations, no hero images, no scroll-jacked sections.

Goals
The site needed to do one thing well: present work clearly. Secondary goals were maintainability (adding a new case study should take minutes, not hours) and performance (no client JavaScript for content that doesn't need it).
Technical Decisions
Next.js 16 App Router
Pages are React Server Components by default. Content routes hit the filesystem and render server-side — no API round-trips, no loading states for static content. Typed routes (typedRoutes: true) catch broken links at compile time.
@next/mdx with dynamic imports
Each case study is an .mdx file under src/content/. The slug page loads the right file via a typed content map:
const contentMap: Record<string, ContentLoader> = {
crm: () => import('@/content/crm.mdx'),
lams: () => import('@/content/lams.mdx'),
// ...
}
dynamicParams = false means unknown slugs 404 at the edge rather than at runtime.
Custom MDX components
Rather than pulling in @tailwindcss/typography, prose elements are mapped to custom components in src/mdx-components.tsx. This keeps the bundle smaller and makes it straightforward to adjust any element's styling without fighting utility overrides.
Tailwind v4
A single @import "tailwindcss" in globals.css. No tailwind.config.js. Configuration moves to CSS @theme blocks when needed, which keeps tooling simpler.
Radix Dialog for mobile nav
The mobile navigation drawer uses Radix UI's Dialog primitive, which handles focus trapping, scroll locking, and ARIA attributes. The visual implementation is a right-sliding panel with an overlay — two CSS transitions, no animation library.

What Was Left Out
- No blog (not actively written)
- No skills progress bars
- No separate about page
- No testimonials
- No analytics until there's traffic worth measuring
Retrospective
The MDX setup with @next/mdx and dynamic imports worked cleanly. The one friction point was pnpm's symlinked node_modules causing a Turbopack panic — fixed with node-linker=hoisted in .npmrc.
Writing the case studies was more valuable than the code. The process of articulating why decisions were made surfaces things that aren't visible in a codebase.