Develop a Theme for Hugo


This article was originally published on zeolearn .

Introduction

In this tutorial, I will show you how to create a basic Hugo theme. I assume that you are familiar with basic HTML, and how to write content in markdown. I will be explaining the working of Hugo and how it uses Go templating language and how you can use these templates to organize your content. As this post will be focusing mainly on Hugo's working, I will not be covering CSS here.

We will be starting with some necessary information about the terminology used in Hugo. Then we will create a Hugo site with a very basic template. Then we will add new templates and posts to our site as we delve further into Hugo. With very slight variations to what you will learn here, you will be able to create different types of real-world websites.

Now, a short tutorial about the flow of this post. The commands that start with $ are meant to be run in the terminal or command line. The output of the command will follow immediately. Comments will begin with #.

Some Terminology

Configuration File

Hugo uses a configuration file to identify common settings for your site. It is located in the root of your site. This file can be written in TOML, YAML or JSON formats. Hugo identifies this file using the extension.

By default, Hugo expects to find Markdown files in your content/ directory and template files in your themes/ directory. It will create HTML files in your public/ directory. You can change this by specifying alternate locations in the configuration file.

Content

The content files will contain the metadata and text about your posts. A content file can be divided into two sections, the top section being frontmatter and the next section is the markdown that will be converted to HTML by Hugo. The content files reside in /content directory.

Frontmatter

The frontmatter section contains information about your post. It can be written in JSON, TOML or YAML. Hugo identifies the type of frontmatter used with the help of identifying tokens(markers). TOML is surrounded by ---, YAML is by --- and JSON is enclosed in curly braces { and }. The information in the front matter of a content type will be parsed to be used in the template for that specific content type while converting to HTML.

I prefer to use YAML, so you might need to translate your configurations if you are using JSON or TOML.

This is an example of frontmatter written in YAML.

---
date: "2018-02-11T11:45:05+05:30"
title: "Basic Hugo Theming Tutorial."
description:
  "A primer about theme development for Hugo, a static site generator written in
  Golang."
categories:
  - Hugo
  - Customization
tags:
  - Theme
---

You can read more about different configuration options available for frontmatter here .

Markdown

The markdown section is where you will write your actual post. The content written here will automatically be converted to HTML by Hugo with the help of a Markdown engine.

Templates

In Hugo, templates govern the way; your content will be rendered to HTML. Each template provides a consistent layout when rendering the markdown content. The templates reside in the /layouts directory.

There are three types of templates: single, list and partial. Each kind of template take some content as input and transform it according to the way defined in the template.

Single Template

A single template is used to render a single page. The best example of this is about page.

List Template

A list template renders a group of related content. It can be all recent posts or all posts belonging to a particular category.

The homepage template is a specific type of list template. Hugo assumes that the homepage will serve as a bridge to all the other content on your website.

Partials

Partials are short code snippets that can be injected in any other template type. They are instrumental when you want to repeat some content on every page of your website. The header and footer content are good candidates to be included in separate partials. It is a good practice to use partials liberally in your Hugo site as it adheres to DRY principle.

Okay, Let's Start

So now that you have a basic understanding of Hugo, we will create a new site using Hugo. Hugo provides a command to generate new sites. We will use that command to scaffold our site. It will create a basic skeleton of your site and will give you a basic configuration file.

$ hugo new site ~/zeo
$ cd ~/zeo
$ ls -l
total 28
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 archetypes
-rw-r--r-- 1 yash hogwarts   82 Feb 11 11:13 config.toml
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 content
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 data
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 layouts
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 static
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:13 themes

Note: I will use YAML format for the config file. Hugo, By default, uses TOML format.

A small description of this directory structure:

  • archetypes: The archetypes contains predefined frontmatter format for your website's content types. It facilitates consistent metadata format across all the content of your site.

  • content: The content directory contains the markdown files that will be converted to HTML and served to the user.

  • data: From Hugo documentation

    The data folder is where you can store additional data for Hugo to use when
    generating your site. Data files are not used to generate standalone pages;
    rather, they are meant to be supplemental to content files. This feature can
    extend the content in case your front matter fields grow out of control. Or
    perhaps you want to show a larger dataset in a template (see example below).
    In both cases, it is a good idea to outsource the data in their files.

  • layouts: The layouts folder stores all the templates files which form the presentation of the content files.

  • static: The static folder will contain all the static assets such as CSS, JS and image files.

  • themes: The themes folder is where we will be storing our theme.

