Rebuilding JohanPersson.nu from WordPress to Markdown

Categories: tech

NOTE: This post was drafted by my agent JarMes, who also helped perform the migration, and then edited by me.

I have been running JohanPersson.nu on WordPress for a very long time (even longer… before it was hosted on DasBlog). It has survived different hosting providers, design changes, plugins, media uploads, podcast embeds, conference notes, product tests, and a lot of small technical notes that I mostly wrote so I would not have to solve the same problem twice and so that others in the #community can benefit.

The migration ended up preserving more than 1,400 posts and pages, so this was never only about publishing a new home page. The archive was the product.

That is also why I did not want a migration that only looked good on the home page. The important part was preserving the old blog as a useful archive: the old URLs, the old posts, the categories, the media, the RSS feed and enough of the old visual identity that it still felt like the same place. At the same time, I wanted the future writing workflow to be simpler. My notes already live in Markdown, and I use Obsidian every day. So the goal became:

  • keep the old public blog working
  • move the source content to Markdown
  • make the site static and easy to deploy
  • keep writing from Obsidian possible
  • avoid adding a CMS before the content model was stable

Why move away from WordPress?

This migration did not start as a framework experiment. It started from a more boring and practical problem: the existing hosting environment was running out of storage, and the blog had accumulated many years of uploads, generated files, cache files and WordPress history. WordPress is still a great publishing platform, but for this blog I wanted fewer moving parts:

  • no database required to serve old posts
  • no PHP/runtime dependency for normal page views
  • easier version control of content
  • easier local validation before publishing
  • a writing workflow closer to my daily note-taking workflow

The archive matters more than the admin UI.

The direction: Astro, Markdown and GitHub Pages

The proof of concept used Astro with Markdown files as the source of truth. Posts are stored by date and slug, similar to the old WordPress permalink structure:

src/content/posts/YYYY/MM/DD/post-slug/index.md

Each post has frontmatter with the original permalink, date, categories and other metadata. The site then generates the old-style URLs so existing links keep working.

I started by deploying a staging site to GitHub Pages, while the final target remained the original domain: https://blog.johanpersson.nu/

Because the old WordPress site was still available during the work, the static rebuild could be tested before changing DNS. That made the cutover much less scary.

A small custom domain gotcha

One important detail was that the staging build and the production build are not quite the same. On GitHub Pages staging, the site lived under /blog.johanpersson.nu/, but after moving to the real domain the site needed to be built for the root path.

That meant using:

ASTRO_SITE=https://blog.johanpersson.nu ASTRO_BASE_PATH=/ npm run build

Otherwise the HTML would still point CSS and assets to /blog.johanpersson.nu/..., which works on staging but breaks the design on the real domain.

The custom domain also needed to be preserved as part of the Pages deployment using a CNAME file containing:

blog.johanpersson.nu

That small file is easy to forget, but it is important for GitHub Pages and HTTPS.

What had to be preserved

The migration had a few non-negotiable requirements:

  • old post URLs should continue to return 200
  • category pages should exist
  • RSS should still work
  • media paths should be preserved where possible
  • podcast posts should not flood the normal front page feed
  • product tests should be separated from the main news flow
  • search should be available without WordPress

The result is not only a static export. It is a rebuild of the blog’s publishing model.

Search without WordPress

Search is handled with Pagefind. That gives the static site a client-side search experience without needing a server-side search index or WordPress plugin.

This was one of the important checks before considering a cutover. A blog archive without search is much less useful.

Cleaning up migration artifacts

One of the interesting parts of the migration was that converting old WordPress HTML to Markdown is never perfect. Some posts had Markdown-looking links embedded inside raw HTML, for example patterns like:

[text](<a href="...">...</a>)

Those do not render as links. They render as visible artifacts.

So part of the migration work became building audit scripts that check the generated HTML, not only the source Markdown. That was important because the browser only cares about what was actually generated.

The audit now checks for things like:

  • visible Markdown-wrapped HTML links
  • possible visible Markdown links
  • nested or empty <strong> artifacts
  • missing or suspicious old media references
  • RSS and sitemap availability

Keeping the old feeling

A migration like this can easily end up looking like a generic new static site. I did not want that.

The old blog had a dark, compact WordPress theme with a clear header, side columns, a search field, profile imagery and a simple list of posts. The new Astro site now deliberately leans back toward that look instead of becoming a modern card-heavy landing page.

The current design keeps:

  • the dark background
  • the blue links
  • the compact post list
  • a left column with profile imagery
  • the MVP badge
  • a right column for podcasts and categories
  • the old JohanPersson.nu identity

It is not a pixel-perfect clone, but it feels like the same blog.

Obsidian as the next publishing workflow

The next step is authoring. Since the source content is Markdown, Obsidian can become the writing environment.

The plan is to use a post template that creates the required frontmatter:

title: "My post title"
date: "2026-07-15T00:00:00+01:00"
draft: true
permalink: "/2026/07/15/my-post-title/"
categories:
  - tech
tags: []
source: obsidian

The important part is that new posts should be easy to start, but still safe. draft: true means a post can live in the repository without appearing on the public site until it is ready.

Once everything was in place and cleaned up, we changed the URL in GitHub Pages and the DNS settings and now we are live :-)

Things still to decide

  • whether comments need to be archived
  • where new images should live, at the moment they are in the repo
  • how much automation the Obsidian publishing flow needs
  • how useful TinaCMS will be as a local editing UI on top of the Markdown-first workflow
  • how to handle federation of posts

I also started experimenting with TinaCMS as a local editing UI for the Markdown content. It is not part of the production publishing flow yet, but it may become useful for editing metadata and drafts without leaving the Markdown-first model.

Reflection

The biggest lesson so far is that a blog migration is not mainly about pages. It is about trust.

Can old links be trusted? Can the archive still be searched? Can the RSS feed still be used? Can I write the next post without fighting the tooling? Can I cut over without losing the ability to roll back?

That is why this migration has been done in small steps: export, convert, validate, design, search, audit, and only then cutover.

The end goal is simple: a blog that is easier to maintain, easier to write for, and still feels like JohanPersson.nu.