I've just moved my blog from Wordpress to Ghost in record time. I've stayed up late, played with settings, learnt new things, broken stuff, panicked, and laughed hysterically in relief. This is the story of my blog migration, both the senseless and sensible  parts.

The main reason for considering a new platform has been to improve security (safety from hacking), to better respect visitor privacy (no tracking at all), increase speed and flexibility (a headless CMS) and reduce complexity in favor of a compelling writing experience (no clunky editor).

What I did

I started out by creating a trial account with Ghost(Pro)—the hosted solution—to start playing with it. Within 24 hours I was sold on the simplicity and speed. My first thought, 15 days prior to launching the new site, was to spend the Christmas holidays calmly moving from one to the other. But the very next day I noticed my Wordpress-hosted account was set to renew for another year in early December. Time was suddenly of the essence, to avoid unnecessary costs. I went for it. Faster than can be considered reasonable.

For reference, if you are not aware of what a headless CMS (content management system) is, you can read this, although it's not mandatory knowledge for this story.

Headless CMS explained in 1 minute
Explore the difference between headless CMS versus decoupled CMS architecture. Learn the benefits of moving to a content infrastructure for digital content delivery.

The steps I took to prepare and migrate

  • I decided I was happy with the default theme in Ghost, called Casper, knowing that I would have to tweak any theme I chose. My reasoning this time was that support would be the absolute best for the default theme.
  • Getting the content over from my Wordpress blog was a mere question of installing Ghost's WP plugin to export the content, and then use the labs area in the admin of Ghost to upload. I actually redid this 2 times because there were a couple of settings related to tags I'd forgotten to do before exporting (Ghost has only tags, not categories – another thing I appreciate). Note, however, that I still have some concerns about images (see the "How it's going" section).
  • The first theme tweak I did was remove the dark mode setting (if a Mac is set to dark mode the website can recognize this and itself turn dark). I wanted more control. The other stuff was mostly header colors. I've left fonts as they are for now, but I'm going to have to make the contrast better in some areas.
  • The first "complex" tweak I did was add support for multi-lingual content. Ghost uses a templating framework known as Handlebars.js that I had never used before. Not gonna lie, learning something new was part of the thrill. And Ghost has some great tutorials, like this one on multi-language content. What took me weeks to figure out, set up and tweak on Wordpress actually took me less than 30 minutes understand and set up on Ghost.
  • To make multi-language work, all Swedish posts have a hidden tag: '#svenska'. (I made sure this converted in the correct format during export from Wordpress, on my third export attempt 😅). All tags using a hash are hidden in Ghost. This means I can access this tag to code exceptions and filters into the theme. I essentially used the same technique previously in Wordpress, but Ghost feels more "ready" for it. Note for power users: you also need to ensure the slug for hidden tags reads like this example: "hash-svenska" for #svenska. I did need to e-mail support to get that right.
  • The new blog has a slug "/en/" for displaying all English posts and a slug "/se/" for displaying all Swedish posts. The front page mixes content; the main page is in English but the language is also set inline for each article that is in Swedish. This means screen readers should hear each snippet of content in the correct language even on pages that have mixed-language content. Need to test that.

Note: When I say slug I'm not talking about the slimy mollusc but about the bit at the end of the web address (url). So for "https://axbom.blog/en/" the slug is "/en/".

  • Another thing I did was add a Javascript that allows visitors to click images in posts to bring up the bigger, original image. I always appreciate this and it's not part of Ghost core functionality.
  • I decided on Commento for the commenting system (Ghost, being a headless CMS, does not have its own). Commento is a small independent piece of software that protects user privacy, unlike many free alternatives. Commento has  a flat fee of $10 per month as long as the site is under 50,000(!) daily page views. You might wonder why I still want to keep the comments? Most of it is out of respect for the many people who have contributed to my blog through their comments over the past two decades. And in that spirit, I do believe comments is a driving force behind shared growth and learning. During the migration all historic comments became anonymized, but I decided  from a privacy perspective this turned out for the best.

Honestly I really appreciate this blurb on Commento's website, which aligns with several of the reasons I was making this move:

