There would be 15 years' worth of posts to migrate.

This was something that I had mulled over for the last few years. WordPress (WP), the blogging-cum-content platform that was initially released in 2003, has gotten too unwieldy for writing down my weekly thoughts. But in 2026, the list of alternatives is pretty slim; other competing services have come and gone in the past two decades. I've kept an eye on one such survivor, Ghost, since it launched from its Kickstarter back in 2013.

And so while sitting on the couch, keeping half my attention on The Hunger Games playing at the kids' request in the background, I cloned the Ghost repo—and checked off the first of many todo items off my long-standing list of tasks to migrate off of WordPress[1].

To Migrate 800 Posts

In theory, migrating onto Ghost shouldn't be a difficult process. Its tech stack is simple: a NodeJS backend hooking up to either a SQLite or MySQL database, Handlebars templates rendering the pages, and APIs for data access. WordPress is written in PHP and typically runs on MySQL, and integrated APIs onto the core platform as well. And while the database schemas—that is, how the data is structured—differs, many concepts are either identical or translate cleanly: authors, posts, pages, featured images, categories/tags, etc. In the three decades' time that blog posts have been around, we managed to converge on a canonical structure.

Ghost offers native importers for other publishing platforms in their admin interface. But with 800+ posts to migrate—the exported XML file alone was ~13MB, not counting for associated images and attached files—it struggled to parse all the data in a single run. My fallback was to turn to an additional command-line tool provided by the core Ghost development team, and break up my imports a year at a time. It was, at least, a promising start.

Have You Met my Footnotes?

Okay, so I really like using footnotes—probably too much. According to the migration script that I ran over the entire corpus, in the 800 posts I've written since 2011, 667 of them include at least one footnote[2].

I had been using a JavaScript library called bigfoot.js, implemented via a WP plugin so that footnotes can be annotated with inline [footnote][/footnote] tags that get parsed and repositioned in the rendered HTML. This was an admittedly hacky solution; none of the editors understood this custom tag, but the [] brackets gave it the appearance of a valid HTML element that editors would try and fail to parse the text. Plus, neither the library or the plugin have been maintained in over a decade.

So I took this opportunity to translate everything over to the footnotes notation defined in Markdown's extended syntax, which is far more widely supported. A non-trivial, one-off script rewriting structured data would be the ideal vibe coding task…except that WordPress was architected and written well before the age of LLMs. Its evolution, from blogging engine to Content Management System, has ruined any semblance of abstraction and modularity.

Claude Coding PHP

Of course, I can't blame WP for not writing their code to be LLM-friendly two decades before agentic coding was invented. But its coding environment, despite the large amount of PHP available as training data as well as the standardized access of MySQL databases, gave Claude plenty of challenges:

  • Large, monolithic codebase with lots of implicit dependencies;
  • Functionality scattered on top as plugins, many lacking formal documentation;
  • Lack of testing;
  • Architecture that is inconducive to testing.

I had to babysit the AI as it tried, again and again, to parse WP's exported data and make sense of it, read files that I manually fed into the app, and try to rewrite the file to something that the Ghost importer could parse without error. By contrast, on the Ghost side, Claude was able to stretch its legs a bit and iterate independently by checking its own work.

Image Wrangling

Another hurdle I ran into was dealing with images, specifically the featured images linked by each post. Both WP and Ghost include this feature, but the folder structure differs just enough between the platforms to confuse both the official tooling and my Claude-coded scripts. To make matters worse, one of the plugins I installed years ago optimized images by automatically resizing them upon upload, so I had folders with 4 copies of the same image at different resolutions.

So as my script imported each year's worth of posts into Ghost, it found and linked to…none of the associated featured images.

Here, I have to credit my new host, Synaps Media. Their founder/customer support happened to be online just as I was trying to debug a couple dozen broken links, and he helped me not only with uploading my files in bulk, but normalizing the folder structures so they were available at the expected locations. I did have to manually delete a bunch of pictures due to corrupted data and filename issues, but they were almost all images that were generated by Midjourney, during an earlier era of AI image generation when the results were extremely crude.

Ghost and Its Writing Focus

One of the first things I noticed was the raw speed of loading and rendering items. This is not just due to hosting services; I'm running these apps locally to play around with settings and source code[3], and Ghost is consistently faster to work with.

The act of writing within Ghost isn't too far off from WP; both have block-based editors—Koenig (Ghost) and Gutenberg (WordPress)—that do the job well, integrating raw text with richer elements like videos and images and other media embeds. If anything, it's the restraint that Ghost shows that stands out: it expects third parties to integrate via APIs and webhooks, and there's one place to inject custom code if needed.

