Thinking. Writing. Philosophising.

Email Github LinkedIn

Web Development: From Wordpress to Hugo

Posted on November 11, 2022 — 11 Minutes Read

10 years ago in 2012, the domain name kurtcms.org was registered, and this website was born. From the start it was built with Linux, Apache, MySQL and PHP i.e. the LAMP stack, and WordPress for content management. Throughout the years, Apache was replaced by NGINX for its lower memory footprint and better overall performance i.e. from LAMP to LEMP, which, in the rise of containerisation, was further refactored into a number of containers orchestrated by Docker Compose, for rapid and modular deployment that fits in any microservice architecture. Yet in the unending pursuit of speed, and considering the static nature and relative simplicity of this site, there comes a time when the rich features that come with WordPress become more of a burden than a blessing, and that fundamental architectural decision will need to be made in order to further reduce page load time.

Increasing popular for generating simple website are the static site engines such as Hugo and Gatsby. Not only do these engines generate static HTML pages that are blazingly fast. It does so from sources written in the lightweight and easy-to-read markup language, Markdown. Once generated, these HTML pages can be deployed to anywhere — from a traditional web server on LAMP or LEMP, or a Javascript, API and Markup platform i.e. a JAMstack such as Cloudflare Pages, that serves HTML pages from its global edge network, milliseconds away from 95% of the world’s internet population. What follows will be a walk-through on migrating this website from the feature-rich WordPress to a fast and modern static site generator, Hugo, and deploy it on Cloudflare Pages for a lightning fast performance.


Hugo can be installed on a variery of operating systems. For macOS, an open source package manager such as, Homebrew, can be used.

$ brew install hugo

The official Quick Start page details the steps of creating a new site, and it is as straightforward as a single command.

hugo new site *domain*

$ hugo new site kurtcms.org


This website contains one homepage, that lists all of the available posts, ordered by the publish date.

├── post1
├── post2
├── post3
└── postN


For the simplicity of this site, a theme is created from scratch, instead of importing an existing one. For reference and further development, the theme is shared on Github.

Download a copy of the theme with git submodule.

$ git submodule add https://github.com/kurtcms/thoughts/ themes/thoughts
$ git submodule update --init --recursive


The beautiful Bootstrap 5 CSS framework is used for this site. A custom CSS file is added alongside, with the !important property for certain style override. All of which are placed under static/css/ in the theme directory.


Partials are, as the name suggests, templates that contain parts that can be imported to a page. These are useful for common elements that are displayed on every pages on the site. For the needs of this site, four of them are needed.

  1. head.html
  2. header.html
  3. footer.html
  4. render-link.html

The head.html partial template details the layout of the <head> section before the <body>. The Bootstrap 5 and the custom CSS files should be referenced here, alongside the other metadata of the page. Hugo ships with a number of internal templates that are rather handy. For example, for support with the Open Graph protocol and Twitter Card, it is a matter of a one-liner to import the necessary boilerplates. This website does not use Google Analytics, but for those venturing down a similar path with such need, a template can be imported in a single line as well. For reason unknown, the templates for Open Graph and Twitter Card seem to be missing some metadata e.g. twitter:site and og:updated_time etc. that are needed for this website. They are added as such on top of the boilerplates, with a simple if .IsHome conditional that places them where they should be. All of which are in layouts/partials/head.html in the theme directory.

The parameters needed for Open Graph, Twitter Card and Google Analytics e.g. .Site.Params.description and .Site.Params.twitter etc. should be supplied to Hugo as variables in the config.toml file, which will be discussed in a later section.


The header.html partial goes otherwise inside the <body> section right before the content of the page e.g. <main>. Since this partial is displayed before the page content, for other website it is the perfect place for a navigation menu, for this website however, contacts will suffice. This is in layouts/partials/header.html in the theme directory.

A few images are imported from Bootstrap Icons for contacts. These are placed under static/img/ in the theme directory.


The footer.html partial denotes the <footer> section that comes after the <body>. For other website, it may include a HTML sitemap, or a legal or privacy statement. For this website, a copyright statement will suffice. This is in layouts/partials/footer.html in the theme directory.


The render-link.html partial informs Hugo that external links are to be open in a new tab. This is in layouts/_default/_markup/render-link.html in the theme directory.


The content of each page is informed by the corresponding layout template. For the simplicity of this site, only three are needed.

  1. index.html
  2. single.html
  3. 404.html

Being the homepage of the site, the index.html curates the first impression to the audience. For this website, a simple list of all of the available posts, ordered by the publish date, will suffice. This is in layouts/index.html in the theme directory.


The single.html is responsible for the layout of a post. For this website, below the title of the post, the publish date and a reading time, estimated at 200 words per minute, are displayed. This can be done by using the built-in fuctions. Table of contents is also displayed before the content of the post. All of which are in layouts/_default/single.html in the theme directory.


In case the audience is lost, the 404.html page will be displayed. For this website, a simple message, together with a short list of the latest posts, generated by the range first 10 .Pages function will suffice. This is in layouts/404.html in the theme directory.