We will edit the config.yaml file to edit some basic configuration of the site.

$ vim config.yaml
baseURL: /
title: "My First Blog"
defaultContentLanguage: en
languages:
  en:
    lang: en
    languageName: English
    weight: 1
MetaDataFormat: "yaml"

Now when you run your site, Hugo will show some errors. It is normal because our layouts and themes directories are still empty.

$ hugo --verbose
INFO 2018/02/11 11:20:59 Using config file: /home/yash/zeo/config.yaml
Building sites ... INFO 2018/02/11 11:20:59 syncing static files to /home/yash/zeo/public/
WARN 2018/02/11 11:20:59 No translation bundle found for default language "en"
WARN 2018/02/11 11:20:59 Translation func for language en not found, use default.
WARN 2018/02/11 11:20:59 i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.
WARN 2018/02/11 11:20:59 [en] Unable to locate layout for "taxonomyTerm":
...
...

This command will also create a new directory called public/. This is the directory where Hugo will save all the generated HTML files related to your site. It also stores all static data in this folder.

Let's have a look at the public folder.

$ ls -l public/
total 16
drwxr-xr-x 2 yash hogwarts 4096 Feb  11 11:22 categories
-rw-r--r-- 1 yash hogwarts  400 Feb  11 11:25 index.xml
-rw-r--r-- 1 yash hogwarts  383 Feb  11 11:25 sitemap.xml
drwxr-xr-x 2 yash hogwarts 4096 Feb  11 11:22 tags

Hugo generated some XML files, but there are no HTML files. It is because we have not created any content in our content directory yet.

At this point, you have a working site with you. All that is left is to add some content and a theme to your site.

Create a new theme

Hugo doesn't ship with a default theme. There are a lot of themes available on Hugo website. Hugo also ships with a command to create new themes.

In this tutorial, we will be creating a theme called zeo. As mentioned earlier, my aim is to show you how to use Hugo's features to fill out your HTML files from the markdown content, I will not be focusing on CSS. So the theme will be ugly but functional.

Let's create a basic skeleton of the theme. It will create the directory structure of the theme and place empty files for you to fill in.

# run it from the root of your site
$ hugo new theme zeo
$ ls -l themes/zeo/
total 20
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:30 archetypes
drwxr-xr-x 4 yash hogwarts 4096 Feb 11 11:30 layouts
-rw-r--r-- 1 yash hogwarts 1081 Feb 11 11:30 LICENSE.md
drwxr-xr-x 4 yash hogwarts 4096 Feb 11 11:30 static
-rw-r--r-- 1 yash hogwarts  432 Feb 11 11:30 theme.toml

Fill out LICENSE.md and theme.toml file if you plan to distribute your theme to outside world.

Now we will edit our config.yaml file to use this theme.

$ vim config.yaml
theme: "zeo"

Now that we have an empty theme, let's build the site.

$ hugo --verbose
INFO 2018/02/11 11:34:14 Using config file: /home/yash/zeo/config.yaml
Building sites ... INFO 2018/02/11 11:34:14 syncing static files to /home/yash/zeo/public/
WARN 2018/02/11 11:34:14 No translation bundle found for default language "en"
WARN 2018/02/11 11:34:14 Translation func for language en not found, use default.
WARN 2018/02/11 11:34:14 i18n not initialized, check that you have language file (in i18n) that matches the site language or the default language.
 
                   | EN
+------------------+----+
  Pages            |  3
  Paginator pages  |  0
  Non-page files   |  0
  Static files     |  0
  Processed images |  0
  Aliases          |  0
  Sitemaps         |  1
  Cleaned          |  0
 
Total in 12 ms

These warnings are harmless in our case, as we are developing our site in English only.

Hugo does two things while generating your website. It transforms all the content files to HTML using the defined templates, and its copies static files into the site. Static files are not transformed by Hugo. They are copied exactly as they are.

The Cycle

The usual development cycle when developing themes for Hugo is:

  • Delete the /public folder
  • Run the built-in web server and open your site in the browser
  • Make changes to your theme files
  • View your changes in browser
  • Repeat step 3

It is necessary to delete the public directory because Hugo does not try to remove any outdated files from this folder. So the old data might interfere with your workflow.

