Writing a custom Hugo theme

I am currently in the process of creating this Hugo blog and I decided to create my own theme in order to learn Hugo internals. While testing this theme I of course needed content, so I thought “why not document the process of creating the theme as the first post in the blog?”. This is that post!

Initial theme creation and set-up

First steps

After creating a base Hugo site with: -

hugo new site my-site

a new blank theme can be created with: -

cd my-site
hugo new theme my-theme

This will create the theme’s skeleton in my-site/themes/my-theme.

Modifying the main HTML layout

The theme can be thought of as a hierarchy where the top-most element represents a HTML page that will be rendered. Within this element, child nodes can be included to render specific parts of the page.

The overall HTML page template is stored in themes/my-theme/layouts/_default/baseof.html.

<!DOCTYPE html>
<html lang="{{ $.Site.LanguageCode | default "en" }}">
    {{- partial "head.html" . -}}
    <body>
        {{- partial "header.html" . -}}
        <div id="content">
        {{- block "main" . }}{{- end }}
        </div>
        {{- partial "footer.html" . -}}
    </body>
</html>

This template file contains the base structure of the HTML page and also includes “partials”. Partials are these other child elements of the hierarchy that can be included at certain points to render specific content. For example, the head.html partial is included where the HTML <head> section should be, therefore this partial is responsible for rendering this section. We’ll see how to modify this next.

The only change I’ve made to the template above is to add the lang attribute. This attribute’s value is obtained from the site’s global settings ($.Site.LanguageCode) or defaults to “en” if that does not exist.

Partials

Partials are templates representing specific elements within the page. For example the head.html partial included in the skeleton is responsible for rendering the HTML <head> section.

Here’s how I modified the default version in themes/my-theme/layouts/partials/head.html: -

<head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{ block "title" . }}{{ with .Params.Title }}{{ . }} | {{ end }}{{ .Site.Title }}{{ end }}</title>
</head>

I added some <meta> and <title> tags. For the title, the block “title” is included. Within this block, the default content .Params.Title plus a “|” plus the .Site.Title is included. This will be used if the “title” block does not have any content itself. This way of including a block but also providing default content is common. The with function takes the value of the argument (.Params.Title) and uses that value as the current context, which is denoted by a period. This context is output using {{ . }} and then a literal " | " is appended. The next {{ end }} terminates the with block. Ready for the .Site.Title to be output.

List page template

The next thing I wanted to do was to be able to render lists, such as a list of posts. This is done by default by the themes/my-theme/layouts/_default/list.html template.

{{ define "main" }}
<main>
    <article>
        <header>
            <h1>{{ .Title }}</h1>
        </header>
        {{ .Content }}
    </article>
    <ul>
        {{ range .Data.Pages }}
        <li>
            <a href="{{ .Permalink }}">{{ .Date.Format "Mon Jan 2, 2006" }} | {{ .Title }}</a>
        </li>
        {{ end }}
    </ul>
</main>
{{ end }}

This template is pretty simple; it outputs the .Title which is the title of the list (e.g. “Posts”) and then any content which is associated with this section. It then loops through all pages in .Data.Pages using the range function and for each outputs an <li> containing a link showing the page date plus “|” plus the page .Title.

Single page template

Now that we have a page which can list other pages and which includes a link to each page, we need a template which can render a single page. By default, the template at themes/my-theme/layouts/_default/single.html is used.

{{ define "main" }}
<section id="main">
  <h1 id="title">{{ .Title }}</h1>
  <div>
        <article id="content">
           {{ .Content }}
        </article>
  </div>
</section>
<aside id="meta">
    <div>
    <section>
      <h4 id="date"> {{ .Date.Format "Mon Jan 2, 2006" }} </h4>
      <h5 id="wordcount"> {{ .WordCount }} Words </h5>
    </section>
    {{ with .Params.topics }}
    <ul id="topics">
      {{ range . }}
        <li><a href="{{ "topics/" | absURL}}{{ . | urlize }}">{{ . }}</a> </li>
      {{ end }}
    </ul>
    {{ end }}
    {{ with .Params.tags }}
    <ul id="tags">
      {{ range . }}
        <li> <a href="{{ "tags/" | absURL }}{{ . | urlize }}">{{ . }}</a> </li>
      {{ end }}
    </ul>
    {{ end }}
    </div>
    <div>
        {{ with .PrevInSection }}
          <a class="previous" href="{{.Permalink}}"> {{.Title}}</a>
        {{ end }}
        {{ with .NextInSection }}
          <a class="next" href="{{.Permalink}}"> {{.Title}}</a>
        {{ end }}
    </div>
