Migrating from WordPress to Ghost

Documenting the process and decisions behind moving from self-hosted WordPress to cloud-hosted Ghost. Also comparing Ghost and Eleventy.

Migrating from WordPress to Ghost
Photo by Dina Badamshina / Unsplash

I've been struggling with my blogging solution using WordPress on a self-hosted EC2 instance in AWS. I get crawled constantly by AI bots, commented on by spammers, and forced to run through Ubuntu dist-upgrades to keep my database and PHP versions up to date.

Should I switch to a static site generator?
Thinking about the switch between WordPress and the Eleventy static site generator after 23 years of blogging.

I've been debating the static site generator approach with something like Eleventy, but that was more work than I wanted. I love to tinker, but I think I'd rather be writing than maintaining, mostly. When Veronica Explains posted about Ghost, that grabbed my attention. It seemed like it had everything I needed to host a site, manage my newsletter subscribers, and some bonus fediverse integration. An integration that I may or may not use.

I spun up a local instance on my laptop to check it out, and it seemed pretty slick. I had over 300 WordPress posts imported locally with no hassle at all. It even dealt with all my featured images that gave me so much trouble when trying Eleventy.

I started a free trial at ghost.org and with a little bit of exporting, I had my nearly complete WordPress site transferred over in just a few steps.

Eleventy Decision

Eleventy looked like a nice option and the base blog imported all of my WordPress content OK. It wasn't very nice looking, or, was just pretty basic. I would have had to add niceties myself.

Eleventy Pros

I've been thinking of moving to a static site generator like [[Migrating from WordPress to Eleventy]], and there is a draft post languishing that I've never finished because it's been too technically complex for my time available. Eleventy SHOULD have the benefits of:

  • Speed
  • Customization possibilities are endless
  • Hosting on some free tier static site repo
  • Publish pipeline could be automated through git from Obsidian somehow
  • Security - since I'm not managing a running instance anymore

Eleventy Cons

However, with great power comes great responsibility.

  • Customization is complex
    • Have to build CSS and program thumbnails for posts myself
    • Cover images were NOT displayed at the top of each post, so I'd have to use the template language to reliably locate where the thumbnail for each post resides and insert that into the post and the collection browser. I almost had this working after a few hours, but it didn't LOOK nice. Also, since I coded it myself, I wasn't sure how reliable it would be. First time ever using JavaScript. YOLO I guess?
    • I could post my failed code snippets for doing this, but I'm sort of embarrassed that I couldn't make it work in less than a day.
  • No comments without something like Disqus
  • No subscription or email
  • Have to maintain a customized publishing pipeline and troubleshoot it if it breaks
    • This is less of a con than you would think - BUT - sometimes you just want to post something and not troubleshoot a pipeline job in the cloud.
  • The private posts have to be handled manually somehow.

Ghost Decision

Ghost looked like it had everything I wanted and just worked immediately. I had both a local instance running beautifully on my laptop, and the free trial within just an hour or two.

Ghost Pros

Ghost on the other hands has pros of:

  • Speed and auto-scale that they manage
  • Publishing is simple
  • Customization is possible but has simple defaults that are easy to use
    • No need to crack open CSS or templates if I don't want to
    • My cover images immediately worked and looked great on all the posts that have them. No custom templating needed.
  • No maintenance
  • Security - since they're hosting it and have it behind some WAF
  • Comments AND ActivityPub
  • Email newsletter!!!
  • Private posts are converted to drafts.

Ghost Cons

And the Ghost cons are:

  • I would rely on Ghost hosting for $15 per month that could go up
    • But, I COULD self-host since it's open source
    • Except I'm really done self-hosting Internet facing services
  • Have to relinquish my ec2 instance so I can't use it for OTHER things,
    • But honestly this is fine, could always spin one up on demand
    • I don’t know why this is a con. This is why I wanted to run this project in the first place, removing that long-running instance. I guess I just have a sentimental attachment to this VM I’ve been shepherding through the years.
  • Have to find another hosting source for my newsletter mp3s (S3?)
    • This is probably better anyway - and was really fast to setup
  • Have to figure out dated slugs
    • This was easy right in the Ghost docs with a redirect.yaml
    • Redirects let me do some other cool link shortening too.
  • No image hosting for my gallery, which I'm not doing anyway and could have at another sub-domain that's hosted at my home behind a login.
    • Sharing my whole camera roll with the Internet is so 2005.
  • No easy comment migration
    • Sad about this one, but I have all the comments in an XML file and database backup. I could integrate them somehow if I really chose to do so.

