Building My Website with Next.js, CloudFront and Resend

26/02/2026 • 14 min read

A full behind-the-scenes story of how I built and evolved my personal site: routing, gallery architecture, mobile UX, CI/CD headaches, and production email delivery.

I started this website thinking it would be a simple one-page profile. A few sections, a contact form, maybe a small gallery. In practice, it became a full engineering project. It needed design choices, a deploy plan, a way to serve lots of images, and a fair amount of debugging under pressure.

The Stack and First Steps

The site runs on Next.js, with the app in a src folder, my own styling, and more pages added over time. Early on, I focused on making it look consistent. That meant clear fonts, good contrast on a dark theme, a timeline for my work history, and a contact form that was simple but strict enough to block junk.

Building the Track Days Gallery

The first big feature was the Track Days section. At first it only had placeholder images. Once the real photos arrived, the needs grew fast. The site had to find gallery folders on its own, group photos by session, let people browse folder by folder, and use clear URLs instead of fragile encoded paths. That moved me from static pages to dynamic routes.

Designing the URLs was one of the most fun parts. I wanted links that people could read, not random folder names. So I used patterns like /track-days/<location>/<month>/<year>, /track-days/<location>/<year> and /track-days/css/<level>. To make that work, the code reads each folder name, turns it into a clean slug, maps month names to two-digit numbers, and can work back from a URL to the right photos.

I also had to keep old links working while the URLs kept changing. Redirects were key: an old link should not break just because the site got better. That sounds small, but it matters. Bookmarks, shared links and habits all lose trust fast when pages vanish.

Next, the gallery grew beyond a plain list of images. I added cover cards for each folder, a page per folder, and then a full-screen viewer. On desktop, it needed arrow keys and buttons to move between photos. On mobile, the hard part was closing it. Once a photo fills the screen, tapping to close is not reliable. A clear close button turned out to be a must, not a nice extra.

Moving Media to CloudFront

Speed and the size of the code repo came next. Keeping thousands of large photos in the public folder made the git history huge and every deploy slow. The right fix was to serve the photos from a CDN. I moved them to CloudFront and took them out of the app. Now a code change no longer means shipping gigabytes of photos, and updates are much simpler.

That move taught me a classic cloud lesson: the right DNS does not mean the files can be reached. The domain worked, but CloudFront returned a 403 error until the storage permissions and file paths lined up. The key was telling app bugs apart from access problems in the cloud setup. Once the URLs matched how the files were really stored, the photos loaded reliably.

Email Delivery and Build Configuration

Sending email from the contact form was its own journey. Sending through a normal mailbox over SMTP was not reliable here, so I switched to Resend, a service built for this. Adding it to a Next.js route was easy. The hard part was the hosting setup. Settings that looked correct in the console still failed when the site ran, until I made the build and run settings clear and consistent.

So I tightened the build setup in Amplify. The build now writes the needed settings in a fixed, predictable way, and checks them up front, so a missing value fails early with a clear message. In a deploy pipeline, guesswork is costly. A build should fail loudly when a setting is missing, rather than ship a broken contact form that users find first.

I also hit trouble with optional native packages, such as lightningcss, Tailwind's oxide engine and the SWC compiler, in the cloud build. Everything can work fine on your own machine, and then the cloud build fails because a Linux version of a package is missing. The fix is not exciting, but it works: install the Linux packages the build needs by name, and keep the framework settings in line so they do not clash.

Branch Discipline and Documentation

Another lesson was about branches. Once I started using git-flow, I saw how easy it is to commit to the wrong branch when moving fast. If a feature works on your machine but not in production, the problem may be which branch was released, not the code. Checking the branch is now part of every deploy checklist I use.

On the content side, I added a blog to record this very process. The value is not only in sharing the end result. It is in writing down the choices. Why I moved photos to a CDN. Why I changed the URLs. Why I dropped fragile regex checks in the browser. And why small mobile details matter more than desktop testing suggests.

What I Would Do Differently

If I sum up the project in one line: this site became a small production system, not a static portfolio. It has a URL plan, a photo setup, a hardened build and deploy pipeline, reliable outside services, and design changes driven by real feedback.

What I would do from day one next time: agree URL rules early, move heavy media out of the app straight away, add build checks for settings before the first release, and write a short guide for fixing build and deploy problems. Those four choices alone remove most of the late surprises.

The final lesson is simple: a personal website can be a serious piece of engineering if you treat it as one. It is small enough to change quickly, but real enough to test the habits that matter in larger systems. Those habits are clarity, repeatable builds, good monitoring and respect for how people actually use the site on their own devices.

More from the blog