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 with two clear functions: publish articles and present products or projects in a more structured way. And with enough simplicity so that the maintenance will be easy. That sounds straightforward in one sentence. In practice, it took me through a few different systems before I found one that matched how I wanted to work.

The path was WordPress and Notion first, then Ghost, then Astro. Each step made sense at the time. Each also made the limits of the previous choice more visible.

WordPress and Notion: familiar, but “too much”

WordPress was the first option because it was the familiar one. It is flexible, there is a plugin for almost everything and it’s easy to get something live quickly.

But familiarity is not the same as fit. For a personal site, WordPress brings a lot of moving parts I did not need: a database, a PHP runtime, plugin management, security updates, compatibility overhead. I did not need a CMS with user roles and comment moderation. I needed a fast site where I control the content directly.

Notion was on the list too, but I also ruled it out quickly: it is a notes tool first and a publisher second. Public sites rely on third-party wrappers, layout control is limited and my content would be locked inside a proprietary database instead of living as portable files I own.

There was also a more practical reason: I wanted this project to teach me something new and on a more modern stack.

Ghost: closer, and actually shipped

Ghost was more interesting. It is purpose-built for publishing, the editor is excellent and the experience of writing in it is much cleaner than in WordPress. I went past evaluating it: I set up an instance and built a working version. At the time of publishing this article, it is still online at ghost.jrmlab.dev (probably not for long).

That part matters. It is easy to dismiss a tool after only reading about it. In this case, I used Ghost enough to understand both what I liked and where it stopped fitting.

The problem was not getting Ghost to work. The problem was getting it to feel like my site.

The themes were the main issue. I wanted a homepage with a clear introduction, one flagship piece of work, other featured work below it and then recent articles. I wanted work pages to feel different from article pages, with structured metadata and a clearer case-study format. Those things are not impossible in Ghost, but Ghost themes are Handlebars templates with tight coupling between markup and data. Once the changes became structural rather than cosmetic, I was spending more time fighting the theme than shaping the site.

The second issue was simpler: Ghost runs a Node.js server. For a personal site with modest needs, that is machinery I did not need to run, update and secure. I wanted static HTML served from a CDN: fast everywhere, cheap to host, nothing to maintain between posts.

Ghost is excellent for teams that need a publishing workflow with roles and scheduling. For a solo site with a specific visual vision, it was the wrong shape.

Discovering Astro: content in markdown

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

Two things stood out immediately.

First, it generates static HTML by default. No JavaScript shipped to the browser unless you explicitly opt in. For a content site, that is the right default: pages load instantly, hosting is a flat file server and there is nothing to maintain at runtime.

Second, content lives in Markdown files with YAML frontmatter. This sounds like a constraint, but it turned out to be a feature for how I wanted to work. Markdown is portable, easily version-controlled and pairs well with an AI assistant. Content and code are cleanly separated: the AI works on components and configuration; I work on Markdown files. No CMS UI to navigate, no database state to manage, no WYSIWYG editor fighting my formatting.

Astro was the first option where the tool felt like it could adapt to the project, instead of the other way around.

An Astro project is ultimately a repository, with code and content cleanly separated. This means it was easy to:

  1. Keep versioning via git.
  2. Deploy changes (e.g., new articles) just by committing and pushing. GitHub and Coolify on my VPS handle the deployment automatically.

Choosing a starting point

Once Astro was the direction, the next question was whether to start from scratch or begin with a template. I looked at several options:

  • Cloudflare Workers starter: the entry point I initially explored. Interesting for edge deployment and functionally clean, but too bare-bones. I would be building most things from scratch, which defeated the point of starting with a template.
  • AstroPaper: clean, minimal, blog-focused. Good for a pure writing site, but it only supports one content type. I already knew I needed articles and work entries with different schemas, different layouts and different logic around what gets featured.
  • AstroWind: landing-page oriented, heavier on marketing components than on publishing.
  • AstroPlate: the most fully-featured option. Multi-author support, tag and category taxonomies, search, dark mode, responsive layouts, SEO handling and a theme system driven by JSON configuration. More than I needed out of the box, but it already had things I expected to want: a search modal, a clean component architecture and a stronger base to customize.

I went with AstroPlate. Trimming a template that has too much is faster than building missing features from scratch. I was not just choosing a look; I was choosing a starting architecture.

The first set of changes

Before changing any code, I spent time on content structure and visual direction. I built HTML mockups, iterated on them and locked the design before touching the codebase. That discipline paid back later, because every implementation question had a reference to compare against.

