Converting my Wordpress blog to Hugo

As many others, I have happily been running my blog using Wordpress. I have now converted it to Hugo instead, and this blog post will tell you how I chose to do it, and give you links to further resources.

Some of my friends (you know who you are 😉) recommended me to use Hugo when I wanted to create the blog. But because it was very easy to get up and running, I chose to go for Wordpress back then.

For different reasons, the time has now come for me to finally switch my blog over to Hugo. If you’re thinking about using Hugo for you blog or website, I hope this post can help you get going a bit faster, since you’ll then have my collection of initial choices and experiences to help you get going.

My initial Hugo setup

Choosing a theme

I found it hard to find a suiting theme for my blog. One big reason might be, that I wanted a theme similar to the one I used on Wordpress. But adding to that, there generally isn’t as many themes readily available for Hugo as there is for Wordpress.

The theme I ended up choosing is Mainroad created by Vimux. You can find it on GitHub using this link: To get an idea about how it looks, just let your eyes wander around this page or elsewhere around my blog 😉

After some tweaks and modifications I think that will do just fine. I have currently made some minor tweaks to it, colors and similar stuff, but will do more later.

“Posts” folder structure

Using Hugo you can arrange your posts in different ways. I prefer to create one folder per post, which then contains the posts markdown file and all the images and files used in it.

In Hugo terms this way of organizing content is named Page Bundles. Read more about it on the following Hugo documentation page:

How you chose to organize your content is of course very much up to your own preferance. Though, if you would like to use a headless CMS, like Forestry, you might be forced use one single folder for all the static files you use in your posts. There are several headless CMS’ to chose from, and if you need one, then you should be able to find one fitting your needs.

Build and hosting on Cloudflare

Various vendors offer easy to setup and use build and hosting for Hugo generated sites, among others GitHub and Cloudflare.

I chose Cloudflare because of combination of its multitude of available services, its speed, and its fairly easy setup.

Build setup

The following guide describes how you setup Cloudflare Pages to build and deploy your Hugo generated website when its Git repository is updated:

The guide actually explains the entire process from installing Hugo locally, setting up a site, hosting it on GitHub, and then finally setup build and deploy using Cloudflare Pages.

Setting Hugo version in Cloudflare Pages

I quite early ran into an issue, where my local build version of my blog looked different than the one built and hosted on Cloudflare.

I first realized the differences when I saw that example code snippets in my posts didn’t use the set theme when seen in production. Then I saw that most of the HTML generated by Hugo looked different when comparing the two environments.

After searching for any explanations and fixes to this issue, I found out that the Hugo version automatically used by Cloudflare Pages is apparently a very old one. I then found out that it is quite easy to manually ask Cloudflare Pages to use a newer specific Hugo version for the builds, and if you already read above mentioned Cloudflare Pages setup guide, you should know how to do just that. Here’s a link directly to the section describing how to set the Hugo version manually:

Migrating content from Wordpress

Since I already had a Wordpress blog up and running, I wanted to convert my exsiting blog posts from Wordpress to Hugo. My hope was, that this could be done without to much manual, and cumbersome, work for me.

Luckily for me, a Google search resulted in this blog post by Mattias Geniar: The post gives a nice walk-through of the needed steps, with all the heavy work done by the following Wordpress plugin created by Cyrill Schumacher:

The plugin does a nice job at exporting both the posts and all the static files included in them. Some manual conversion work is still needed, but using the plugin certainly limits it quite a bit.

Adding Google Tag Manager

Searching for resources about how to best add Google Tag Manager to Hugo, I quickly found this blog post written by Martijn van Vreeden:

I would recommend you to give that a read, since it gave me a good introduction in how a tool like Google Tag Manager fits into a Hugo setup. Based on that, the following shows you how I chose to do it on my blog.

Setting you GTM container ID in the Hugo settings file

As Martijn describes, I have added my GTM Container ID as a settings variable in my Hugo setting file. My Hugo setup is using TOML files, so my setting file is named “config.toml” and is placed in the root folder of my setup.

The setting is placed something like this in the file.

    gtm_id = "GTM-XXXXXXX"

Script and Data Layer setup

My Google Tag Manager script and its data layer is placed in a separate partial, e.g., an HTML component, named “gtm_head.htm”, which I have placed in the following Hugo specific folder: “layout/partials/”. This is the part of the Google Tag Manager implementation Google asks you to place in the HEAD part of your HTML documents.

This is the contents of the file.

window.dataLayer = window.dataLayer || [];
    'page_title': '{{ .LinkTitle }}',
    {{- if eq .Kind "page"}}
    'page_id': '{{ with .File }}{{ .UniqueID }}{{ end }}',
    {{- end}}
    'page_permalink': '{{ .Permalink }}',
    'page_kind': '{{ .Kind }}',
    'page_type': '{{ .Type }}',
    {{- if .Params.categories}}
    'page_categories': '{{ delimit .Params.categories "|" }}',
    {{- end }}
    {{- if .Params.tags}}
    'page_tags': '{{ delimit .Params.tags "|" }}',
    {{- end }}
    {{- if .ExpiryDate }}
    'page_expiry_date': '{{.ExpiryDate.format "2006-01-02"}}',
    {{- end }}
    {{- if not (eq (.PublishDate.Format "2006-01-02") "0001-01-01")}}
    'page_publish_date': '{{.PublishDate.Format "2006-01-02"}}',
    'page_modified_date': '{{.Lastmod.Format "2006-01-02"}}',
    {{- end }}
    {{- if .ReadingTime }}
    'page_reading_time_minutes':{{ .ReadingTime }},
    'page_reading_time_seconds':{{- $readTime := mul (div (countwords .Content) 220.0) 60}}{{- math.Round $readTime}},
    {{- end }}
    {{- if not (eq .WordCount 0)}}
    'page_word_count':{{ .WordCount }},
    'page_fuzzy_word_count':{{ .FuzzyWordCount }},
    {{- end }}
    'page_author': '{{ if -}}{{ }}{{- else if -}}{{ }}{{- end }}',
    'page_language': '{{ .Language }}',
    'page_translated':{{ .IsTranslated }}
<link href="" rel="preconnect" crossorigin>
<link rel="dns-prefetch" href="">
<!-- Google Tag Manager -->
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
})(window,document,'script','dataLayer','{{ .Site.Params.gtm_id }}');</script>
<!-- End Google Tag Manager -->

As you might see yourself, I defined this a bit different than what Martijn describes in his post.


Standard Google Tag Manager implementation also include an iframe part placed in the BODY part of your HTML documents. This is there for browsers with JavaScript disable or browsers not supporting JavaScript at all.

You might argue that this isn’t needed, since very few websites even run without using JavaScript. I just went ahead and added it to my setup as a file named “gtm_body.html” placed in the same folder as the one containing the script and data layer.

This is the contents of the file.

<noscript><iframe src="//{{ .Site.Params.gtm_id }}" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>

I haven’t added site search to my blog yet, but it is possible to do in various different ways.

Way back in 2018, Simo Ahava wrote the following post about his chosen method: The post also describes a couple of other options available at the time.