Commento is more than just a comments widget you can embed — it’s a return to the roots of the internet. An internet without the tracking and invasions of privacy. An internet that is simple and lightweight. An internet that is focused on interesting discussions, not ads. A better internet.

The only way to get the old comments into Commento, however, was to create a free Disqus account and first export my comments to Disqus. I then in turn exported my comments from there to get them in a format that Commento could import. Then I deleted everything on Disqus, including my Disqus account. 😅

  • RSS is usually bit tricky to get right but I wasn't that worried about the migration part for once. Ghost has a redirect JSON file for all redirects, easily edited to make sure old urls point to the right location. And setting up separate RSS feeds for Swedish and English followed the same essential structure as doing the separate web pages (or "collections"), as per this guide: Custom RSS and Ghost.
  • RSS feeds also need to be added to the head section which isn't a problem using code injection in the admin interface, but the main feed is added automatically, with a title that isn't as explanatory as I would like it to be. A small detail to iron out in the future.
  • Backing up the old site is always important. A static copy that I can open on my computer I find is best. So I installed HomeBrew to install wget to make a static backup copy of my old blog. I've also placed that content on its own (hidden) domain for easy reference when I need to fix stuff that broke during migration. You can also use something like HTTrack for this.

The search engine

Another thing that Ghost does not have is a search engine. One recommendation is often to run the free option of Site Search 360, but my blog has more than 740 posts and only 150 pages are included in the free tier. I didn't want to run a Google Custom Search—with all the unsolicited tracking that would imply—so I did the next worst thing and signed up for the Bing Web Search API. Still, it has a great index of my site and I'm hosting PHPSearch on search.axbom.com to provide a decent interface, and I control which of my domains are part of the search results. Still bits and pieces to improve that experience though.

The panic moment

So there was a moment on Wednesday night, when I pushed all the buttons to bring the domain axbom.blog over to Ghost(Pro), that made my innards tie themselves into a knot.

The way this works is that in my DNS (domain name settings) I need to use something called CNAME to point my domain to Ghost's hosted service. So I go to enter this data for axbom.blog—deleting everything related to the Wordpress install—and my DNS provider won't let me(!) set CNAME for the root domain: axbom.blog. It will only let me do this for subdomains, like www.axbom.blog.

There are basic rules of engagement for root domains, and I've been loosely aware of them—so I wasn't surprised beyond my wits, I had just been in too much of a rush. But it was a huge disappointment. And I was now doing live changes to the DNS for the domain and the question was whether I roll back (but no, I was sooo ready for this!), or submit to using the subdomain version of the domain (I really, really hate that)... or keep going.

Hell, it's 10.30pm, and I always keep moving. If I learn it can't be done I want to do it even more.

So in this feeling of heightened stress I quickly feel the need to read up on CNAME and root domains, and find this wonderful article by Dominic Fraser:

Why a domain’s root can’t be a CNAME — and other tidbits about the DNS
This post will use the above question to explore DNS, dig, A records, CNAME records, and ALIAS/ANAME records from a beginner’s perspective. So let’s getstarted. First, some definitions * Domain Name System (DNS): the overall system for converting a human memorable domain name (example.com) to…

The best part of the article is where he mentions a couple of hosts that use techniques to allow a "virtual" CNAME on the root domain. One of those is Cloudflare. And I know Cloudflare is already part of the Ghost(Pro) universe. I think to myself "that can't be bad", and in the next four minutes I create an account with Cloudflare and move the nameservers for my domain over to them. I set up the DNS on Cloudflare the way it's supposed to be and prepare to wait. Because having the domain name's new nameservers  propagated across the entire Internet can take up to 24 hours. It's around 10:45 pm now and I'm thinking I should call it a night.

Of course, I can't.

Meanwhile, there is a spinner on my Ghost admin screen that is searching for the correct setup in my DNS. It's saying that DNS isn't set up correctly. I'm staring at it. So is it set up correctly but just not available yet? I keep switching tabs to ensure I've added all the correct data. I go brush my teeth. This is stupid, I think to myself. What am I waiting for? This will take forever.

