I've had my fair share of issues with WordPress, so I migrated my blog, The Pegasus Daily, to (a self-hosted instance of) Ghost a few days earlier. It was a rather complicated process, so I thought it was worth an article to explain the process.

Since there are numerous existing articles on the very topic, I'm only going to outline the basic process here, with an emphasis on a few tidbits about what I did correctly or wrong.

Step 1: Export your WordPress data

To get started, you'll need to export all your posts and pages on your WordPress site to a Ghost-formatted JSON file. This can either be done with the official Ghost WordPress exporter plugin, aptly named Ghost, or with a tool called wp2ghost that converts a WordPress XML export file to the Ghost JSON file.

Ghost uses tags to organize content. Note that the WordPress plugin doesn't convert categories into tags - you'll need to use the built-in Categories and Tags Converter to convert categories to tags.

Another caveat: the exporters only support the Ghost 1 JSON format, so you'll need to spin up a Ghost 1.x install to import the data.

You'll also need to manually migrate your media files. Open the JSON file, and replace /wp-content/uploads/ with /content/images/.

Step 2: Import your data

Follow the official local install guide at https://docs.ghost.org/install/local/ to install Node.js and the Ghost CLI tool.

Then, spin up a Ghost 1 instance by running ghost install local --v1 in an empty directory. Head to http://localhost:2368/ghost to set up your interim Ghost site. Go to the Labs section of your admin area and upload your exported data by clicking on the Import button. Some warnings and errors may be displayed.

After that, upgrade the Ghost install by running ghost update. Now, you can head to the Labs settings and export the ready-to-use Ghost 2 JSON file.

If you're a Docker person, you can replace the local install with:
$ docker run -d \
	--name container-name \
	-p 2368:2368 \
	-v /path/to/ghost/blog:/var/lib/ghost/content \
And then upgrade to Ghost 2 by running:
$ docker rm -f container-name

$ docker run -d \
	--name container-name \
	-p 2368:2368 \
	-v /path/to/ghost/blog:/var/lib/ghost/content \

Step 3: Set up production instance

Set up a production-ready Ghost install by following https://ghost.org/docs/install/ubuntu/ (Ubuntu).

Set up your site as usual, and import the Ghost 2 export file.

Download your /wp-content/uploads/ folder from your WordPress install, and upload the contents to /path/to/your/ghost/install/content/images/.

Step 4: Inspect your site

Do check your posts for unexpected issues. For example, quote blocks created in Gutenberg may not display correctly. If you previously used a $\LaTeX$ plugin, you may need to embed the MathJax or KaTeX script (and make modifications to your post) to make your fancy math text display correctly.

You may also need to setup redirects so that your old URLs remain functional. Ghost handles redirects with the redirects.json file, which can be accessed in the Labs settings of your admin dashboard. You can read more about it here.

The permalink format of my blog was /yyyy/mm/slug, and the default link format in Ghost is /slug. Instead of editing the redirects.json file, I simply added a few rewrite rules in my nginx site config file:

rewrite "^/([0-9]{4})/([0-9]{2})/(.*)" /$3 permanent;
rewrite "^/page/(.*)" / permanent;
rewrite "^/category/(.*)" / permanent;

The snippet above removes the date from the old post links, and redirects WordPress specific pages to /.

Enjoy your new Ghost site!