How I built JRM Lab: from WordPress to Ghost to Astro

This article is about the decisions I took along the way when building JRM Lab. I wanted a personal site that does two things: publish articles, and present products and projects with a bit more structure than a regular blog post. And I wanted maintenance to stay out of the way. Easy to say in one sentence; in practice it took me through three different systems before I landed on one I actually liked.

The order was WordPress and Notion first, then Ghost, then Astro. None of those steps were wasted. Each one made the limits of the previous choice easier to see.

Table of contents

Open Table of contents

WordPress and Notion: familiar, but “too much”

WordPress was the obvious place to start because I had used it before. It is flexible, there is a plugin for almost everything, and you can have something live in an afternoon.

Familiarity is not the same as fit, though. For a personal site, WordPress drags in a lot of moving parts I did not need: a database, a PHP runtime, plugins to keep current, security updates, theme-and-plugin compatibility to babysit. I was not running a publication with editors and comments. I just wanted a fast site whose content I controlled directly.

I considered Notion too and ruled it out almost as quickly. It is a notes tool that publishes on the side. The public-facing layer relies on third-party wrappers, layout control is limited, and my content would live inside a proprietary database instead of as portable files I own.

There was a more selfish reason too. I wanted to learn something building this site, and reaching for WordPress again would not have taught me much.

Ghost: closer, and actually shipped

Ghost was more interesting. It is purpose-built for publishing, the editor is a pleasure to write in, and the writing experience alone is worth the comparison to WordPress. I did not stop at evaluating it: I stood up an instance and built a working version. As of writing this, it is still online at ghost.jrmlab.dev (probably not for long).

That bit matters to me. It is easy to dismiss a tool from the outside; I wanted to use Ghost long enough to know where it actually stopped fitting.

Getting Ghost to work was never the problem. Getting it to feel like my site was.

The themes were where I kept running into walls. I wanted a homepage with a short intro, one flagship piece of work, more featured work below, then recent articles. I wanted the work pages to look and behave differently from the article pages — structured metadata, a case-study format, that kind of thing. Ghost themes are Handlebars templates with markup and data quite tightly coupled, so as soon as my changes got past cosmetic, I was spending more time arguing with the theme than designing the site.

The other reason was simpler. Ghost runs a Node.js server. For a personal site with modest traffic, that is a server I have to keep running, patch, and worry about. What I really wanted was static HTML on a CDN and nothing else to maintain between posts.

If I had been setting this up for a publishing team with editors and a content calendar, I would probably still recommend Ghost. As a one-person site with a specific visual idea, it kept fighting me.

Discovering Astro: content in markdown

When I started reading about Astro, the project felt lighter almost immediately.

The thing that grabbed me first was that it generates static HTML by default. No JavaScript ships to the browser unless I explicitly ask for it. For a content site that is exactly the right default — pages load instantly, hosting is a flat file server, nothing to maintain at runtime.

The other thing was the content model. Pages and posts are Markdown files with YAML frontmatter. That sounds restrictive on paper and turned out to be the opposite. Markdown is portable, lives happily in git, and pairs unusually well with an AI assistant: the AI works on components and configuration, I work on the Markdown. No CMS UI to click through, no database state to wrangle, no WYSIWYG editor quietly adding <span> tags I did not want.

For the first time, I was bending the tool to the project I had in my head, not the reverse.

The whole project is just a repository, with code and content separated cleanly. So:

  1. Versioning is whatever git already does.
  2. Deploying a new article is git push. GitHub and Coolify on my VPS handle the rest.

Choosing a starting point

Now the question was whether to start from scratch or pick a template. I looked at four options.

The Cloudflare Workers starter was the first one I poked at. It is interesting if you want edge deployment and it is functionally clean, but it is bare-bones enough that I would still be building most things by hand, which is exactly what a template is supposed to spare you.

