Just Ship
After years of not having a proper personal site, I finally decided it was time to change that.
The real motivation came when I saw Happy Hues by Mackenzie Child on X (Twitter). I instantly fell in love with the colors and the typography so I decided to try palette 15.
I'm not a designer by any means, but I wanted to try building something from scratch, including the design and typography. I have to say, it was a lot of fun!
I've always wanted to start blogging, so the focus was on making something that lets me write without friction. Andy Bell wrote a great post about how writing doesn't need to be "too serious". I'm going to try to follow this advice. I have a ton of stuff that I believe could be interesting to share, so hopefully I'll find the momentum to do so.
Once I had the design direction, I needed to figure out how to build it. I have tons of experience with WordPress, but for my personal site it's just overkill. I didn't want to deal with hosting, the headless approach, or any other complexity. I decided to use a statically generated site and deploy it to Vercel. I wanted to keep it simple.
The neat part about statically generated sites is that they're easy to deploy. I use the free tier of Vercel with a custom domain. Vercel's free tier gives you 100GB of bandwidth per month, which should be more than enough for this site. If I ever need more, I can upgrade to the paid tier later.
Stack
Rather than building from scratch, I used the Vercel Blog example as my starting point. It's a Next.js project that gave me a solid foundation with:
- MDX and Markdown support
- Optimized for SEO (sitemap, robots, JSON-LD schema)
- RSS Feed
- Dynamic OG images
- Vercel Speed Insights / Web Analytics
I forked it and customized it to fit my needs, adding my own design, contact form, and personal touches.
However, I immediately ran into a problem, blog posts weren't rendering correctly. The pages would crash with a React version mismatch error. After some investigation, I found that others had encountered the same issue and documented the solution in this GitHub issue.
The fix involved two changes:
- Transpiling
next-mdx-remote: I added it totranspilePackagesinnext.config.tsto resolve React version conflicts. - Awaiting params: In Next.js 15, dynamic route parameters need to be awaited. I updated
generateMetadataand the page component to be async and await theparamsobject.
At the time of writing this post, a PR with these fixes is ready to be merged into the Vercel examples repository.
How It Works
The entire site is statically generated at build time. Here's the flow:
Blog Posts: I write posts as MDX files in app/blog/posts/. Each file has frontmatter with metadata:
---
title: 'My Post Title'
publishedAt: '2025-01-01'
summary: 'A brief description'
---
Static Generation: Next.js reads all MDX files at build time using a simple utility function:
// app/blog/utils.ts
export function getBlogPosts() {
return getMDXData(path.join(process.cwd(), 'app', 'blog', 'posts'))
}
If you're interested in seeing the entire file, you can do so here: app/blog/utils.ts
Build Process: When I run pnpm build, Next.js generates all pages as static HTML, processes all MDX files, optimizes assets, and outputs everything to the out/ directory.
Deployment: The out/ directory contains pure static files (HTML, CSS, and JavaScript).
The Contact Form
Since this is a static site, I needed a way to handle form submissions without a backend in traditional sense. Formspree solves this perfectly.
Formspree's free tier gives me 50 submissions per month, which is more than enough for a personal blog. The form integrates seamlessly with React, and I get email notifications for each submission.
Hosting on Vercel
Here's my vercel.json configuration:
{
"buildCommand": "pnpm build",
"outputDirectory": "out",
"installCommand": "pnpm install",
"rewrites": [
{
"source": "/blog",
"destination": "/blog.html"
},
{
"source": "/blog/page/:page",
"destination": "/blog/page/:page.html"
},
{
"source": "/blog/:slug",
"destination": "/blog/:slug.html"
}
]
}
The rewrites ensure clean URLs for blog posts. When someone visits /blog/my-post, Vercel serves /blog/my-post.html behind the scenes. This also works for pagination.
The Writing Experience
The writing workflow is straightforward. I just drop a new .mdx file in app/blog/posts/, add some frontmatter metadata, write in Markdown, and push to BitBucket. Vercel handles the rest.
What's Next?
I'll try to write more about personal projects, interesting client work, and whatever else I find worth sharing. We'll see where this goes.
