4 min read

Some notes on migrating to Ghost using Jekyll

Welcome to my newly-migrated blogfolio site! This is the 3rd time I've migrated Ændra.com, or like the 12th iteration of my personal site if you include the versions that were on aendrew.com (I'm not too precious about my most recent deadname, the way I used to spell it was one of my earliest expression of queer identity. I'll be paying for that domain until the heat death of the universe though, alas).

The last iteration of my blog was on Sanity.io, and migrating it to Ghost was, uh, fraught with peril. I figured it would be worth documenting my process a bit in case it helps anyone.

But Ændra! You said "jekyll" in the title of this post!

...I'm getting to that.

Sanity is the single worst tool I've ever had the misfortune of choosing to blog with. There, I've said it. The fact I'm saying this after eleven other iterations, each with a different CMS, speaks volumes. I honestly cannot in good faith recommend it to anyone. It's probably better for something like an e-commerce site if you have like $20,000 in dev labour to throw at it, I don't know, but it is unbelievably bad for blogging. If you look at my post history, I've not written much in the last decade full stop, but I could only stomach writing like a total of two posts with it in the three or so years it ran on my domain.

What's even worse is that Sanity is nigh-on impossible to do simple dumps out of, given its god awful block-based content storage system. I tried and failed for a solid hour. Fortunately in this case, I wrote sweet FA in Sanity, so I still had the vast majority of my content in Markdown files from my previous (also failed) Gatsby site, I just had to convert my singular published Sanity post to Markdown and I was good to go. Unfortunately, Ghost doesn't have a straight-up Markdown importer. That, of course, would have been far too simple. Instead, they have a collection of semi-maintained developer-level tools you can use to generate the dumps to import into Ghost.

An "Import unsuccessful" email from Ghost. Expect to get a few of these...

Fortunately, one of those semi-maintained tools is a Jekyll importer (Sidenote, I will inevitably misspell it as "Jerkyll" once in this post because I've been doing it all day; rest assured this is not intentional), which, given my Markdown already has Front-Matter metadata, made this a lot easier.

Except for the fact the migrator tool is practically undocumented and extremely unhelpful when it fails. Which it inevitably will.

Here's the minimum you need: all of your posts in a single directory named _posts. Your posts should have an author field in the front-matter (this should be the first part of your email address; so, if I wanted the author email to be howdy@aendra.com, I'd put author: howdy) and the permalink needs to be in a field named basename. The basename value should not contain slashes and not include your domain name; slashes get replaced with - (edit: I initially prefixed my basenames with slashes and had to wipe then re-import everything after writing this because all of my slugs started with dashes after import, breaking all of my old URLs. I do not recommend this experience, I write as I try to recover this post from before said-experience). Note that the property is named basename and not permalink like it is literally everywhere else and indeed in the Jekyll docs.

Put all of this in a zip file:

$ zip -r posts.zip _posts

Then pass that to the migrator. You'll need to npm install the Jekyll plugin too (this assumes your directory doesn't have a package.json file already; omit the first line if it does):

$ npm init -y
$ npm install @tryghost/mg-jekyll-export --save
$ npx @tryghost/migrate jekyll --pathToZip ./posts.zip \
--email aendra.com --scrape none --url https://aendra.com

That last part is on two lines (omit the backslash if you want to write it as one line) because it contains the three flags you need to make this work:

  • --email This bit (and an @ symbol) gets appended to the end of your author name. You'll probably get Example Author as the author of all the posts if you don't set it.
  • --scape This influences whether the script tries to scrape all the images and whatever else on your existing site. Maybe your site has a lot of images or videos and you need to do this, I don't know. Mine is mostly text. I had no issues telling it not to scrape files, but it would start spewing errors if I didn't include it, not sure why.
  • --url This is the key bit. I couldn't figure out why things always failed, but it was because I wasn't including this.

It's worth noting that for a quality import I had to provide all three of these flags, but their significance isn't documented anywhere. I was literally looking through the code of the migrator to figure out how everything worked.

This will generate a zip file you can upload using the "Universal Import" button in the Ghost admin panel under Settings > Advanced > Import/Export (Shortcut to that is yoursite.ghost.io/ghost/#/settings/migration). Don't worry too much if you mess it up. This button very much exists for a reason (I had to hit it like 3 — edit, 4 — times before I was successful):

The Ghost "Danger Zone: Permanently delete all posts and tags from the database, a hard reset" button
This button is my best friend today and today alone.

Note also that the importer will tag every piece of content with #jekyll. Do your readers care about what format you imported your old blog from? I write nerdy AF shit and I'm pretty sure my readers don't even care. Fortunately, you can rename the tag by going to Tags > Internal Tags (That's yoursite.ghost.io/ghost/#/tags?type=internal in case you can't find it) and editing the tag in question. I changed it to #archive. It also creates a tag denoting the day the content was imported, which might be useful if you have to import a lot of content across several domains, but not for me with my sub-100 post blog.


Wow, 1000 words. I've already hit publishing parity with my Sanity site only an hour after binning it. Good riddance. Viva Ghost!

-æ.