It is also a good idea to track changes in your theme with the help of a version control software. I prefer Git for this. You can use others according to your preference.

Run your site in the browser

Hugo has a built-in web server which helps considerably while developing themes for Hugo. It also has a live reload and watch feature which watches for changes in your files and reloads the web page accordingly.

Run it with hugo server command.

Now open http://localhost:1313 in your browser. By default, Hugo will not show anything, because it cannot find any HTML file in the public directory.

The command to load web server with --watch option is:

$ hugo server --watch --verbose
...
...
                   | EN
+------------------+----+
  Pages            |  4
  Paginator pages  |  0
  Non-page files   |  0
  Static files     |  0
  Processed images |  0
  Aliases          |  0
  Sitemaps         |  1
  Cleaned          |  0
 
Total in 11 ms
...
...
 

Update the Home page template

Hugo looks for following directories in theme's /layout folder to search for index.html page.

  • index.html
  • _default/list.html
  • _default/single.html

It is always desirable to update the most specific template related to the content type. It is not a hard and fast rule, but a good generalization to follow.

We will first make a static page to see if our index.html page is rendered correctly.

$ vim themes/zeo/layouts/index.html
<!DOCTYPE html>
<html>
  <body>
    <p>Hello World!</p>
  </body>
</html>

Build the site and verify the results. You should see Hello World! when you open http://localhost:1313.

Building a functional Home Page

Now we will create a home page which will reflect the content of our site every time we build it.

For that, we will first create some new posts. We will display these posts as a list on the home page and on their pages, too.

Hugo has a command for generating skeleton of posts, just like it did for sites and themes.

$ hugo --verbose new post/first.md
INFO 2018/02/11 11:40:58 Using config file: /home/yash/zeo/config.yaml
INFO 2018/02/11 11:40:58 attempting to create "post/first.md" of "post" of ext ".md"
INFO 2018/02/11 11:40:58 curpath: /home/yash/zeo/archetypes/default.md
...
...
/home/yash/zeo/content/post/first.md created

The new command uses an archetype to generate the frontmatter for new posts. When we created our site, hugo created a default archetype in the /archetype folder.

$ cat archetypes/default.md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
 
---

It is a good idea to create a default archetype in the themes folder also so that users can override the theme's archetype with their archetype whenever they want.

We will create a new archetype for our posts' frontmatter and delete the default archetype/default.md.

$ rm -rf archetype/default.md
$ vim themes/zeo/archetypes/post.md
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
Description: ""
Tags: []
Categories: []
 
---

Create one more post in content/post directory.

$ hugo --verbose new post/second.md
INFO 2018/02/11 12:13:56 Using config file: /home/yash/zeo/config.yaml
INFO 2018/02/11 12:13:56 attempting to create "post/second.md" of "post" of ext ".md"
INFO 2018/02/11 12:13:56 curpath: /home/yash/zeo/themes/zeo/archetypes/post.md
...
...
/home/yash/zeo/content/post/second.md created

See the difference. Hugo used the theme's archetype for generating the frontmatter this time.

By default, Hugo does not generate posts with an empty content section. So you will need to add some content before you try to build the site.

Let's look at the content/post/first.md file, after adding content to it.

$ cat content/post/first.md
---
title: "First"
date: 2018-02-11T11:35:58+05:30
draft: true
Tags: ["first"]
Categories: ["Hugo"]
 
---
 
Hi there. My first Hugo post

Now that our posts are ready, we need to create templates to show them in a list on the home page and to show their content on separate pages for each post.

We will first edit the template for the home page that we created previously. We will then modify "single" templates which are used to generate output for a single content file. We also have "list" templates which are used to group similar type of content and render them as a list. The home page will show a list of last ten posts that we have created. Let's update its template to add this logic.

Update your home page to show your content

Now add your template code to themes/zeo/layouts/index.html.

$ vim themes/zeo/layouts/index.html $ cat !$ cat themes/zeo/layouts/index.html
<!DOCTYPE html>
<html>
  <body>
    {{ range first 10 .Data.Pages }}
    <h1>{{ .Title }}</h1>
    {{ end }}
  </body>
</html>

Hugo uses Go Template Engine. This engine scans the templates for commands that are enclosed between {{ and }}. In this template, the commands are range, first, .Data.Pages, .Title and end.

The template implies that we are going to get first 10 latest pages from our content folder and render their title as h1 heading.