The first important decision was content modeling. I separated the site into two collections (articles and work) instead of forcing everything into a single blog. That was a product decision more than a technical one: articles and case studies are not the same thing. They answer different questions and need different structure.

A work entry needs fields an article does not: a type (product or project), a status (concept through archived), a problem/outcome pair for the narrative arc, tools used, team context and a featured flag that controls homepage placement. Keeping them in separate collections with their own schemas made the data model honest from the start.

The homepage was the second structural decision. I did not want a generic listing page. I wanted a curated composition: a short introduction, one flagship work item, additional featured work and then recent articles. That meant reshaping the homepage into something specific rather than accepting the template default.

Search was part of the plan from the beginning. AstroPlate shipped with basic regex matching on titles. I replaced it with Fuse.js, a weighted fuzzy search across titles, descriptions, tags and body content from both collections. Results show a badge indicating whether each match is an article or a work entry. The search index is generated at build time into a single JSON file.

I also wanted a work detail page that told a complete story at a glance. Each work entry gets a two-column layout: a sticky sidebar on the left with structured metadata (including the problem, outcome, tools, tags, link) and the prose content on the right. Someone scanning the sidebar for five seconds should understand the full arc without reading the case study.

Several other foundational pieces went in during this first phase because they tend to become harder to add later: solid SEO handling via astro-seo with proper canonical URLs and OG metadata, RSS that combines both collections, dark mode with system detection and persistent toggle, branded favicons and a configuration layer so site-wide values like author name, copyright and social links live in JSON files instead of being scattered across templates.

This first phase moved the project from “modified template” to “site with its own content model.”

What changed after the base was working

Once the site was functional, the work shifted from structural decisions to refinement.

The newly designed homepage (implemented by the AI initially as a monolith) was broken into clear components so each section owned its own functionality and visibility (if there is no content for a section, it does not render at all). Site-wide values were moved into configuration so the codebase did not depend on hardcoded strings. Social sharing and author profiles were consolidated so each had a single source of truth instead of duplicated values across files. Articles were organized into year-based folders so the file structure could scale more cleanly over time.

For analytics, I evaluated PostHog, Plausible and Umami. PostHog is a full product analytics suite (session recordings, funnels, feature flags, A/B testing) with a generous free tier but a ~70KB script. For a content site that mostly needs pageviews and referrer data, that is overkill in both features and weight. Plausible is privacy-first and simple, but has no free tier. Umami fit the scope: web analytics with a clean dashboard, zero cookies, GDPR-compliant without a banner and a 2KB script, over 30 times smaller than PostHog! Even when loaded asynchronously, that matters. I went with Umami Cloud, implemented with a domain restriction so development traffic is not tracked.

Accessibility and contrast were improved in a final pass. The site scores 100 on Lighthouse Accessibility, Best Practices and SEO, and 95 on Performance (when served from my local 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 did not fully appreciate at the start is how much the tooling decision would shape the way I could work with AI.

This site was built almost entirely through conversation with Claude Code. Not as a pure code generator, but as a collaborator I question, push back on and correct. The workflow is: I describe what I want, we plan the approach together (automated tests included), the AI implements, tests and verifies; then I do final verifications and tests, and point out anything that is not right.

I caught hardcoded values, alignment issues, empty states that should have been hidden and architectural shortcuts that would cause problems later. But it also works the other way around: the AI caught things I would have missed, like View Transitions compatibility for the analytics script or a missing filter that would have leaked index files into the RSS feed.

That kind of collaboration only works well when the project is readable. Astro helps there. Markdown files, config files, components and a static build make the whole system explicit and inspectable. There is less hidden state than in a CMS, which makes both implementation and review more manageable. That was not the only reason for choosing this stack, but it became a meaningful advantage once the project was underway.

What I would do differently

Not much. The biggest time cost was “cleaning up” AstroPlate, as in removing sample content, unused components and default configurations. Starting from a template saves time on features you want; you pay some of it back on features you don’t.

If I were starting today, I would skip the Ghost experiment entirely. Not because WordPress, Notion or Ghost are bad tools, but because they solve different problems. WordPress is a broad CMS. Notion is a note-taking tool built on databases. Ghost is a focused publishing platform. Astro gave me something closer to a personal system: fast, file-based, flexible and easier to shape around both writing and work.

Sometimes the simplest architecture is the right one. You just have to try a couple of complicated ones first to be sure.