Ghost Migration Process

Here's a rough overview of the steps needed to test this out on my laptop.

  • Export the blog content from WordPress to XML
  • Make sure the WordPress site stays online
  • For local laptop migration, use the Ghost migrate wp-xml tool
    • It fetches all the images and blog content
    • It creates a zip file with all the JSON and assets needed
  • Load this zip file into the Ghost migrate tool universal migrator, and it pretty quickly creates all the posts
  • Get the site looking and working how you want, then set it all up the same way on ghost.org.

With the 14-day trial, this was even easier. I just uploaded the WordPress XML file to my ghost.org site management console and it did the rest. All the blog posts, tags, and images were imported from my running site.

Then I took my validated redirects.yaml file from my local dev site and uploaded it to the prod site. That gave me the confidence to know all my redirects would work properly, and they DID, the first time without any troubleshooting in prod.

Ghost Migration Task List

The following is a dump of my notes on the migration. I hope this is useful to someone moving from WordPress to Ghost.

You can see my raw thought process in bullet form.

Redirect the dated post slugs to the root/slug

301:
    ^\/[0-9]{4}\/[0-9]{2}\/(.*): /$1

Can I have this AFTER that? Does it need to be on a separate line? It stays in the 301 section. This redirects anything at blog/root like the pgp-keys page.

301:
    ^\/blog\/(\S+): /$1

It seems like this would be the way - oh - but what about /blog/ to / or /blog to /.

Tested the following and it works for what I need. The ghost tutorial doesn’t tell you, but place redirects.yaml in the following location for testing this out on your local site. I dug into that and the documentation does tell you where to place this file.

content/data/redirects.yaml

301:
    ^\/blog\/[0-9]{4}\/[0-9]{2}\/(.*): /$1
    ^\/blog\/wp-content\/uploads\/[0-9]{4}\/[0-9]{2}\/(\S+): https://bbbblog.s3.us-east-1.amazonaws.com/$1
    ^\/blog\/(\S+): /$1
    ^\/blog: /
    ^\/s3\/(\S+): https://bbbblog.s3.us-east-1.amazonaws.com/$1

Notice how I also got a cool redirect there too in the last line. I can pop files into s3 in a bucket I own and just write out

https://bbbburns.com/s3/<file-name>

Much shorter than the bucket URL.

  • [x] Figure out slug overlap #blog ✅ 2026-02-12
    • There isn't any slug overlap between all the dated urls and I verified this with the following. This means I can flatten all post to the root which is the ghost default. Default means less work!!
    • cat bbbblog.WordPress.2026-02-08.xml| grep "<link>" | grep -E '[0-9]{4}/[0-9]{2}' | sed -E 's|.*\/([a-zA-Z0-9-]+)/<\/link>|\1|' | sort > wp-slugs.txt
    • uniq -d wp-slugs.txt
    • Thanks AI for coming up with a sed expression quickly
  • [x] Figure out mp3 hosting #blog ✅ 2026-02-14
    • Setup redirects for those mp3s in the redirect file, shown above.
    • Yes. This works, like: ^\/cnn: https://cnn.com
    • Tracking that in [[Newsletter Links]] and [[S3 Bucket for File Hosting]]
  • [x] Put pgp key files at root of theme or fix mail key discovery? #blog ✅ 2026-02-15
    • Use public servers or skip this and give up on PGP.
    • Key base identity or keyoxide?
    • Or delete all keybase? Nah, don't NEED to.
      • Setup keybase on iPad and used keybase DNS TXT record to verify my domain.
    • Use my own wkd or setup keys.openpgp.org
      • Used the CNAME openpgpkey.bbbburns.com
      • Checked and WKD works:
╰─➤  gpg --locate-keys --auto-key-locate clear,nodefault,wkd burns@bbbburns.com 
gpg: key 4B33CDC4903940DE: "Jason Burns <burns@bbbburns.com>" not changed
gpg: Total number processed: 1
gpg:              unchanged: 1
pub   rsa3744 2015-05-02 [SC] [expires: 2028-05-11]
      44D9CD4EB5DE7C2567B6969C4B33CDC4903940DE
uid           [ultimate] Jason Burns <burns@bbbburns.com>
sub   rsa2048 2015-05-02 [S] [expires: 2028-05-11]
sub   rsa2048 2015-05-02 [E] [expires: 2028-05-11]
sub   rsa2048 2015-05-02 [A] [expires: 2028-05-11]