AstroPaper is clean, minimal, and blog-focused. Lovely if you only want to write — but it only supports one content type, and I already knew I needed articles and work entries with different shapes.

AstroWind leans landing-page, with more marketing components than publishing ones.

AstroPlate had the most baked in: multi-author support, tags and categories, search, dark mode, responsive layouts, SEO handling, and a JSON-driven theme system. More than I needed out of the box, but most of it pointed in directions I expected to want anyway.

I went with AstroPlate. Trimming back a template you find overbuilt is generally faster than retrofitting one you find under-built. And really I was choosing a starting architecture more than a starting look.

Part of that architecture was the deployment story. The production build runs in three stages: install everything, produce the static pages, then wrap those pages in a tiny web server. Only the last stage becomes the running site — about 30 MB on disk, 15 MB of memory. The first two stages get thrown away once they have done their job. A new article goes live in about a minute; code-level changes take a few minutes more. The previous version keeps serving traffic until the new one is fully ready, so a deploy never causes downtime.

The first set of changes

Before I touched any code, I spent time on content structure and visual direction. HTML mockups, iterations on those mockups, then the design locked in. That up-front discipline paid back later because every implementation decision had a reference to compare against.

Content modelling was the first real call. I split the site into two collections, articles and work, instead of cramming everything into one blog. That was a product call more than a technical one. A short reflection on a working pattern and a case study about something I shipped are not the same thing, and the moment you treat them the same you start losing detail in both.

A work entry needs fields an article does not: a type (product or project), a status (concept through archived), a problem-and-outcome pair for the narrative arc, the tools involved, who I worked with, a featured flag for homepage placement. Two collections, two schemas. Anything less honest started leaking.

Then the homepage. I did not want a generic listing page. The composition I wanted was specific: short intro, one flagship work item, additional featured work, and only then the recent articles. So the homepage got reshaped rather than accepted as-is from the template.

Search was in scope from the beginning. AstroPlate ships with basic regex matching against titles, which I replaced with Fuse.js — weighted fuzzy search across titles, descriptions, tags, and body content from both collections. Each result shows a small badge so you know whether it is an article or a work entry. The index gets generated at build time into a single JSON file.

The work detail page was the other piece I cared about. Each entry uses a two-column layout: a sticky sidebar on the left with the structured metadata (problem, outcome, tools, tags, link) and the prose on the right. Five seconds of skimming the sidebar should give you the arc of the project, even if you never read the case study itself.

A handful of foundational pieces also went in during this phase, mostly because they are harder to bolt on later: proper SEO via astro-seo with canonical URLs and OG metadata, an RSS feed that combines both collections, dark mode with system detection and a persistent toggle, branded favicons, and a configuration layer that pulls site-wide values like author name, copyright, and social links into JSON instead of scattering them across templates.

Somewhere in this phase the thing stopped feeling like a modified template.

What changed after the base was working

With the foundation in place, the work shifted from structural decisions to refinement.

The homepage (which the AI had initially produced as a single big component) got broken into clearer sections, each owning its own visibility — if a section has nothing to show, it does not render at all. Hardcoded strings moved into configuration. Social sharing and author profiles were consolidated so each value had a single source of truth instead of being duplicated across files. Articles got reorganised into year-based folders so the file tree could keep scaling.

For analytics, I looked at PostHog, Plausible, and Umami. PostHog is a full product analytics suite with session recordings, funnels, feature flags, A/B testing — and a roughly 70 KB script. For a content site that mostly cares about pageviews and referrers, that is overkill in both features and weight. Plausible is privacy-first and simple, but it has no free tier. Umami fit the shape of what I needed: web analytics with a clean dashboard, no cookies, GDPR-compliant without a banner, and a 2 KB script — over thirty times smaller than PostHog. Even loaded asynchronously, that gap matters. I went with Umami Cloud and added a domain restriction so my development traffic does not get tracked.