</aside>
{{ end }}

There’s slightly more to this template that the list template, so let’s break it down one step at a time.

Page title and content

<section id="main">
  <h1 id="title">{{ .Title }}</h1>
  <div>
        <article id="content">
           {{ .Content }}
        </article>
  </div>
</section>

This is the simplest section; it simply outputs the page .Title and .Content. Content is the HTML which Hugo has rendered from the page’s Markdown source.

Date, word count, etc.

<section>
    <h4 id="date"> {{ .Date.Format "Mon Jan 2, 2006" }} </h4>
    <h5 id="wordcount"> {{ .WordCount }} Words </h5>
</section>

This section outputs a formatted date and the page’s .WordCount. The date comes from the page’s front matter, which is metadata associated with the page in the page’s Markdown source. The .WordCount is an automatically generated variable which contains the total number of words in the page.

List of topics and tags

{{ with .Params.tags }}
<ul id="tags">
    {{ range . }}
    <li> <a href="{{ "tags/" | absURL }}{{ . | urlize }}">{{ . }}</a> </li>
    {{ end }}
</ul>
{{ end }}

This section first uses the .Params.tags list from the page’s front matter. This is a list of tags which are assigned to this page. Remember, using with assigns the value of the argument to the current context . within the block.

The range function then loops over each tag in the list and builds a link to the associated tag list page for that tag. The link is created by piping “tags/” to the absurl function which returns an absolute URL, then the tag name itself is appended to this after being piped through the urlize function which sanitises the tag name appropriately. The link content is simply set to the tag name and then the link is enclosed in an <li> tag.

Why not just use {{ range .Params.tags }} however? We want to output an <li> for each tag, however we want to output the opening and closing <ul> tags before and after this loop. We don’t want to output these tags however if the tags list is empty. with is a convenient method for handling this situation; if .Params.tags is null then the whole with block will be ignored.

The page topics are also listed in exactly the same way.

<div>
    {{ with .PrevInSection }}
        <a class="previous" href="{{.Permalink}}"> {{.Title}}</a>
    {{ end }}
    {{ with .NextInSection }}
        <a class="next" href="{{.Permalink}}"> {{.Title}}</a>
    {{ end }}
</div>

This section simply outputs a link for the .PrevInSection and .NextInSection pages if they exist. For each, a link to the page’s .Permalink is created using the page’s .Title as the link content.

Adding Bootstrap

Static assets

Firstly I downloaded and extracted the Bootstrap v4 distribution, then placed the css and js directories in themes/my-theme/static/. As Bootstrap also needs jQuery I downloaded that too and placed that in the js directory.

In order to load these files the head.html template needs to be modified: -

<head>
  <meta charset="utf-8">
  <title>{{ block "title" . }}{{ with .Params.Title }}{{ . }} | {{ end }}{{ .Site.Title }}{{ end }}</title>
  <link rel="stylesheet" href="{{ "css/bootstrap.min.css" | absURL }}" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk">
  <script src="{{ "js/jquery-3.5.1.min.js" | absURL }}"></script>
  <script src="{{ "js/bootstrap.min.js" | absURL }}" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI"></script>
</head>

Here the style sheet and JavaScript are loaded from the theme’s static directory.

Basic Bootstrap mark-up

For Bootstrap, all page content should be wrapped in a <div> with a class of “container” or “container-fluid”. To do this, I modified the themes/my-theme/layouts/_default/baseof.html: -

{{- partial "header.html" . -}}
<div id="content" class="container">
{{- block "main" . }}{{- end }}

For the single page template, I decided to create a responsive layout where the content is 9-column and the post metadata is 3-column at medium screen size or greater, and full-width on smaller sizes. To do this I modified the single.html template: -

<div class="row">
  <section id="main" class="col-md-9">
    <h1 id="title">{{ .Title }}</h1>
...

The previous content is now wrapped in a <div> with the “row” class. Then the <section> is given the appropriate “col” class.

...
  </section>
  <aside id="meta" class="col">
      <div>
...

The <aside> is also given a “col” class, causing it to flow alongside the main content when appropriate.

