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.
$ 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.
homepage ├── post1 ├── post2 ├── post3 ... └── postN
Download a copy of the theme with
$ 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.
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.
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.twitter etc. should be supplied to Hugo as variables in the
config.toml file, which will be discussed in a later section.
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.
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.
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.
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.
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.
description, contacts e.g.
.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.
Apart from the homepage that is described by the
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.
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/*
Hugo allows front matter in a post for metadata specific to it e.g.
date, and this theme expects the following.
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
<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
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.
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.
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
Cloudflare Web Analytics
head.html partial template for metrics collection on every page.