Tim Kapitein

The Stack Behind This Blog - A Story of Procrastination

Let's start with a confession: I had a perfectly working blog setup, and I could have been writing articles. Instead, I spent a few weeks refactoring everything to use the latest tech. Classic developer move - using "but I should upgrade to the newest version first" as an excuse to tinker with code instead of writing blog posts.

The Comfort Zone: Starting with Remix

I started with Remix, a framework I'd used extensively and genuinely enjoyed. My initial setup was based on Pedro Cattori's excellent remix-blog-mdx template - a clean, straightforward approach to handling MDX files as routes. Everything was working fine; content was rendering, and I could have just kept writing. As I'm not really expecting to write a lot of articles, I would be fine with just keeping this setup.

And then React Router 7 came out. React Router is actually built by the same team behind Remix, and version 7 represents a major milestone - it's essentially Remix v3, as the teams decided to merge the projects together. Both Remix and React Router 7 use Vite as their build tool, which provides incredibly fast development experience. While Remix handled MDX files as routes out of the box, I discovered through this GitHub issue that React Router 7 deliberately left out this capability. In the same issue, I noticed Pedro Cattori mentioned he was working on a framework-agnostic way to handle MDX content, similar to how Astro does it. It sounded promising, but I didn't want to wait. I needed my excuse to procrastinate now.

Enter MDX and MDX-bundler

Before we dive deeper, let's talk about why I'm using MDX in the first place. MDX is markdown with superpowers - it lets you import and use React components right in your markdown files. This is perfect for a technical blog where I want to show interactive examples alongside my explanations. Instead of just describing a component, I can render it right there in the post.

With React Router's route-based MDX support gone, I needed a new solution. This led me to MDX-bundler, a tool created by Kent C. Dodds. It handles all the complex parts of transforming MDX into something React can render.

The silver lining? My routing setup actually became much simpler. Instead of treating each MDX file as its own route, I now have a single dynamic article route. Adding a new blog post is as simple as creating an MDX file in my articles folder - no route configuration needed. Funny enough, this limitation pushed me toward a much nicer setup.

The Deployment Journey

MDX-bundler is great, but it comes with some deployment constraints. As Kent C. Dodds explains in the documentation, it won't work in environments like Cloudflare Workers. The reasons are technical but straightforward: Workers can't run binaries (which mdx-bundler needs for esbuild), and they can't run eval (needed for processing the bundled code).

This meant I needed to find a different deployment solution. I first tried Fly.io - their developer experience is fantastic, and deployment was a breeze. However, since my blog isn't exactly going to break any traffic records, the cold starts became a bit of an issue. Sure, there are probably ways to solve this, but remember - I was looking for excuses to keep rebuilding things, not actual solutions.

While exploring alternatives, a friend suggested I just use a long-running server. I was hesitant - I'd always used services like Netlify, Vercel, Fly, and Cloudflare Pages. Running my own server felt like unknown territory. But with some help from Claude AI, setting up everything turned out to be surprisingly straightforward. Turns out I was overcomplicating things - a simple server setup was all I needed.

The Current Architecture

With the deployment puzzle solved, I could focus on architecting where my data lives. My idea was simple: use Github as my CMS to store the MDX files for my articles. This way, I wouldn't need to run new deployments every time I write or update a post. Just commit and push.

However, while Github is great for storing content, it's not exactly optimized for serving it quickly. This is where Redis comes in. I set up Redis on my server and leveraged the excellent @epic-web/cachified package to handle caching. Cachified is a powerful tool that provides cache management with features like time-to-live (TTL), stale-while-revalidate (SWR), and type-safety.

Here's how it works:

  • When a request for an article comes in, a cachified function first checks if it has a cached response
  • If it finds one, it returns it immediately
  • In the background, it checks if the cache needs to be revalidated
  • If there's no cached version, it fetches the article from Github and adds it to the cache

I've also built in a lever to manually invalidate the cache when needed.

This setup gives me the best of both worlds: the simplicity of using Github as a CMS, with the performance benefits of Redis caching.

Local Development

For local development, I wanted to keep things simple. Using import.meta.env.DEV, I check if I'm in development mode. If I am, I skip the whole Github/Redis setup and use local variants of the article and article list services that work directly with the MDX files on my machine. This way I can write and preview articles instantly, without any network calls to Github or Redis. This makes the development experience fast and straightforward.

The Road Ahead

Of course, there are still some things to iron out. I'm currently wrestling with component imports in my MDX files - they're not working quite right yet. But that's next on my list to fix.

I'm also planning to add some analytics. My blog might be small, but I'm curious about whether anyone actually reads these posts. I want to own my data though, so Google Analytics is out of the question. I've been looking at Umami.is as a privacy-friendly alternative. I'll probably write about setting that up once I get to it.

A Final Twist

Just when I got everything working, React 19 was released. I thought "here we go again, another technical rabbit hole to dive into." But surprisingly, it was just a matter of installing the update and everything worked perfectly.

Wrapping Up

So there you have it - what started as me finding excuses not to write turned into a complete rebuild of my blog. Was any of this necessary? Not really. Did I learn some interesting stuff along the way? Definitely. Now I guess I'm out of excuses to not write those articles...