I also updated the list.html template to improve the look of the list of pages: -

    <div class="list-group">
        {{ range .Data.Pages }}
        <a class="list-group-item list-group-item-action" href="{{ .Permalink }}">{{ .Date.Format "Mon Jan 2, 2006" }} | {{ .Title }}</a>
        {{ end }}
    </div>

Homepage

The site’s home page template is stored in themes/my-theme/layouts/index.html. I added some simple content using Bootstrap classes to make it look OK: -

{{ define "main" }}
<div class="jumbotron">
    <h1>Welcome to {{ $.Site.Title }}!</h1>
    <p>
        This is my personal blog containing articles related to programming,
        operating systems (mainly Linux), Emacs, etc.
    </p>
    <a class="btn btn-primary" href="/post/">View posts!</a>
</div>
{{ end }}

Header (navbar) and menus

I wanted my site to have a top navigation bar which contains the site name (which is also a link to the home page), links to other pages and a drop-down list of post categories.

In order to achieve this, I used a Bootstrap navbar as well as Hugo menus.

I first defined my navbar’s menu in config.toml: -

[menu]

  [[menu.navbar]]
    identifier = "posts"
    name = "All posts"
    pre = "<i class='fas fa-envelope'></i>"
    url = "/post/"
    weight = 100

  [[menu.navbar]]
    identifier = "about"
    name = "About"
    pre = "<i class='fas fa-user'></i>"
    url = "/about/"
    weight = 101

The template used for the site’s header is, unsurprisingly, themes/my-theme/layouts/partials/header.html. There’s a fair bit in here so I’ll describe each important section in turn.

<div class="navbar navbar-expand-lg navbar-dark bg-dark">
    <a class="navbar-brand" href="/">{{ $.Site.Title }}</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
...

This defines the main navbar element itself and adds the $.Site.Title as the first element with a link to the home page.

...
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
        <ul class="navbar-nav mr-auto">
...

Here I start defining the navbar items.

...
            {{ range .Site.Menus.navbar }}
                {{ if .HasChildren }}
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown-{{ .Identifier }}" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            {{ .Pre }}
                            <span>{{ .Name }}</span>
                        </a>
                        <div class="dropdown-menu" aria-labelledby="dropdown-{{ .Identifier  }}">
                            {{ range .Children }}
                                <a class="dropdown-item" href="{{ .URL }}">
                                    {{ .Pre }}
                                    <span>{{ .Name }}</span>
                                </a>
                            {{ end }}
                        </div>
                    </li>
                {{ else }}
                    <li class="nav-item">
                        <a class="nav-link" href="{{ .URL }}">
                            {{ .Pre }}
                            <span>{{ .Name }}</span>
                        </a>
                    </li>
                {{ end }}
            {{ end }}
...

Here is where I process the “navbar” menu defined in config.toml. For each item, I either display a navbar item or a drop-down item containing the actual items depending on if the menu item has nested children. For each item, I output a link pointing to the item’s .URL and then display .Pre and .Name as the item’s content. .Pre allows me to include optional HTML in front of the menu item, which I’m using here to display an icon.

            <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle" href="#" id="dropdown-categories" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                    Post categories
                </a>
                <div class="dropdown-menu" aria-labelledby="dropdown-categories">
                    {{ range $key, $value := $.Site.Taxonomies.categories }}
                    <a class="dropdown-item" href="{{ "categories/" | absURL }}{{ $key }}">{{ $key | humanize }}</a>
                    {{ end }}
                </div>
            </li>
        </ul>
    </div>
</div>

Finally I display a drop-down list of categories. I loop through each item in $.Site.Taxonomies.categories and extract the $key and $value. The $key is the actual identifier of the category taxonomy term and is used to form the URL and to display the name. The name is piped to the humanize function for display purposes.

Icons on the right-hand side

I also wanted some icons on the right-hand side of the navbar for things like RSS and Twitter links. To do this, I defined a second menu in config.toml called navbar_icons and modified the header.html template so that this menu is output after the first menu: -

...
    <ul class="navbar-nav ml-auto">
        {{ range .Site.Menus.navbar_icons }}
            <li class="nav-item">
                <a class="nav-link" href="{{ .URL }}" target="_blank" aria-label="{{ .Name }}">
                    {{ .Pre }}
                    <span class="d-lg-none">{{ .Name }}</span>
                </a>
            </li>
        {{ end }}
    </ul>
...

