8 December 2021

How we use Hugo to generate the OpenValue website

Hugo is a static site generator. After the OpenValue website had been a static site for a while, we switched to Hugo to generate it. This article summarizes helpful tips, and some problems we’ve encountered when rebuilding the OpenValue website using Hugo. In this process we used a lot of default configuration, when it differed I’ll mention the changes made. Of course, it is always possible to configure Hugo differently than we did (https://goHugo.io/getting-started/configuration/).


The OpenValue website serves mostly static content. Before we were aware of (and made time to use) Hugo, the website basically looked like this:

└── about
|   └── index.html
└── assets
|   └── css
|   |   └── main.css
|   └── js
|   |   └── main.js
└── career
|   └── index.html
└── contact
|   └── index.html
└── images
|   └── openvalue-logo.png
└── imprint
|   └── index.html
└── privacy
|   └── index.html
└── services
|   └── index.html
└── tech-insights
|   └── index.html
└── training
|   └── index.html
└── index.html
└── sitemap.xml

Each page on our website was a static html file. This caused us to have a lot of duplicate code. With Hugo we could get rid of this duplication. But, before we dive into Hugo’s syntax, commands and definitions, it is useful to understand the basic idea behind Hugo. Hugo generates static files using provided “content” to fill in “specific templates”. I’ll explain what I mean by that after an initial Hugo quick-start.

Because how do you use Hugo? The quick-start of Hugo is simple enough (https://goHugo.io/getting-started/quick-start/). Hugo has a CLI, with which you can instantiate a Hugo project. A Hugo project holds a config.toml and typically a content, layouts, and themes directory.


After instantiating a Hugo project, we can use the Hugo server. The Hugo server is a webserver that serves your website without having to generate it first, watching your files for changes. So we made our previous static repository into a Hugo project. We hadn’t changed anything yet. With the command hugo server we tried to make the website work again.

We started with all these 404’s, because Hugo couldn’t find which templates to serve. Hugo searches for its templates in the layouts directory. After a quick and very shallow documentation reading of Hugo, we saw that Hugo also takes subpaths into account when deciding which template to render. So we copy-pasted the whole lot of the original app folder into the layouts folder. That looked like this:

└── about
|   └── index.html
└── career
|   └── index.html
└── contact
|   └── index.html
└── imprint
|   └── index.html
└── privacy
|   └── index.html
└── services
|   └── index.html
└── static
|   └── css
|   |   └── main.css
|   └── images
|   |   └── openvalue-logo.png
|   └── js
|   |   └── main.js
└── tech-insights
|   └── index.html
└── training
    └── index.html
└── index.html
└── sitemap.xml

Weirdly that did not work… However, we did see the homepage rendered correctly, so something worked. We came to learn that this had everything to do with content organisation.

Content Organisation

The content folder is where you provide Hugo with values to fill your templates, and the organisation of this directory is important. Hugo assumes that you have organized your content the same way that is used to render the site. All this is very well hidden in Hugo’s documentation.

Your content can be organized using page bundles. A page bundle can be:

  • a leaf bundle, leaf means it has no children
  • a branch bundle, it can have children

A branch bundle is defined using a _index.md file, whereas leaf bundles lack a _ and can be named whatever you desire. The layout used for branch bundles is list, and for leaf bundles is that single.

We knew which paths we wanted to have, so we combined that knowledge with the page bundle knowledge. Firstly, we changed the content as shown below.

└── content
    └── about.md           // <- https://openvalue.eu/about/
    └── career.md          // <- https://openvalue.eu/career/
    └── contact.md         // <- https://openvalue.eu/contact/
    └── imprint.md         // <- https://openvalue.eu/imprint/
    └── privacy.md         // <- https://openvalue.eu/privacy/
    └── route.md           // <- https://openvalue.eu/route/
    └── services.md        // <- https://openvalue.eu/services/
    ├── tech-insights.md   // <- https://openvalue.eu/tech-insights/
    └── _index.md          // <- https://openvalue.eu/

This way all subpaths were leaf bundles, and the home page a branch. All these markdown files were empty, since all static content was already present on the templates. Second of all, we renamed all index.html to single.html, and the root index.html to list.html. After all, that were the file names that Hugo would go looking for based on our content organisation.

A big step closer to a working website, but we’re still not there yet. There are some more lookup order rules to take into account. In general, we found out that Hugo will look for:

  1. layouts/<TYPE>/<LAYOUT>.html
  2. layouts/_default/<LAYOUT>.html

If we wanted to use the single.html depending on the path in layouts (e.g. layouts/about/single.html for https://openvalue.eu/about/), we had to tell Hugo what type the content is. Documentation states the content type is resolved from either the type in front matter or, if not set, the first directory in the file path. Front matter is declared in the content. So we had to fill in our markdown files with some information for Hugo.


The content folder is where you provide Hugo with values to fill your templates. From this point in our story we just needed to declare the type in something called front matter. For example, this is what about.md looked like:

type: about

With the --- we tell Hugo that this part encloses front matter written in the YAML format. type is a reserved keyword within front matter, and it has many more. We applied this change to all markdown files, of course with the correct type value. These changes made Hugo pick out the correct template for each page. We generated the same website as we had before we started with Hugo!

Hugo’s power

Now we had a functioning website again, it was time to use Hugo as intended. We started by getting rid of duplication. For instance, every single.html and the index.html contained an identical footer, a similar head, a similar banner, a similar navigation menu, and a similar script. Luckily you can have a base template which all other templates inherit from. We would put similar parts on this base template and move page-specific information to the children. The base page is the layouts/_default/baseof.html. So we created this basic html template and pasted all identical or similar parts into it. We removed these parts from the other templates. This caused us to lose page-specific information and all pages only consisted of the shared parts. We had to refer to page specific information on this base template. That’s where block comes in.


The block keyword enables you to define which part children need to define. Within the layouts/_default/baseof.html we created a block for the content like this:

{{ block "main" . }}{{ end }}

In every template we enclosed the remaining content like this:

{{ define "main" }}
{{ end }}

We did the same for any page specific script. That way we kept the shared script on the layouts/_default/baseof.html and any additional script could be defined on the children’s templates. Hugo made block function that everything within block will be overridden when defined by its children. This way we can define a default on the base page, and when we want to differ from the default we can define it on the page-specific template. Take for example the banner. All banners are static, with the homepage as an exception. On the homepage we would want to display an animated banner. So we defined the static banner on the layouts/_default/baseof.html within {{ block "header" . }}. In the index.html we overridden it with the template of the dynamic banner. We did not define this block in the single.html’s. That way all single.html’s got the static banner defined in layouts/_default/baseof.html.

We had a lot less duplication and most of the site was working again. But the layouts/_default/baseof.html looked cluttered. That’s why we moved some parts of this template to layouts/partials.


A partial is a template located in the layouts/partials directory. A partial is like a block which you cannot override. The footer was identical for every page, so a perfect candidate. That is why we cut the footer and pasted it in a new file layouts/partials/footer.html. Then we replaced the footer part on the layouts/_default/baseof.html with:

{{ partial "footer.html" . }}

The . after the partial name represents the current context. Suggesting that you can pass parameters to a partial. Even so, with partial you cannot leave out parameters, and best practice is to pass in the current context. With other parts of our website passing parameters would come in handy.


Take the <head> section. It contains meta tags and stylesheet links. We paid great attention to the content of meta, because it provides better SEO which makes your website findable via search engines. We would like to have it specified per page. In addition to that we would be able to add necessary stylesheets per page. So we went back to our content markdown files, since that is where page specific content lives.

Here we added all meta values, and a list of stylesheets to the front matter for pages that need additional stylesheets. Below you’ll find the content/about.md file.

title: "About OpenValue: better software, faster"
ogTitle: "About OpenValue: better software, faster"
twitterTitle: "About OpenValue: better software, faster"
description: "OpenValue consists out of decentralized teams of over 100 highly skilled full stack Java experts operating from different countries in Europe."
ogDescription: "Decentralized teams of 100+ full stack Java experts"
twitterDescription: "OpenValue consists out of decentralized teams of over 100 highly skilled full stack Java experts."
canonicalLink: "https://www.openvalue.eu/about/"
ogUrl: "https://www.openvalue.eu/about/"
type: "about"
stylesheets: ["css/map.css"]

With the .Params keyword we are able to refer to the parameters declared in our front matter. So, we made a layouts/partials/meta-data.html. This partial looked lik this:

<meta charset="utf-8" />

<meta property="og:image" content="https://www.openvalue.eu/images/openvalue-color-square.png" />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />

<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@OpenValue" />

<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<base href="/">

<title>{{.Params.title }}</title>
<meta property="og:title" content="{{.Params.ogTitle}}" />
<meta property="twitter:title" content="{{.Params.twitterTitle}}" />
<meta name="description" content="{{.Params.description}}"/>
<meta property="og:description" content="{{.Params.ogDescription}}" />
<meta property="twitter:description" content="{{.Params.twitterDescription}}" />
<link rel="canonical" href="{{.Params.canonicalLink}}"/>
<meta property="og:url" content="{{.Params.ogUrl}}" />

As we added some Hugo generation magic in between, we wanted to see if the website would still be generated the same as we started with. With Hugo’s CLI we could generate all files and via a git compare we’d see if files remained the same. As it turns out our 100+ in the description was html escaped to 100&#43;. It would work alright because it would be within a html attribute. Then again, it was messing up our git compare, so we wanted to see if we could prevent this using some of Hugo’s template functions.


There are multiple functions that Hugo facilitates. We solved the “html escaped” problem in our meta values by using the htmlUnescape and safeHTML function. By combining the two with a pipe when we pass the context as parameters to the layouts/partials/meta-data.html the HTML escape codes are unescaped. This looked like this:

{{ partial "meta-data.html" . | htmlUnescape | safeHTML }}

The htmlUnescape function alone did not do the trick because “Remember to pass the output of this to safeHTML if fully un-escaped characters are desired”. And yes, that is what we desired.

Continuing, we still had stylesheets as a parameter. Because a page could have more than 1 additional needed stylesheet, we declared the stylesheets parameter as a list in front matter. And we want to loop through that list to add a <link rel="stylesheet"> tag. This was achieved by use of the range function. Resulting in a <head> on the layouts/_default/baseof.html looking like this:

	{{- partial "meta-data.html" . | htmlUnescape | safeHTML -}}
	{{ range .Params.stylesheets }}
	<link rel="stylesheet" type="text/css" href="{{.}}" />
	{{ partial "stylesheet.html" . }}

As you can deduct out of the snippet above, the . within range represents an element in the list. In case of the stylesheets, that would be a string of the additionally required stylesheet.

At this point a lot of the website was functioning as before. A big styling part that was still broken, was the navigation. The original navigation bar had a blue line underneath the page that was active. This was managed by adding a class to the <li class="nav-active">. So, we wanted to add this class conditionally, depending on the .Type of the page/child. This is how it looked like when it worked:

<nav id="nav">
   <span class="logo-helper">
   	<a id="home-link" href="#"><img src="/images/openvalue-white.png" alt="OpenValue logo"></a>
   <span class="nav-items">
   		<!-- top menu bar-->
   		<li id="nav-home-icon"><a href="#"><i class="fas fa-home"></i></a></li>
   		<li id="nav-home" {{if eq .Type "home"}}class="nav-active"{{end}}><a href="#">Home</a></li>
   		<li id="nav-services" {{if eq .Type "services"}}class="nav-active"{{end}}><a href="services/">Services</a></li>
   		<li id="nav-training" {{if eq .Type "training"}}class="nav-active"{{end}}><a href="https://openvalue.training">Training</a></li>
   		<li id="nav-career" {{if eq .Type "career"}}class="nav-active"{{end}}><a href="career/">Career</a></li>
   		<li id="nav-tech" {{if eq .Type "tech-insights"}}class="nav-active"{{end}}><a href="tech-insights/">Tech insights</a></li>
   		<li id="nav-about" {{if eq .Type "about"}}class="nav-active"{{end}}><a href="about/">About</a></li>
   		<li id="nav-contact" {{if or (eq .Type "contact") (eq .Type "route")}}class="nav-active"{{end}}><a href="contact/">Contact</a></li>

You can see that we combined the if function with the eq and, in case of the contact page, with the or function.

Future improvements

With Hugo it is quite normal to move the actual content of the page to the related markdown file. In our process, you did not see any texts being moved from the layouts/<type>/single.html to the content/<type>.md. Reason for this is the previously established styling of the website. Whenever you start your website design at the same time as you use Hugo, you can manage your styling easier. However, we started with our whole website statically written making use of classes or id’s. Hugo makes use of Goldmark as default Markdown processor. Goldmark supports adding classes, id and custom attributes to heading only. So we would use a lot of styling when we would move text to the markdown files.

A possible way of solving this, is completely restructure our css files. A big disadvantage with this would be that we could not be sure the website looks the same as it did before. We would lose our git compare advantage to ensure the website staying the same.

Another possibility would be to make use of Hugo’s shortcode. A shortcode is like a partial which you can use in your content-files. You would refer to a shortcode in the content/<type>.html passing the text as a parameter. The layouts/shortcode/<type>.html would define classes, id’s etc, and would inject the text via .Params. Problem solved? Well, the styling is not implemented generically. So, this would result in a layouts/shortcode/<type>.html for almost every piece of text we have. Figuring, this is not going to decrease the size of our repository or increase readability.


For now, we concluded our rebuilding phase for the purpose of the Hugo framework. We have made a lot of improvements on our website’s repository for future adjustments and maintenance. Future improvements require a lot more time. However, the obstacle we have encountered with rebuilding our website and its styling is a problem which can be surpassed whenever you start building your webiste with Hugo from the start.

Rosanne Joosten

Rosanne is a software engineer at OpenValue. She works for top companies in the Netherlands always questioning the way things are done. Rosanne codes in the language that is required, which she is glad to learn when necessary. She worked on the company's website using the Hugo framework. She also likes puzzles, meditating and yoga.