..

Introduction to Hugo

This article aims to be a follow-up to the official “Getting Started” guide for Hugo that goes beyond “Install fancy theme and start posting”.

I’ll try to explain the structure of a Hugo project, show how to make a minimalistic working theme from scratch, and how to glue everything together.

I assume that a reader already knows what is Hugo and may even tried to follow the official “Quick Start”. If you haven’t, don’t worry, we’ll repeat the basic steps again through the course of this article. But first, we need to say a couple of words about …

Principles of Hugo

I started with Hugo, but quickly got overwhelmed by its complexity. I picked up a good-looking theme and tried to hack on it, but OMFG that was tough! Lots of JavaScript everywhere, weird templates, TONS of CSS… It was unbearable to change anything: things just kept falling apart. At this point, I realized that the only possibility to make things look the way I wanted them was to create a theme from scratch.

bemyak on Hugo

The first thing that you need to understand about Hugo is that all meaningful code lives in a theme. A theme in Hugo is not just about visual appearance, theme defines everything, starting from what HTML content will be inside your rendered pages to the whole structure of your site and even what Markdown extensions you can use.

It’s possible to start a new site from scratch and implement everything on the project level, provided you do not plan to reuse any of the code. But usually people either use a ready theme or make their own.

Will your Hugo site use JavaScript or CSS? Depends on the theme. Will your Hugo site support MathJax / MermaidJS / something else? If the theme decides so.

Without an installed theme you won’t even be able to render your site.

In this sense it’s better to think about Hugo as a framework for building a theme, similar to UI frameworks for building graphical applications. Hugo offers some API and asks a theme to define some behavior, but in the end, it’s pretty much up to a theme what will happen with your content and what kind of website it will generate. If you are unhappy with what you see inside the generated pages it’s only the theme to blame, nothing else.

Configurable = Unfinished.

Fully Configurable = Completely Unfinished.

Computer “Science” terms exposed

The “framework” principle has several consequences. The most important is that content and theme if not tightly coupled, at least have to be aware of each other. Themes make assumptions about content structure and often require certain metadata from content. Content in order needs to know how to be organized, which metadata it should expose, and what kind of Markdown extensions (shortcuts and render hooks) it may use.

To workaround this coupling problem themes that you see in the Hugo catalog heavily rely on configuration. It creates a problem itself: even the most basic themes have complicated code.

Good news, it doesn’t have to be so. Soon you will see how simple a theme can be.

Let’s start by refreshing the 101s of working with Hugo.

Installing Hugo

There are several ways of installing Hugo (e.g. apt and brew). I personally recommend installing it through go install:

CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest

Note that go install installs binaries to the location determined by GOPATH & GOBIN. Make sure that you have that location on your PATH.

Check that Hugo is installed by hugo version:

$ hugo version
hugo v0.120.1+extended linux/amd64 BuildDate=unknown

Hugo Project Structure

Let’s create a brand-new site

hugo new site testsite

and look what we got:

~/testsite$ tree
.
├── archetypes
│   └── default.md
├── assets
├── content
├── data
├── hugo.toml
├── i18n
├── layouts
├── static
└── themes

Before we explain what these folders are for, it’s important to note that a theme has the exact same structure as a project:

└── themes
    └── example
        ├── archetypes
        ├── assets
        ├── content
        ├── data
        ├── hugo.toml
        ├── i18n
        ├── layouts
        ├── static
        └── theme.toml

It’s not a coincidence. When building a site Hugo “merges” the contents of these two hierarchies as if the files inside were all in the same place. In case of a collision files from the project override files from a theme. Otherwise, the semantics of these directories and the files inside them are absolutely the same for a project and for a theme.

archetypes

Archetypes in Hugo terminology are templates that hugo new content uses to create new content. It’s not as important as it sounds, because you could create content in content folder manually. CLI command and archetypes just help to fill metadata (called in static site generators jargon a front matter) with default values (data, title, draft status, etc.).

assets

From documentation:

The assets directory contains global resources typically passed through an asset pipeline. This includes resources such as images, CSS, Sass, JavaScript, and TypeScript.

Resources in this directory are available when explicitly requested by templates using resources.Get function or one of its siblings (resources.ByType etc.).

Hugo publishes assets to the public when you invoke .Permalink, .RelPermalink, or .Publish. You can use .Content to inline the asset.

If you put a file (e.g. image) to the assets directory and simply reference it from content markdown it won’t work. Assets in this folder need to be programmatically requested from one of the templates and then published.

Assets in the asset directory are often passed through some sort of preprocessing (like converting sass to css).

More here.

content

The most important folder of the project, where all the markdown pages (later rendered to HTML) are stored. As I already mentioned, you can create files in this folder manually, but Hugo offers a CLI command hugo new content that helps with filling new content with default metadata. The content folder has its own conventions on the file and folder structure inside, which I will discuss later.

data

This directory is for data driven websites. The idea is that you put there raw data in JSON, YAML, TOML, or CSV format and then it becomes available in Hugo templates through .Site.Data