I gave this navbar a Bootstrap class of ml-auto so that the left margin is automatically sized, meaning that the content will be pushed to the far right of the container.

Alternative list templates

I wanted to customise the list page template based on the type of list being displayed. For example, when viewing a list of posts in a certain category, I wanted the title to read something like “Posts with the category ‘programming’”. To do this, I added a more specific list template specifically for categories in themes/my-theme/layouts/_default/category.html: -

{{ define "main" }}
<main>
    <article>
        <header>
            <h1>Posts with the category &quot;{{ .Title }}&quot;</h1>
        </header>
        {{ .Content }}
    </article>
    <div class="row">
    {{ range .Data.Pages }}
        <div class="col-sm-12 col-md-6 col-lg-4">
            <div class="card">
                {{ with .Params.images }}
                    {{ $coverLink := index (.) 0 }}
                    {{ with $coverLink }}
                        <img class="card-img-top" src="/{{ . }}" />
                    {{ end }}
                {{ end }}
                <div class="card-body">
                    <h5 class="card-title">{{ .Title }}</h5>
                    <p class="card-text">{{ .Date.Format "Mon Jan 2, 2006" }} | Reading time ~ {{ .ReadingTime }} minutes</p>
                    <a class="card-link" href="{{ .Permalink }}">View</a>
                </div>
            </div>
        </div>
    {{ end }}
    </div>
</main>
{{ end }}

I also created another almost identical template themes/my-theme/layouts/_default/tag.html, this time for listing posts that have a specific tag.

More partials and DRY

DRY, what’s that? Don’t repeat yourself. Don’t Repeat Yourself? Yes, don’t repeat yourself.

After creating a few different templates I realised that some functionality is being duplicated amongst them. For example, listing posts is now done in category.html, tag.html and list.html. This isn’t ideal as in this case I want both lists to appear in the same way and having two copies of the code is both wasteful and impractical: I’d have to remember to change the other if I made a change to the first.

To solve this problem, Hugo allows us to create “partials”, which are small template units which can be included in multiple templates as necessary.

We’ve actually been using partials already; for example, the header.html and head.html files in the theme’s layout/partials are partials, as the containing directory name might suggest.

To solve the above problem, I created a new partial themes/my-theme/layouts/partials/list_of_posts.html: -

<div class="row">
    {{ range . }}
    <div class="col-sm-12 col-md-6 col-lg-4">
        <div class="card">
            <div class="card-body">
                <h5 class="card-title">{{ .Title }}</h5>
                <p class="card-text">{{ .Date.Format "Mon Jan 2, 2006" }} | Reading time ~ {{ .ReadingTime }} minutes</p>
                <a class="card-link" href="{{ .Permalink }}">View</a>
            </div>
        </div>
    </div>
    {{ end }}
</div>

Then I modified the templates that need to use if, for example the new category.html: -

{{ define "main" }}
<main>
    <article>
        <header>
            <h1>Posts with the category &quot;{{ .Title }}&quot;</h1>
        </header>
        {{ .Content }}
    </article>
    {{ partial "list_of_posts.html" .Data.Pages }}
</main>
{{ end }}

This template is now much simpler! To include the new partial, I can use the partial function together with the name of the partial template and the context. The context in this case is what we were previously looping through: .Data.Pages. This will be passed to the partial as the current context; when looking at the code for the partial above we can see that we’re now using range on the current context . rather than directly on .Data.Pages. This allows us to use the partial to list anything we want, not just .Data.Pages, depending on what we pass as the second argument to the partial function.

I really like RSS and it was very important to me when building this blog to allow each list to be consumable as RSS. Fortunately Hugo makes this very simple! I simply added the following to head.html: -

...
{{ range .AlternativeOutputFormats -}}
    {{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end -}}
...

This causes a <link> tag to be generated for each alternative output format defined for the page. By default RSS is the only alternative format. By including this, anyone visiting a page on my site can discover an RSS feed associated with it.

Of course it’s also nice to include a direct link to the overall RSS feed, so I added a link to the navbar pointing to /post/index.xml, which is an automatically generated RSS feed for all of the posts.

Header metadata (OpenGraph and Twitter Cards)

When blogging it’s not only important for people to be able to read the content but also for other services to be able to read and parse the relevant metadata easily. Systems such as OpenGraph and Twitter Cards exist for just such a purpose.