range is an iterator function. Hugo treats every HTML file created as a page, so range will loop through all the pages created. Here we are instructing range to stop after first ten pages.

The end command signals the end of the range iterator. The engine loops back to the next iteration as soon as it encounters the end command. Everything between range and end will be evaluated for each iteration of the loop.

Build the website and see the changes. The homepage now shows our two posts. However, you cannot click on the posts and read their content. Let's change that too.

Linking your posts on Home Page

Let's add a link to the post's page from home page.

$ vim themes/zeo/layouts/index.html
<!DOCTYPE html>
<html>
  <body>
    {{ range first 10 .Data.Pages }}
    <h1>
      <a href="{{ .Permalink }}">{{ .Title }}</a>
    </h1>
    {{ end }}
  </body>
</html>

Build your site and see the result. The titles are now links, but when you click on them, it takes you to a page which says 404 page not found. That is expected because we have not created any template for the single pages where the content can be rendered. So Hugo could not find any template, and it did not output any HTML file. We will change that in a minute.

We want to render the posts, which are in content/post directory. That means that their section is post and their type is also post.

Hugo uses section and type information to identify the template file for each piece of content. It will first look for a template file which matches the section or type of the content. If it could not find it, then it will use _default/single.html file.

Since we do not have any other content type yet, we will just start by updating the _default/single.html file.

Remember that Hugo will use this file for every content type for which we have not created a template. However, for now, we will accept that cost as we do not have any other content type with us. We will refactor our templates to accommodate more content types, as we add more content.

Update the template file.

$ vim themes/zeo/layouts/_default/single.html
<!DOCTYPE html>
<html>
  <head>
    <title>{{ .Title }}</title>
  </head>
  <body>
    <h1>{{ .Title }}</h1>
    {{ .Content }}
  </body>
</html>

Build the site and verify the results. You will see that on clicking on first, you get the usual result, but clicking on second still produces the 404 page not found error. It is because Hugo does not generate pages with empty content. Remember I mentioned it earlier.

Now that we have our home page and posts page ready, we will build a page to list all the posts, not just the recent ten posts. This page will be accessible at http://localhost:1313/post. Currently, this page is blank because there is no template defined for it.

This page will show the listings of all the posts, so the type of this page will be a list. We will again use the default _default/list.html as we do not have any other content type with us.

Update the list file.

$ vim themes/zeo/layouts/_default/list.html
<!DOCTYPE html>
<html>
  <body>
    {{ range .Data.Pages }}
    <h1><a href="{{ .Permalink }}">{{ .Title }}</a></h1>
    {{ end }}
  </body>
</html>

Add "Date Published" to the posts

It is a standard practice to add the date on which the post was published on the blog. The front matter of our posts has a variable named date. We will use that variable to fetch the date. Our posts are using the default single template, so we will edit that file.

$ vim themes/zeo/layouts/_default/single.html
<!DOCTYPE html>
<html>
  <head>
    <title>{{ .Title }}</title>
  </head>
  <body>
    <h1>{{ .Title }}</h1>
    <h2>{{ .Date.Format "Sun, Feb 11, 2018" }}</h2>
    {{ .Content }}
  </body>
</html>

Adding top-level Pages

Okay, so now that we have our homepage, post-list page and post content pages in place, we will add a new about page at the top level of our blog, not at a sublevel like we did for posts.

Hugo uses the directory structure of the content directory to identify the structure of the blog. Let's verify that and create a new about page in the content directory.

$ vim content/about.md
---
title: "about"
description: "about this blog"
date: "2018-02-11"
---
### about me
 
Hi there, you just reached my blog.

Let's generate the site and view the results.

$ hugo --verbose
$ ls -l public/
total 36
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 12:43 about
drwxr-xr-x 3 yash hogwarts 4096 Feb 11 12:43 categories
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:20 css
-rw-r--r-- 1 yash hogwarts  187 Feb 11 12:43 index.html
-rw-r--r-- 1 yash hogwarts 1183 Feb 11 12:43 index.xml
drwxr-xr-x 2 yash hogwarts 4096 Feb 11 11:20 js
drwxr-xr-x 4 yash hogwarts 4096 Feb 11 12:43 post
-rw-r--r-- 1 yash hogwarts 1139 Feb 11 12:43 sitemap.xml
drwxr-xr-x 3 yash hogwarts 4096 Feb 11 12:43 tags

