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/).
Overview
The OpenValue website serves mostly static content. Before we were aware of (and made time to use) Hugo, the website basically looked like this:
app
└── 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.
Quick-starting
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:
layouts
└── 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:
layouts/<TYPE>/<LAYOUT>.html
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.
Content
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.
block
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" }}
<!-- PAGE SPECIFIC HTML CONTENT -->
{{ 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
.
partial
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.
.Params
Take the <head>
section. It contains meta
tags and stylesheet link
s. 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+
. 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.
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:
<head>
{{- partial "meta-data.html" . | htmlUnescape | safeHTML -}}
{{ range .Params.stylesheets }}
<link rel="stylesheet" type="text/css" href="{{.}}" />
{{end}}
{{ partial "stylesheet.html" . }}
</head>
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>
<span class="nav-items">
<ul>
<!-- 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>
</ul>
</span>
</nav>
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.
Conclusion
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.