This is useful for websites like catalogs, shops, API documentations — anything that visualizes a huge set of homogeneous data.

Files inside the data directory should be in one of the recognized formats, putting there arbitrary files will trigger errors.

More information here.

It is worth to note that some themes use their data directory for storing theme configuration, but as I understand it’s not something good, because themes can be configured in a more explicit way through their own hugo.toml.

As a rule of thumb, if your website is not of a data-driven type, this directory should be empty.

i18n

As the name suggests it’s related to internationalization. I’m not interested in making multi-language websites, so I will skip this topic.

layouts

The most important folder of a theme. It stores templates that Hugo invokes when generating a website from your content. This is the heart of your theme. This directory has its own complex convention for organizing files and folders inside, which I will discuss later.

static

From documentation:

The static directory contains files that will be copied to the public directory when you build your site. For example: favicon.ico, robots.txt, and files that verify site ownership. Before the introduction of page bundles and asset pipelines, the static directory was also used for images, CSS, and JavaScript.

More here.

themes

As the name suggests this is the directory where you put your theme(s). Each theme must be in its own directory. Which theme to use is configured through project hugo.toml by setting theme = <theme-folder>. As I already mentioned a theme has exactly the same structure as a project.

resources

This directory is not shown in the listing above, because it’s created in the process of building the site. This directory contains cached output from asset pipelines (see above).

public

This directory is not shown in the listing above, because it’s created in the process of building the site. Here goes the generated site. You take the contents of this folder and deploy it on your server.

Sections and Page Bundles

Let’s talk now about the content directory. Usually, it is the most important directory of your project where all your posts and articles live.

The recommended way to create new content is by using CLI:

$ hugo new content posts/my-first-post.md
Content "/home/igor/testsite/content/posts/my-first-post.md" created

As you see we just used the relative path as a parameter for the hugo new content.

The directory name posts defines a section of my-first-post page. This name is arbitrary, it can be anything, for example, articles or products.

Keeping your markdown directly under the section directory is not the best idea, though. Let’s say we want to include an image in our markdown:

![Bunny](bunny.jpg)

The question is where should we place bunny.jpg? The answer is static. Obviously, it’s not perfect because page resources got detached from pages (and probably mixed together).

Hugo offers a better way of organizing page resources: Page Bundles. Let’s create a second post this way:

hugo new content posts/my-second-post/index.md
Content "/home/igor/testsite/content/posts/my-second-post/index.md" created
  • Directory name posts here is still the category.
  • The subdirectory my-second-post is a page bundle (or, more specifically leaf page bundle).
  • index.md is a predefined name, keep it as it is.

Now we can put our bunny.jpg under my-second-post:

content/
└── posts
    ├── my-first-post.md
    └── my-second-post
        ├── bunny.jpg
        └── index.md

There may be some confusion with bundles vs. categories, for example, consider:

content
├── about
│   └── index.md

This is a page bundle “about” with an empty category, not a category “about”.

Page bundles can be nested. Branch page bundles (the ones that have other page bundles inside) must have _index.md instead of index.md. But in general, you can live without nesting, so we won’t dive into the topic.

Further documentation on page bundles (and organizing your content) can be found here.

Go Templates in 60 Seconds

Hugo transforms your content Markdown to HTML using the markdown processor library called Goldmark. But rendering Markdown is only a beginning. Rendered content with metadata then substituted to one of the templates. Templates decide what to do with the rendered content and how the layout of the page will look.

Now I have good news and bad news for you.

Good news: if you’re going to install a ready theme, don’t touch anything inside and just write content, you won’t need to care about templates at all.

Bad news: the scenario from above is highly unlikely. Most probably you will need to tackle the theme code at least to some extent and therefore understand the template’s syntax.

I already mentioned the word “templates” several times. But what exactly are they? You can think of a template as a printf on steroids. Hugo uses a template engine from the standard Go library (I wish C++ had such a rich standard library😅) that uses {{ }} syntax.

To put it simply, the template is a text where everything outside of {{ }} blocks is copied to output as-is, while {{ }} blocks are substituted with some data.

The trivial example:

<title>{{ .Title }}</title>

<title> and </title> here are copied to output as-is and the text between is substituted with the property Title of an object passed to the template (it’s often referred to as dot). Hugo tries to follow the philosophy “everything is a Page”, so this will be the title of a page for which this template is invoked.

Go templates have a rich syntax that I won’t cover here. But remember that this topic is quite important and if you have zero knowledge of Go templates I’d recommend taking a short break and learning their syntax.

Packages text/template and html/template have their own documentation https://pkg.go.dev/text/template but honestly, it’s not the best.

Fortunately, Hugo has brilliant documentation on this topic. For beginners, I recommend following this manual. Just remember that it’s very Hugo-centric and doesn’t tell anything about “behind the scenes”.

For those who are curious, I wrote an example in Go playground that demonstrates how to use text/template from Go. Writing Go code may seem unrelated to the topic of Hugo as a static site generator, but I personally find it useful: you can play with the code and learn by action.

