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:

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
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:
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.