A final pass cleaned up accessibility and contrast. The site now scores 100 on Lighthouse Accessibility, Best Practices, and SEO, and 95 on Performance (served from my development laptop).

Lighthouse scores for the JRM Lab home page
The home page: curated composition with a hero, one flagship work item and the latest articles.

Working with AI as an implementation partner

One thing I underestimated at the start was how much the choice of stack would shape how I could work with AI.

I built this site almost entirely through conversation with Claude Code. Less of a code generator and more of a collaborator I argue with. The loop went: I describe what I want, we agree on an approach (tests included), the AI writes and runs the implementation, then I do the final verification and flag whatever is off.

I caught my share — hardcoded values, alignment issues, empty states that should have been hidden, architectural shortcuts that were going to bite later. The AI caught its share too. View Transitions compatibility for the analytics script was its catch, and so was a missing filter that would have leaked index files into the RSS feed.

This kind of back-and-forth only really works if the project is readable. Astro helps there. Markdown files, config files, components, a static build — most of the system is explicit and inspectable. There is less hidden state than in a CMS, which means I can review the AI’s work without first paging in a runtime model of what is happening. I did not pick the stack for that reason, but I would now.

What I would do differently

Not much. The biggest time tax was cleaning AstroPlate down: pulling out sample content, unused components, default config. Templates save you time on the features you want and charge some of it back on the features you do not.

If I were starting again today, I would skip Ghost. Not because it is a bad tool, but because I now know what shape of tool I needed. WordPress is a broad CMS. Notion is a database that publishes on the side. Ghost is a focused publishing platform. None of those are the same thing as what I wanted, which was something closer to a personal system on top of a static repository.

Did I need to walk through three of these to be sure? Probably not. But I am a little more confident in the answer now than I would have been if I had jumped straight to Astro.

Related Posts

Building a LinkedIn-publishing agent skill in Claude Code

I write LinkedIn posts in Markdown (and yes, AI helps me write them as well). I tag people, drop in screenshots, schedule for the morning, sometimes link to an SVG diagram I made in Mermaid. The posti

read more
From the Buildathon to Match Amplified

From the Buildathon to Match Amplified

Table of contents Context: The Buildathon I'm writing about the Buildathon a bit late. Intentionally. I wanted to share what came after, not just what happened during. Back in January 2026, I

read more
Match Amplified: the agentic architecture under the hood

Match Amplified: the agentic architecture under the hood

Match Amplified: The Agentic Architecture Under the Hood This is the technical companion to the Match Amplified product case and part of a series of articles o

read more
Match Amplified: From Lovable to Claude Code

Match Amplified: From Lovable to Claude Code

From Lovable to Claude Code https://www.matchamplified.com I've used Lovable for personal projects before: a winding-down routine webapp, a budget app with receipt "OCR" and LLM-powered auto-categ

read more
Match Amplified: VPS and infrastructure

Match Amplified: VPS and infrastructure

VPS and infrastructure: learning the boring stuff that keeps it all running This post is about the boring stuff that doesn't make it into demos or screenshots. But without it, nothing runs. During

read more
Match Amplified: what's already built

Match Amplified: what's already built

What's already built in Match Amplified What does Match Amplified actually do? The platform is in beta, but it's a fully functional beta, not a landing page with a waitlist. What's live today: **

read more
Match Amplified: the roadmap

Match Amplified: the roadmap

The roadmap: where Match Amplified is heading https://www.matchamplified.com Last post was about what Match Amplified already does. This one is about where it's going. A roadmap for a solo side p

read more

Match Amplified: choosing the name

How I chose the name Match Amplified Naming a project is almost as hard as building it! 😅 During the Buildathon, we called it NextStep. It was a nice name, but with too many collisions in the jo

read more
Match Amplified: what's next

Match Amplified: what's next

What's next: an honest closer to the series This is the last post in the series. Time to be honest about where things stand. What Match Amplified is now A side project and a portfolio piece. A

read more