I’ve been running this blog on a self-hosted ghost platform ever since the very first post. Over time, ghost served me well - and this is not a decision against ghost, but for a simplified site setup.
I’ve been eyeing at hugo for some time now. As it builds a static webpage, there are tons of hosting options (free and paid). To me, this provides the benefit of being able to decomission the server I used to run the blog on.
In this blogpost, I’ll outline the steps I took to migrate the existing content to the new blog.
To get started, I used ghostToHugo - a fork of the original ghostToHugo project, to convert all posts in Ghost to Hugo templates.
This is as easy as creating an export from the ghost admin panel (
Settings -> labs -> "Export your content") and downloading the resulting
We then run
ghostToHugo -p ~/mysite export.json - which creates a hugo project for us.
Next, I took an existing simple theme and configured this for my new hugo project
git clone https://github.com/hugo-sid/hugo-blog-awesome.git themes/hugo-blog-awesome
we then add this theme to our
hugo.toml file as
theme = "hugo-blog-awesome".
At this point, we already have an initial page up and running, which we can start with
Update links and assets
It’s worth pointing out that links between posts won’t work at this point, nor will images render correctly.
Inspecting one of the posts, it becomes immediately clear why - ghost’s export added a
__GHOST_URL__ in front of all links.
We can fix this pretty easily by removing all occurances of
__GHOST_URL__/ (for the above case, this leaves
Images have a similar behavior - though slightly more complicated, as ghost stores them as follows:
As I like to keep the post and corresponding image together, I used the following approach (shown on the example of a fictionary post
- Create a directory next to the
- Move the file into the directory.
- Move corresponding image files into the directory (i happened to have the original files still in a backup repository).
- Replace image sources.
mkdir content/post/hello-world/ mv content/post/hello-world.md content/post/hello-world/index.md mv ../imagesource/hello-world.jpg content/post/hello-world/
We can now replace the link to be a relative link:
We obviously have to repeat this for every blogpost with images. The resulting structure is a mix of directories (posts with images) and standalone posts (posts without images).
Update Post header
Out of the conversion, all posts had a header as follows:
+++ date = 2023-08-03T11:30:00Z description = "" draft = false slug = "hello-world" title = "hello world" summary = "Hello world blog post" tags = ["personal"] +++
While this is a good first step - it’s not ideal. The description was empty in all cases - however many posts had a summary.
A quick look at the Lighthouse benchmark revealed that the posts were missing a “description” field (your experience may vary depending on the selected theme).
As a quick fix, I’ve simply copied the content of “summary” to “description” - as that seemed to be fitting in most instances.
+++ date = 2023-08-03T11:30:00Z description = "Hello world blog post" draft = false slug = "hello-world" title = "hello world" summary = "Hello world blog post" tags = ["personal"] +++
My existing ghost blog was using url’s as follows:
https://blog.xmatthias.com/hello-world/. Hugo by default inserts a
/post in the middle, so the url becomes
That’s not a problem, but requires some attention, which means we have to
- Add redirects - ideally as 301 (hugo-style redirects will hurt SEO).
- Migrate url’s on a comment platform (in my case, disqus).
As i’m going to deploy my hugo page on cloudflare pages, I can use the redirects feature - which is as simple as a
_redirects file in the static directory (
I have the following rules.
/rss/ /post/index.xml 200 /rss /rss/ 301 # Redirect from old post addresses /hello-world /post/hello-world/ 301 /hello-world/ /post/hello-world/ 301 # ... (continued for all posts)
The first 2 lines are there to ensure the RSS url remains the same, available under
All posts will receive a permanent redirect (301) - which will allow SEO to continue to work, as the new page is treated identically to the prior page.
Disqus / comment system
For disqus, there’s a few “migration tools”. Right after putting the new page live, i verified that all redirects work - and then tried to use the “Redirect Crawler”. Unfortunately, it didn’t yield the expected results - which led me to the URL mapper.
This takes a plain csv file with
from,to format - so for our sample post, it looked as follows:
This format is very similar to the one used in
_redirects - and required only some changes.
Saving and uploading the file gave me a confirmation box (so i could check my redirects).
After confirming that this was working, the comments started to imediately pop up on the new page.
In total (including learning hugo, and the most timeconsuming task, picking a theme), this took me about 3-4 hours over the course of a few days.
This was sufficient for me to put the blog out into the wild - though this wasn’t all - and after changing the blog engine, I did some further updates (Theme adjustments, adding security headers). This will however be a topic for another blog post.
comments powered by Disqus