The net effect is subtle; the simplicity and singular focus of Ghost's interface and technology removes much of the management overhead that I had gotten used to with my WordPress install.

Pixel Tweaks

If I have one complaint about web design within the Ghost ecosystem, it's the monotone themes—everything is minimalist with gobs of whitespace and sans-serif typefaces. And yet, because the userbase is smaller and the selection thinner, paid Ghost themes are on average twice as expensive as their WordPress counterparts, while accounting for less overall functionality[4].

After some deliberation, I chose the theme Thoughts from Priority Vision. Beyond the serendipitous name, I like this theme for its focus on typography and writing, while deemphasizing imagery and chrome, and doing this without falling into the minimalist aesthetic. I had built something similar before based on a WordPress theme called Typology, and it's a style that stands out in a sea of sites featuring giant hero images and autoplaying videos.

Pushes to Production

After a week's worth of development, I was ready to get the entire thing online and in a production environment. This took another week, though I had to wait for DNS servers to switch to their new hosts[5].

Surprisingly, for all the developer-friendly overtures available in the Ghost project, this step of migrating from one Ghost instance to another is pretty barebones. Fundamentally, it's not too different from my prior migration from WordPress → Ghost, but made even simpler since the data formats are identical. But Ghost provides no native export functionality beyond exporting all of the content in one JSON file, and their content import is finicky with file sizes and file formats. For Ghost's own paid hosted service, they suggest that anything more complicated than a 1-to-1 data transfer requires either command line wrangling or working with their migration team.

Since I had already painstakingly migrated over all my posts and their associated images, the remaining bits were other entities that I configured locally: tags, pages, navigation, etc. Formatting a JSON file properly seemed prone to errors and annoying data cleanup, so I went the Claude Code route to build a script to move each entity over piecemeal via API. With Ghost's extensive API documentation, this task was easier than my other attempts to create one-off utility scripts, but I still had to enforce proper dry-run logic and logging—it's easy to see how non-developers can vibe code their way into trouble.

Hosting Costs

Cost wasn't a real motivation for this migration, but I did a bit of research anyway into the differences between WordPress and Ghost hosts. Both services have hosting options that start very cheap—in the $3/month range, or even cheaper if you deploy the apps yourself.

Both platforms also offer official managed hosting services: Wordpress.com and Ghost(Pro), respectively. Their rates start out comparable to low-end third-party hosting services, but mind the fine print—they're constrained in annoying ways designed to encourage upsells to the next tier. Those features are also shaped to the strengths of each platform; commerce and analytics for Wordpress, subscriptions for Ghost.

I decided on Synaps Media as an all-in-one managed solution that doesn't have any major restrictions up front, at a very good price. Granted, I won't be hitting any thresholds in compute, storage, members, analytics or emails, but there's value in not even having to think about it. Plus, their customer support is spot-on.

Settling In

As I'm writing this, most DNS servers have refreshed their caches to route to my new host, which is serving the Ghost site.

Beyond the general look-and-feel of the blog, moving to an entirely new platform after a decade on the previous one gives me an opportunity to try new things. I've already been integrating LLMs into my writing process, and looking to expand these posts in length and depth. As a part of migrating my footnotes, I'm also transforming them into sidenotes, which should make for better reading flow[6].

Given how easy Ghost makes it to convert blog posts into newsletters, that will be what I'm going to look into next—even as I painstakingly add the RSS icon to the homepage to remind folks that it's still a great way to receive regular pieces of writing. I'll only be three years behind the curve.


  1. I lay out the apps and frameworks I used in the new Colophon page. ↩︎

  2. Back then, there weren't any provisions for tagging a footnote within HTML; the closest element, <aside>, was only widely supported in browsers in 2015. ↩︎

  3. For those curious, the simplest way I've been able to run WordPress locally on MacOS is via the Local app, which sets up the modern equivalent of the LAMP (Linux + Apache + MySQL + PHP) stack. ↩︎

  4. For instance, premium WP themes often integrate with other major ecosystem vendors, providing templates for e-commerce, marketing, portfolios, etc. ↩︎

  5. The domain registerer estimated it takes "up to 72 hours" for nameservers to refresh, and while I initially thought that was conservative, it has indeed taken that long to update. ↩︎

  6. Here, I made use of an old jQuery plugin, jQuery.sidenotes, to inline convert footnotes → sidenotes. Since this plugin hasn't been maintained in a decade either, I used Claude Code to rewrite the plugin without the jQuery dependency or CoffeeScript dependencies, as SidenotesJS. ↩︎