Integrating these systems into a blog site can be challenging, but luckily Hugo comes with in-built support! All that needs to be done is to add the following template function calls to head.html: -

...
  <meta charset="utf-8">
  {{ template "_internal/opengraph.html" . }}
  {{ template "_internal/twitter_cards.html" . }}
  <title>{{ block "title" . }}{{ with .Params.Title }}{{ . }} | {{ end }}{{ .Site.Title }}{{ end }}</title>
...

The next step was to allow posts to have a “featured” or “cover” image. This is not as simple as just including an image at the beginning of the post because I also want it to be available to the OpenGraph and Twitter Cards templates so that this image will be used when somebody shares a post.

I decided on a fairly simple method for doing this. The front-matter in Hugo posts can contain a list of images, and these will be picked up automatically by the OpenGraph and Twitter Cards templates. I decided to consider the first image in this list as the featured/cover image and therefore I wrote a partial to allow me to retrieve this. I created themes/my-theme/layouts/partials/funcs/get_cover_image.html: -

{{ $coverLink := false }}
{{ with .Params.images }}
    {{ $coverLink = index . 0 }}
{{ end }}
{{ return $coverLink }}

This is an example of returning a value from a partial. First we set the default value to false. Then we take the first element from the images list in the page .Params (if it exists). Then we return the value, which will either be false or the path to the first image.

This can then be used in a template such as themes/my-theme/layouts/_default/list.html: -

...
    <div class="row">
        {{ with partial "funcs/get_cover_image.html" . }}
            <div class="col-4">
                <img class="img-fluid" src="/{{ . }}" />
            </div>
        {{ end }}
        <div class="col">
            <h1 id="title">{{ .Title }}</h1>
        </div>
    </div>
...

Here we use with to get the returned value from the new partial (passing in the current page as the context) and then use the result to set the src attribute of an image tag.

When viewing a post which has a featured/cover image, we now see the image and also see HTML like the following: -

<meta property="og:image" content="https://example.com/post/test-post/cover.png" />
<meta name="twitter:image" content="https://example.com/post/test-post/cover.png"/>

Final tweaks

Syntax highlighting

If this post is anything to go by, my blog posts are going to contain a fair few code snippets. Because of that I wanted to make sure that code blocks are displayed nicely.

Hugo comes with support for syntax highlighting and a colour style sheet can be generated using this command: -

hugo gen chromastyles --style dracula > themes/my-theme/static/css/syntax.css

This will produce a syntax highlighting style sheet using the Dracula Theme; this is my favourite but there are many others from which to choose. This style sheet then needs to be added to head.html: -

  <link rel="stylesheet" href="{{ "css/syntax.css" | absURL }}">

Finally, pygmentsUseClasses = true needs to be added to the site’s config.toml in the root directory.

Table of contents in single template

Adding a table of contents is as simple as adding the following to single.html: -

{{ .TableOfContents }}

However I wanted to use Bootstrap “nav” classes in this list instead. I have yet to find a good way of modifying this template in Hugo, so instead I resorted to modifying the HTML itself that is stored in .TableOfContents like so: -

{{ .TableOfContents | replaceRE "<ul>" "<ul class=\"nav\">" | replaceRE "<li>" "<li class=\"nav-link\">" | safeHTML }}

Here I’m replacing every <ul> with <ul class="nav"> and every <li> with <li class="nav-link">. The result of this will be a string containing HTML, but Hugo’s template engine will always escape HTML when outputting it. To mark this string as raw HTML that should be output directly (without escaping) I piped it through the safeHTML function.

Latest posts on the homepage

Finally, I wanted to add something of use to the homepage; I decided to add a list of the 9 latest posts. To do so I modified the themes/my-theme/layouts/index.html template, adding the following: -

<div class="card">
    <div class="card-body">
        <h2 class="card-title">Most recent posts</h2>
        <div class="card-text">
            {{ with first 9 (where .Site.RegularPages.ByPublishDate.Reverse "Type" "in" .Site.Params.MainSections) }}
                {{ partial "list_of_posts.html" . }}
            {{ end }}
        </div>
    </div>
</div>

Here I use the list_of_posts.html partial as with the other templates. But for the source of posts I take the .Site.RegularPages list, order it by publish date, then reverse the list so that the newest posts come first. I use the where function to filter the list so that I only get posts within the main section. Finally I use the first function to return only the first 9 posts from the resulting list.