Making a Hugo site

After using many different tech stacks to make this blog1 I’ve settled on Hugo with a custom theme and deployed on GitHub pages. It was refreshingly straightforward! For anyone interested as to the process, here it is:

  1. Install Hugo: https://gohugo.io/installation/
    1. Verify you’ve installed it with hugo version
  2. Run:
hugo new site my-app-name
cd my-app-name
git init
hugo server

This should serve a site at localhost:1313, probably giving a 404.

  1. Now let’s make a custom theme (based on this blog post):
hugo new theme exampleTheme
  1. Change the root hugo.toml to look like:
baseURL = 'https://danielmillward.com/'
languageCode = 'en-us'
title = 'Daniel Millward'
theme = "exampleTheme"

We now have the framework we need to develop our custom site.

Making a Custom Theme in Hugo

Hugo uses GoLang’s style of templating, i.e. Having HTML interspersed with {{}} blocks containing variables and functions. The templates you write will live in themes/exampleTheme/layouts/.

By default, Hugo generates two subfolders in themes/exampleTheme/layouts:

  1. _default: This is where important big templates live, e.g. the base template, the default list template (explained later), the post template, etc.
  2. partials: This is where common “building blocks” live, e.g. the header/footer and nav bar.

The main “master” page template is defined at _default/baseof.html. The default one Hugo generates has references to some partials (head and header) as well as a “main” block.

The template for your website’s home page layout is at _default/index.html. You’ll have to make this one. It’ll look something like this:

{{ define "main" }}

<div class="mainContainer">
    <div class="heropicContainer">
        <img src="/images/Ghibli-Portrait-2.webp">
    </div>
    <div class="mainText">
        <p>Here's some text!</p>
      </div>
</div>

{{ end }}

The {{ define "main }} and {{end}} blocks tell Hugo to put this in the block called “main” in baseof.html.

Let’s look at a partial component, partials/header.html:

<div id="nav-border" class="navContainer">
    <h2 class="nameLinkH2">
        <a href="/" class="nameLink">Daniel Millward</a>
    </h2>
    {{/* Need a reference to the page object while looping through the menus */}}
    {{ $currentPage := . }}
    <nav id="nav">
        {{ range .Site.Menus.main.ByWeight }}
        <a href="{{ .URL }}" class="header-link" {{if eq $currentPage.Path .URL}}style="font-weight: bold"{{end}}>
            {{ $text := print .Name | safeHTML }}
            {{ $text }}
        </a>  
        {{ end }}

</div>

This shows off a lot more of Hugo’s templating functionality. You can see:

A big thing here is the . operator (e.g. calling .Name and setting $currentPage to .). The dot is the current context: Think of it as calling this. as a shorthand. By default, the context is (I think?) the Page object. This has a ton of methods you can use to get information about the current page, e.g. the title.

The context can change: In the {{range .Site.Menus.main.ByWeight}} block the context is each of the menu items.

The .Site object can be changed for our needs by changing the configuration files. For this example, the Menus object was set by appending this to the root hugo.toml file:

[menu]
  [[menu.main]]
    name = "Posts"
    url = "/posts"
    weight = 1
  [[menu.main]]
    name = "Categories"
    url = "/categories"
    weight = 2

Notice we called the .ByWeight function in header.html: Hugo lets us sort them by the weight attribute we’ve set.

In addition to the index.html page, Hugo automatically generates pages based on the content folder:

The folders with an _index.md file are called Sections.

So what’s a “list” page? It’s an automatically generated page that can list the pages in its directory. How does it know how to generate it? It uses the first matching template it finds in a set lookup order. For example, if you made a folder content/posts and that had some regular pages, you could put the listing template at themes/exampleTheme/layouts/posts/list.html. It could look something like:

{{ define "main" }}
<div class="postList">
  {{ .Content}}
  {{ range (where .Site.RegularPages "Type" "in" (slice "posts")).GroupByDate "2006" }}
  <div class="categoryBlock">
    <h2 class="postYear">{{ .Key }}</h2>
  {{ range .Pages }}
    {{ partial "postBlock.html" .}}
    {{- end -}}
  {{ end }}
  </div>
</div>
{{ end }}

Some new things here:

To set what an individual regular page (e.g. a blog post) looks like, change the themes/exampleTheme/layouts/_default/single.html template. You can get data from the frontmatter of whatever markdown file is being rendered. Common variables are .Date and .Title. You reference the content itself with .Content.

We’re pretty close to finishing the blog! If you have some images/other static stuff you want to reference, put them in the root static folder. If you keep the default partials/head/css.html then your main css file will be at themes/exampleTheme/assets/css/main.css.

It’s best to add a .gitignore that has the public directory, since that’s where Hugo builds the static site.

Specifically for writing Latex: In the partials/head.html template, add this at the bottom:

<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
      tex2jax: {
        inlineMath: [ ['$','$'], ["\\(","\\)"] ],
        processEscapes: true
      }
    });
  </script>
      
  <script type="text/javascript"
          src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
  </script>

That’s pretty much it! The deployment is very straightforward: just upload your repo to GitHub and add a GitHub Actions workflow. The process is documented thoroughly on Hugo’s website.

If you want to add a custom URL, that’s also straightforward: GitHub has an article on this. Note that for this to work you’ll need your GitHub repo to be called YourUserName.github.io, otherwise Hugo gets confused on where to link to your static files.


  1. Including pure HTML/CSS, Hugo with a default theme, Express, Nextjs, Go server on Oracle Cloud, Go server on a local Windows 11 machine, and Go with HTMX on the same Windows machine ↩︎