After learning basic syntax you can take a look at the rich library of functions available in Hugo templates.

Finally, as I already mentioned everything is a Page in Hugo and it is worth taking a look at Page methods.

Template Lookup Order

This peculiarly sounding topic is the 💖 of Hugo and the 🗝 for understanding how to write your own themes.

In short, when you run a build (through hugo or hugo server) Hugo walks through content folder and decides the set of HTML pages it wants to bring into existence. Some of them directly correspond to the markdown files in the content folder. Some of them (home page, section lists, taxonomies, and terms) are implicitly created by Hugo even if you haven’t asked it. If you don’t need some of these implicit pages you can exclude them through Hugo config. We’ll talk about it later.

Then for each page in the set Hugo looks for a template in the layout folder. It tries to find a template that corresponds the best for the page. Matching is based on a complex set of rules that involves about six different parameters.

I won’t explain the rule system in detail. What’s important is to remember that:

  • When several templates match the same page Hugo prefers the most specific match.
  • Kind (single or list) and Section (page’s first parent in content) are the most important parameters.
  • You don’t have to rely only on the matching system and use peculiar rules. Your templates can inspect . object and decide what to do using simple if-else branching.

Several examples:

layouts/_default/home.html ← match home page.

layouts/_default/list.html ← match any page with Kind = list. If you don’t have a more specific template for Home (like in the example above), it will be used for Home. Yes, by default Home in Hugo is just a list of pages 🤷

layouts/posts/single.html ← match a page with Kind = single in the posts section.

layouts/_default/single.html ← match any page with Kind = single. If you don’t have a more specific template for pages in posts (like in the example above), it will be used.

By the way, what to do inside these templates is completely up to you. Usually, list templates generate the list of links and single templates display content, but you can do anything.

You can read about matching rules here. This page also provides a huge list of examples.

Base Templates

When I mentioned that

for each page Hugo looks for a template in the layout folder

I actually lied 😅 simplified the truth. Hugo has a notion of base templates and pages are generated using two templates: base plus some other.

baseof.html … it is the shell from which all your pages will be rendered

Hugo documentation

The base template is chosen according to the same rule system as the regular template. In theory you can have several different bases for different types of pages. In practice, it’s something uncommon and even the complex themes usually rely only on one base.

I won’t waste your time with an example, because we’ll see it in a moment. it’s finally the time to sum everything up and make our own theme!

scratch theme

Hugo provides a command hugo new theme <theme-name> (must be executed from the root of the site) that is supposed to create a skeleton of a theme. Unfortunately, the code created this way is too bloated to be called a “skeleton”.

Let’s make a minimal working theme without using any pre-generated skeleton or a ready theme. This definitely won’t be the smallest possible Hugo theme, but close enough and the rest is a code-golf.

Start by going to the themes directory and create a subdirectory scratch (that will be the name of our theme). Then make our project use this theme by adding theme = 'scratch' in the hugo.toml. Now let’s write implementation.

1. First of all we need themes/scratch/layouts/_default/baseof.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{ if .IsHome }}
      {{ .Site.Title }}
    {{ else }}
      {{ .Title }}
    {{ end }}
</title>
  </head>
  <body>
    {{ block "main" . }}
    {{ end }}
  </body>
</html>

This will be used as the base template of all HTML pages on our site.

2. I like to define a separate template for the home page (although it’s not necessary, home by default would use list.html template) themes/scratch/layouts/_default/home.html :

{{ define "main" }}
    {{ .Content }}
    {{ range .Pages }}
      <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
    {{ end }}
{{ end }}

By the way, if you are curious where does Home {{ .Content }} comes from, the answer is _index.md (if such file exists) in the root of the content folder.

3. Let’s make themes/scratch/layouts/_default/single.html template for regular posts:

{{ define "main" }}
    <h1>{{ .Title }}</h1>
    {{ .Content }}
{{ end }}

4. Let’s make themes/scratch/layouts/_default/list.html template for the sections:

{{ define "main" }}
  <h1>{{ .Title }}</h1>
  {{ .Content }}
  {{ range .Pages }}
    <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
  {{ end }}
{{ end }}

5. I’m not going to use taxonomies (tags and lists of tags) in my site and make templates for them (although they should work by using list.html), so I disabled generating pages for them in my site hugo.toml:

disableKinds = ['taxonomy', 'term']

That’s all! Now we can render our site! 🎉

I assume that you followed the examples from above and created the following content:

content/
├── about
│   └── index.md
├── _index.md
└── posts
    ├── my-first-post.md
    └── my-second-post
        ├── bunny.jpg
        └── index.md

Run hugo server -D and navigate to http://localhost:1313/ You should see something like this:

Home Page Posts Bunny

You can find the code on GitHub.

Conclusion

If you reached this point, congratulations! 🎉 Now you know the basics of writing Hugo themes and have a bloat-free theme that you can use as a starting point for your site.

If you find this article useful, you can buy me a coffee.