Boom. An e-mail comes in from Cloudflare. "Status Active", it says.

"Status Active" screenshot

I switch tab to look at the spinner again. The spinner is not there. Just the field with the root domain name. Wait, what? I switch to another browser where the website isn't cached (Brave, if you must know). I type in "axbom.blog". Wow, it's... it's...

This scene, where Dr. Frankenstein shouts "It's alive! It's alive", is truly representative of the feeling of launching a new website. From the film Frankenstein (1931).

The time is 11.11 pm and my brand new site is online. Maybe not all over the world yet, but for me. Right here. Right now. I got the settings right. All the different pipes sending information back and forth to various locations across the world once again were matched up. It is truly wondrous.

Different colored water pipes in a mesh of different curves and directions.
I always imagine the Internet as a set of pipes where valves have to be turned on and off to make information flow in the right direction. There are so many suppliers involved it's mind-boggling that it even works.

After decompressing by writing some more content for the About page I went to bed at 2am and slept like a baby.

How it's going

The site is not as private as I'd hoped by default. As outlined in my Privacy statement, there are two cookies set by Cloudflare that I wasn't aware of ahead of time because I hadn't done my research, and was moving too fast. At the same time, those cookies help make the site as blazing fast as it is, and don't contain any personally identifiable information. Still pondering how to best manage this.

The About and Privacy pages are things that I'm working on constantly to get right. The about page is a more personal explanation of the site, including my approach to privacy and accessibility. The Privacy page is designed to give a quick overview of what services are involved when using the site.

However, the two things stressing me out the most right now are:

  1. The export from the site hosted on wordpress.com didn't include images. I don't know what's going to happen to images on older posts when that account dies mid-december. The problem is that images are hosted on a separate domain by Wordpress and no export or scraping function is managing to grab those alongside the site content. I'll get there, but I'm not feeling comfortable yet.
  2. All the damn YouTube embeds that track people without consideration. I need them gone. There is no search and replace, and anyway: I want to replace videos with a clear notice of why they are not embedded, and also a link to the video for people who do want to click and watch on YouTube. Some videos I'll have the possibility of moving to Vimeo as well. Doing all this manually for two decades of posts is going to take a lot of hours...

What's next

  • I may need to ask for help on how to best backup images from the old blog and make them easy to retrieve for each corresponding post on this new blog where they need to be embedded. Currently they are pointing to urls that will stop working in less than two weeks. My life really is a mission impossible film at times. It's just not as visually vivid.
  • Each information page (about, contribute, et al) will need corresponding Swedish translations.
  • With regards to accessibility I'll review fonts and color contrasts. Lots of images are still missing alt attributes as well.
  • But perhaps most important: Write more posts and articles! Honestly I need to kick myself from time to time to remember my incentive for all this in the first place.

My recommendations

Thinking of making a similar move? Well I hope you have learned my lesson. "Done" is not in my vocabulary.

  • Be sure you know what you are getting yourself into. If you're migrating a large site you'll want a clear checklist. And a level of tech saviness that makes you flexible enough to improvise when the list isn't true to the randomness of reality. Or, you know, friends to help you with that.
  • If you're starting fresh with a new blog with no previous content that's a whole other ball game and should obviously be much less stressful.
  • It's okay – and sensible – to have a plan to roll back in case of trouble. Or be prepared to work late into the night like me, playing the fool I can sometimes elect to be in these scenarios.
  • Have fun with it and see it as an opportunity to learn and grow. Or just to do something different from rigid corporate constraints and policies.
  • Don't do it in 14 days while also working a day job at 100%. Apparently it's doable but it won't do mind and body any favors. But then again, maybe it's exactly what I needed  to keep my mind off all that other stuff... these solo retrospectives are hard!

Remember: you'll always want an exit strategy, or "what's next"-strategy. Who knows what the next shiny thing will be? If you're thinking about the future and sustainability, I am confident that a headless CMS will have you better prepared.

So I wish you best of luck in your adventures pursuing this illusive digital future. May your endeavours be filled with curious confusion, exaggerated emotions and cinematic outbursts of joy. As web publishing should be.