Notice that Hugo created a new directory about. This directory contains only one file index.html. The about page will be rendered from about/index.html.

If you look carefully, the about page is listed with the posts on the homepage. It is not desirable, so let's change that first.

$ vim themes/zeo/layouts/index.html
<!DOCTYPE html>
<html>
  <body>
    <h1>posts</h1>
    {{ range first 10 .Data.Pages }} {{ if eq .Type "post"}}
    <h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
    {{ end }} {{ end }}
 
    <h1>pages</h1>
    {{ range .Data.Pages }} {{ if eq .Type "page" }}
    <h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
    {{ end }} {{ end }}
  </body>
</html>

Now build the site and verify the results. The homepage now has two sections, one for posts and other for the pages. Click on the about page. You will see the page for about. Remember, I mentioned that Hugo would use the single template for each page, for which it cannot find a template file. There is still one issue. The about page shows the date also. We do not want to show the date on the about page.

There are a couple of ways to fix this. We can add an if-else statement to detect the type of the content and display date only if it is a post. However, let's use the feature provided by Hugo and create a new template type for the posts. Before we do that, let's learn to use one more template type which is partials.

Partials

In Hugo, partials are used to store the shared piece of code which repeats in more than one templates. Partials are kept in themes/zeo/layouts/partials directory. Partials can be used to override the themes presentation. End users can use them to change the default behavior of a theme. It is always a good idea to use partials as much as possible.

Header and footer of most of the posts and pages will follow a similar pattern. So they form an excellent example to be defined as a partial.

$ vim themes/zeo/layouts/partials/header.html
<!DOCTYPE html>
<html>
  <head>
    <title>{{ .Title }}</title>
  </head>
  <body>
    $ vim themes/zeo/layouts/partials/footer.html
  </body>
</html>

We can call a partial by including this path in the template

{{ partial "header.html" . }}

Update the Homepage template

Let's update our homepage template to use these partials.

$ vim themes/zeo/layouts/index.html {{ partial "header.html" . }}
 
<h1>posts</h1>
{{ range first 10 .Data.Pages }} {{ if eq .Type "post"}}
<h2><a href="{{ .Permalink }}">{{ .Title }}</a></h2>
{{ end }} {{ end }}
 
<h1>pages</h1>
{{ range .Data.Pages }} {{ if or (eq .Type "page") (eq .Type "about") }}
<h2>
  <a href="{{ .Permalink }}"
    >{{ .Type }} - {{ .Title }} - {{ .RelPermalink }}</a
  >
</h2>
{{ end }} {{ end }} {{ partial "footer.html" . }}

Update the single template

$ vim themes/zeo/layouts/_default/single.html {{ partial "header.html" . }}
 
<h1>{{ .Title }}</h1>
<h2>{{ .Date.Format "Sun, Feb 11, 2018" }}</h2>
{{ .Content }} {{ partial "footer.html" . }}

Build the website and verify the results. The title on the posts and the about page should both reflect the value from the markdown file.

Fixing the date shown on About page

Remember, we had the issue that the date was showing on the about page also. We discussed one method to solve this issue. Now I will discuss a more hugoic way of solving this issue.

We will create a new section template to fix this issue.

$ mkdir themes/zeo/layouts/post $ vim themes/zeo/layouts/post/single.html {{
partial "header.html" . }}
 
<h1>{{ .Title }}</h1>
<h2>{{ .Date.Format "Mon, Jan 2, 2006" }}</h2>
{{ .Content }} {{ partial "footer.html" . }} $ vim
themes/zeo/layouts/_default/single.html
<!DOCTYPE html>
<html>
  <head>
    <title>{{ .Title }}</title>
  </head>
  <body>
    <h1>{{ .Title }}</h1>
    {{ .Content }}
  </body>
</html>

Note that we have changed the default single template and added that logic in post's single template.

Build the website and verify the results. The about page does not show the date now, but the posts page still show the date. We can also move the list template's logic to the index.html file of post section template.

Conclusion

We have learnt, how Hugo harnesses the powerful yet simple Go template engine to create the static site generator. We also learnt about partials and their excellent utilization by Hugo in the spirit of Don't Repeat Yourself principle. Now that you know how to make themes in Hugo, go ahead and start creating new beautiful themes. Best of luck for your endaevour.