The theme expects various parameters, such as metadata of the site e.g. title and description, contacts e.g. .Site.Params.email and .Site.Params.github, as well as those needed for Open Graph and Twitter Card e.g. .Site.Params.twitter to be supplied in the config.toml file. The theme provides a template for defining these parameters accordingly.

For the needs of this site, robots.txt is enabled to guide search engine crawlers. Also enabled is raw HTML rendering for support with HTML tags that are of no equivalent in Markdown. The details of which will be discussed in a later section. For those who do not need them, the enableRobotsTXT parameter and the [markup.goldmark.renderer] section may be removed or disabled.

Copy the provided configuration file.

$ cp themes/thoughts/config.toml config.toml

Modify the parameters accordingly.

$ nano config.toml


With the new site created and a theme in place, existing content may be migrated from WordPress, and new content may be created directly with Hugo.

Content Migration

Apart from the homepage that is described by the index.html in layouts/index.html in the theme directory, every post is a separate Markdown document under content/ in the site directory. For migrating content from WordPress to Hugo, the pandoc document converter will prove to be extremly handy. For macOS, Homebrew can be used.

$ brew install pandoc

Pandoc understands a wide variety of document formats including HTML and Markdown, and can be used to convert WordPress posts in HTML, to Markdown documents in a single command.

pandoc -f html -t markdown *output.md* *input.html*

$ pandoc -f html -t markdown web-development-from-wordpress-to-hugo.md web-development-from-wordpress-to-hugo.html

A simple script that loops over all WordPress posts in HTML will have a large part of the migration job done.

The render-link.html partial, in layouts/_default/_markup/render-link.html in the theme directory, distinguishes externals links from internal ones by the http prefix. To open internal links in the same tab, they should be rid of the fully qualified domain name (FQDN) of the site. This can be done rather easily with the sed command.

sed -i '' -e 's/*FQDN*//g' *post.md*

$ sed -i '' -e 's/https:\/\/kurtcms.org//g' content/*

For matching a certain pattern in all the posts, the grep command will come in rather handy.

grep *pattern* *post.md*

$ grep https://kurtcms.org content/*
Front Matter

Hugo allows front matter in a post for metadata specific to it e.g. title and date, and this theme expects the following.

  1. title
  2. date
  3. draft
  4. description

These should be self-explanatory, and being specific to the post, it is most fitting to have them defined in the front matter instead of the config.toml file that houses site-wide parameters. Given that they are used by the head.html partial for generating metadata specific to the post for search engines, Open Graph and Twitter Card, for best Search Engine Optimisation (SEO) practices, it is crucial to define them properly.


Images can be placed either in content/ for page resources, or assets/ for global resources, in the site directory. For images in a post, given the page scope, it is most fitting to have them in content/ alongside the post.


For the needs of this site, specifically for using <iframe> and <table> with <td colspan>, as well as other handy HTML tags that are of no equivalent in Markdown, raw HTML rendering is enabled by the [markup.goldmark.renderer] section in the config.toml file.

Content Creation

Creating new content directly with Hugo is as delightful as a single command.

hugo new post.md

$ hugo new web-development-from-wordpress-to-hugo.md

A new Markdown document will be created under content/ in the site directory ready for editing.


With the new site created, a theme in place, and the content migrated, it is all set for deployment.


Before deploying the new site on the internet, it is best to examine it on a localhost. With Hugo this can be done in a single command.

$ hugo server -D

The new site will now be accessible at //localhost:1313/. Changes are watched by the local server, and will be reflected on the site right away.

Cloudflare Pages

Once everything is set, the site is ready for prime time. The static HTML pages generated by Hugo can be deployed to anywhere — from a traditional web server on LAMP or LEMP, or a JAMstack platform. For the needs of this site, Cloudflare Pages is the JAMstack platform of choice. It has a free tier; it supports deploying from a Github repository; and it serves the pages from its global edge network that is milliseconds away from 95% of the world’s internet population. The official Cloudflare Docs details the steps of deploying a Hugo site. One thing to note is that for reason unknown, at the time of writing, Cloudflare Page will build with Hugo version 0.54.0, which in most cases will be versions behind the local server and will strip of it, features, that come with the later version of Hugo.

Specifying the Hugo version to use for building the site requires an environment variable HUGO_VERSION, and it can be set to the version matching the local server e.g. 0.104.3.


Google Analytics is the analytics platform of choice for many, and as previously discussed, it can be enabled by importing a boilerplate in the head.html partial template, with the tracking ID supplied as variable in the config.toml file.

Cloudflare Web Analytics

For the needs of this site, Cloudflare Web Analytics is used instead. It has a free tier; it is privacy first and non-invasive; and since the domain name kurtcms.org is already managed with Cloudflare DNS, the setup is automatic. For those who would like to take advantage of Cloudflare Web Analytics, with nonetheless a domain not proxied behind Cloudflare, a simple JavaScript snippet can be added to the head.html partial template for metrics collection on every page.


From LAMP to LEMP, from virtual machine to containers, and now from WordPress to Hugo, web development is an ever evolving space, and I am eager to see what comes next.