After spending years in Wordpress, I decided to move my blog to somewhere else. I found Ghost and their ghost.org managed instances and wanted to see how difficult and feasible a migration could be. Here you have the process.

Shopping list

When performing the migration you need to surpass some technical challenges:

  1. The Ghost Wordpress Ghost plug-in exported content only exports database information. And is only compatible with Ghost 1! (Managed Ghost is Ghost 2.0)
  2. Exported Wordpress articles points to Wordpress (images, URLs...)
  3. Probably, you had your blog positioned in search engines. However, Ghost permalinks are different from Wordpress. Your SEO is completely broken!

Exporting your information from Wordpress

The first thing you need is to export (almost all) your information from Wordpress. I used the Wordpress Ghost plug-in, which generates a Ghost 1.x compatible JSON file. The installation and export process is quite straightforward.

The next step is to export all your "media" content from Wordpress. I used  Wordpress Export Media Library plug-in. Install the plugin, export all your media content and put all your images in a folder called "content". ZIP the "content" folder

Is very important to move all your export media to a folder called "content"

Easy, isn't it? Well... this is just the beginning

JSON data changes before you import it into Ghost

After several trial/errors I found that the data exported from Wordpress requires some tweats to properly work with Ghost.

Images are still pointing to Wordpress!

Yes... exported data from Wordpress uses absolute URIs. And those URIs points to your old Wordpress blog. You can fix this by using Ghost redirects, but I'd rather prefered to fix this in the data itself. To that end, you need to manually replace it, one by one. Introducing Visual Studio Code and its powerful search&replace feature.

Find next an example of what you can find in your JSON file:

Browser time:\\n\\n[![](https://www.sesispla.net/wp-content/images/2017/07/nginx-kubegen-running.png)](https://www.sesispla.net/wp-content/images/2017/07/nginx-kubegen-running-1024.png)\\n\\nEasy. Isn’t it? 🙂\\n\\n\\n# Work in progress. Contribute!\\n\\n![Travis CI Build]
``

Given this, use Visual Studio Code Find & Replace feature to replace your FQDN "https://www.sesispla.net/wp-content/" to "/content/"

I am doing a huge assumption here: The URI is absolute. I tried relative URIs but this is not going to work if, like me, you are keen on using advanced routing in Ghost. You may want this to support, for instance, multiple languages.

One more thing I found with images, is the fact that Wordpress includes some sort of "sizing" information in the filename. Information that Ghost do not understand and is leading you to a broken image:

/content/images/2017/07/nginx-kubegen-running-1024.png

To fix this, you need to remove all "-1024" or whatever it is (it changes from picture to picture). I used regular expressions in Visual Studio Search&Replace. I stronlgy encourage you to use regex101.com to cook your regular expression to fix image names:

Looks good! Go to Visual Studio Code, paste your regex, click on the "*" icon on the right and put "$1.$3" in the "Replace" field.

$1 stands for "Group 1" and $3 fo "Group 3" in the previous screenshot. Don't forget to include the "." between $1 and $3 to produce a valid filename!

The regex I produced is the following. Please check and adjust to your needs, because this may change depending on your Wordpress version:

([A-Za-z-\/0-9]*)-([0-9]{4}).(png|jpg|jpeg)

Things you can't fix

Unfortunately, there are things you can't fix "automatically". For instance, if you used code blocks in your posts, as I did, they will be turned into plain text. You need to import them to ghost and then add the appropiate markdown (```) to them, one by one.

Also, information imported from Wordpress is automatically converted into a massive "Markdown" block in Ghost. You will miss most of the editing experience for older posts.

Importing to Ghost

Well, you have a tweaked JSON file and a content.zip file with all your media files exported from Wordpress. You are ready to import to Ghost! But... this JSON file is only compatible with Ghost 1.x :(

Ghost v1 exported JSON files are compatible with Ghost v2.So, I ended up using Docker to host an on-premise Ghost 1, import the JSON and the export it again.

Once you have Docker Desktop installed in your computer, you can easily create two docker instances to migrate your content and test your blog before you jump to Ghost.org:

$ docker run -d --name ghost1 -p 3001:2368 ghost:1
$ docker run -d --name ghost2 -p 3002:2368 ghost:2

When both containers are up and running, you simply need to surf to http://localhost:3001 (Ghost v1) and http://localhost:3002 (Ghost v2)

Complete the registration process in both instances and then head to ghost admin page located at http://localhost:3001/ghost and http://localhost:3002/ghost

Generating a Ghost v2 valid JSON

This is, in fact, quite easy having both Docker images up and running. Just head to http://localhost:3001/ghost, click on "labs" and use the "import context" to import your exiting JSON file and then "export content" to get a Ghost v2 valid JSON file:

Use import content and export content to get Ghost v2 valid JSON file

You won't need ghost v1 anymore, so you can remove your Docker image:

$ docker stop ghost1 && docker rm ghost1

Importing your content to Ghost 2

Time to see your blog up and running in Ghost for first time! If you are using a Docker instance as stated before, head to http://localhost:3002/ghost, click on "Labs" and use the same "Import content" feature you used in Ghost 1:

  1. Content.zip
  2. blog.json (the file with all your wordpress content, whatever its name is)
Ghost comes with some demo content. Generally, is a good idea to use the "Delete all content" feature in "Labs" before importing yours.

How it looks like? Really nice!

Ghost running my Wordpress imported content

Setting up your ghost.org account and migrating all the content

Creating a Ghost managed instance by ghost.org is quite straightforward.

The only tricky (and optional) part maybe is the custom domain which implies:

  1. Have a domain name and access to the DNS server
  2. Create a CNAME that points to your ghost.io domain
  3. Complete the (really intuitive) ghost.org, three step, wizard:
Setting up your custom domain with ghost.org is really easy!
SSL certificate is autogenerated and included in ghost.org price!

From here, you just need to login to your ghost admin page, click on "Labs" and follow the very same Content import process we've performed with the on-prem version.

Ready to go!

Fixing the SEO

You blog is up and runing. You can see your old stuff, create new one... BUT, If you head to Google and filter by website (e.g. site:sesispla.net) and start cliking in the results, soon you realise that none of it works, showing Ghost's 404 generic page...

Don't panic. At Ghost "Labs" you can find a "redirects" JSON file that can help you fixing the problem. I cooked the following JSON file that fitted my needs and decision:

[
    {
        "from": "\/blog\/" ,
        "to":  "/",
        "permanent": true
    },
    {
        "from": "\/blog\/[0-9][0-9][0-9][0-9]\/[0-9][0-9]\/([A-Za-z0-9-_]*)\/" ,
        "to":  "/es/$1/",
        "permanent": true
    },    
    {
        "from": "\/blog\/language\/(en|es)\/[0-9][0-9][0-9][0-9]\/[0-9][0-9]\/([A-Za-z0-9-_]*)\/" ,
        "to":  "/$1/$2/",
        "permanent": true
    },
    {
        "from": "\/blog\/(language\/)?((en|es)\/)?[0-9][0-9][0-9][0-9]\/[0-9][0-9]\/([0-9][0-9]\/)?" ,
        "to":  "/",
        "permanent": true
    },    
    {
        "from": "\/blog\/((language\/en|es)\/)?(tag|category)\/([A-Za-z0-9-_]*)\/\/" ,
        "to":  "/tag/archive/",
        "permanent": true
    },
    {
        "from": "\/blog(\/language\/)?(en|es\/)?\/author\/([A-Za-z0-9-_]*)\/",
        "to":  "/author/$3/",
        "permanent": true
    }    
]

Needs/suppositions supporting this file:

  1. My old blog was hosted under "/blog". Now stands under "/"
  2. I used to have english and spanish posts using a Wordpress multilanguage plugin.
  3. All my old tags and categories are now stored under the tag "archive". Hence, all old tags have been removed, all articles retagged and existing SEO entries should be redirected there. If it is not your case, you can easily tweak the regex redirect rules and point to Wordpress imported tags in Ghost!

Definetly, evaluate your SEO impact and use http://www.regex101.com to adjust all the redirect rules to your needs.

What's left

You probably had Google Analytics, Google Adsense, code highlight...

With Wordpress this was archieved with "plugins", but this is not the case in Ghost. However, all this is possible with Code injection.  I definetly find Ghost approach much simple and clean. Just head to "code inection" configuration and paste:

Header:

!-- Code highlight -->
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/themes/prism-twilight.min.css">
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-numbers/prism-line-numbers.min.css">
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-60677359-1"></script>
<script>
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());

    gtag('config', 'UA-60677359-1');
</script>

<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script>
     (adsbygoogle = window.adsbygoogle || []).push({
          google_ad_client: "ca-pub-9259074226518950",
          enable_page_level_ads: true
     });
</script>

Footer:

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/prism.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-csharp.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-aspnet.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-javascript.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-css.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-bash.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-typescript.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.15.0/components/prism-powershell.min.js"></script>

This includes:

  • My old Google analytics
  • My old Google adsense
  • A brand new code highlight using prismjs. I used highlightjs in my old Wordpress, but I did not managed to get it run with Ghost, and prismjs do the job...

The result

This blog! Just keep navigating. This is the result using the default theme (casper, as of writing) with few more tweaks.

More things you need to put in your TODO list

  • Customize Ghost (Your picture, blog header picture...)
  • Add a comment solution e.g. Disqus (no comments OOB with Ghost)
  • Choose/Customize a theme (limited availability for Ghost)
  • Have a look to your old posts and, maybe, adjust the tags and add code blocks
  • Did your Wordpress supported more than one language? Check this out: https://docs.ghost.org/tutorials/multi-language-content/
  • Keep blogging!