Back to projects
December 2025

Personal Portfolio V2

A minimalist, high-performance portfolio site built with Next.js 16, Motion, and Tailwind CSS v4. Featuring custom MDX content pipelines and zero-layout-shift transitions.

Next.jsReactTypeScriptTailwind CSSMotion

The Problem

My previous portfolio was functional but felt static and generic. It had become outdated and no-longer reflected my achievements or engineering capabilities. I needed a performant site that could handle complex CMS pipelines, including code blocks, math equations, and high-res photography—without sacrificing speed or accessibility.

The Goal

I set out to build a site that felt alive and modern. The specific goals were:

  • "Alive" UI: The site should feel organic, using fluid animations and lighting effects (Aurora) that react to the user, rather than static blocks of color.
  • Seamless Navigation: Navigating between pages should feel like a single-page app (SPA) with smooth exit and entry transitions, despite using the Next.js App Router.
  • Content Management System: Updating blog posts and project pages should be intuitive and straightforward, enabling me to make frequency updates as required.

The Solution

Technical Decisions

I chose Next.js 15 (App Router) as the foundation to leverage React Server Components. This allows me to render the heavy MDX blog content on the server, sending zero JavaScript for the blog text itself to the client.

For styling, I chose Tailwind CSS for iteration speed and Motion for animations. I deliberately avoided heavy 3D libraries like Three.js. While impressive, they often introduce loading spinners. Instead, I created background effects and colour animations using pure CSS gradients and generic HTML elements, ensuring the site remains interactive instantly on mobile devices.

Challenges Faced

A big and unexpected technical hurdle was Cumulative Layout Shift (CLS) within the blog posts. When using Markdown, images typically load without explicit dimensions, causing the text to jump around as images pop in. This meant my header links in the table of content would navigate to the incorrect position on the page.

Solution: I wrote a custom rehype plugin that runs at build time. It scans the filesystem for local images referenced in my markdown, calculates their exact pixel dimensions, and injects width and height attributes into the HTML. This forces the browser to reserve the exact amount of space before the image even loads.

The Result

The final product is a highly polished, interactive portfolio that reflects my attention to detail. Subtle animations make the site feel living whilst not distracting the user, or impacting performance.

The custom Table of Contents blog component uses a "scroll lock" mechanism to accurately highlight the active section without jittering, even during rapid scrolling. Representing the solution to the difficult problem I described above.

Lessons Learned

This project reinforced the importance of the "Platform vs. Library" trade-off. By implementing the Table of Contents and Image sizing logic myself (rather than npm installing a heavy plugin), I kept the bundle size small and the behavior exactly as I wanted it.

I also learned the nuances of React Server Components. Drawing the line between Server (for content) and Client (for interactivity) required careful planning, especially when efficiently passing data to the blog/project card components.

Future Improvements

  • View Counters: I plan to integrate a Redis database to track and display view counts for each blog post.

  • Guestbook: A signed guestbook allowing visitors to leave a permanent mark using GitHub OAuth.