OVERHEAD

Starting a website with Hugo

Deciphering Hugo

Welcome to Hugo!

You’ll learn to hate love it! First, let’s get the basics laid out.

Setting things up

This tutorial assumes a Linux-based OS, see the official Quick Start guide for how to setup Hugo on your OS.

For demonstration purposes, I’ll assume the website is titled “Basic”:

  • Find a place for your website, I tend to put my websites under Public/ to keep them out the way.
  • cd into Public/, then run hugo new site Basic to create your website and its directory structure.
  • cd into Public/Basic/.

The website preview is left until everything is configured, so don’t run hugo server yet.

Directory structure

Each directory has a particular role in how Hugo generates your website, however, you only need to care about some of them for basic functionality. That being said, here’s a rough overview of them all:

  • archetypes: Defines the default markdown for posts & post bundles created with hugo new content.
  • assets: Contains globally accessible content intended for processing, including images, preprocessor stylesheets, and templates.
  • content: Contains the markdown files for all pages, including sections, taxonomies, etc.
  • data: Contains files with data intended to be parsed by functions, you won’t need to worry about it.
  • layouts: Contains the default HTML content, partials, and shortcodes that will consume the markdown files under content/.
  • public: The output directory for hugo or hugo build. It does NOT clear itself when builds are ran repeatedly, so make sure it’s empty before rebuilding.
  • resources: Contains processed resources, for internal use.
  • static: Contains static content not intended to be processed, including normal stylesheets, favicons, etc.
  • themes: Contains themes, which themselves contain duplicate directory structures that can be easily overridden by content defined outside the theme in the usual directories.

In reality, the two directories you’ll be primarily working with are content and layout, since they form the basis of the website content, and how Hugo processes that content. The other directories are important, but tend to be updated less frequently by their nature (the likely exceptions being stylesheets).

From this point, you’ll be building up the website into a minimal, functional demo that you’ll be able to be preview by the end.

Archetypes

There are three different types of archetypes you should be aware of (see the Page bundles documentation for more information):

  1. The default archetype: archetypes/default.md.
  2. Post/Leaf bundles: archetypes/post-bundle/index.md or archetypes/section/index.md.
    • Example command: hugo new content blog/post.
  3. Branch bundles: archetypes/taxonomy/_index.md.
    • Example command: hugo new content content/tags.

Pay close attention to when the underscore prefix is included or not in the filename.

A notable feature of bundles is that they can have page resources, where extra files can be added such as header images, stylesheet overrides, etc (e.g. content/blog/post/override.css), however, only Leaf bundles support nesting the resources under another directory (e.g. content/blog/post/images/header.png).

This is because Branch bundles support nesting Leaf bundles and even other Branch bundles under themselves, so content/branch1/branch2/images/ would treat images/ as another bundle, and it’d additionally treat it as invalid since it has no index.md nor _index.md file.

Here’s an example archetype file to get you started:

Markdown for archetypes/default.md.

---
title: "{{ replace .Name "-" " " | title }}"
description: "Default description..."
tags:
series:
date: {{ .Date }}
draft: true
---

Content goes here.

Don’t worry about the stuff inside the {{ ... }} template blocks for now, you’ll start to learn how they work in the Partials section.

Defining archetypes for default.md post-bundle/, blog/, tags/, and series/ is recommended so you don’t need to manually define page kinds with the -k option (e.g. hugo new content -k post-bundle blog/post). Notice that you don’t need to include content/ in the path if you’re defining a page under a section, taxonomy, or term; you only need to include it if you’re defining a page such as content/about.md.

Content

Here’s where the fun begins!

  • content/_index.md defines the markdown content of the homepage. It’s completely optional, and usually unnecessary unless you want to include any of the markdown content or front matter in your homepage. It’s worth noting that resources stored under content/ are considered page resources for the homepage (e.g. content/header.png).
  • content/*.md defines posts not under any section, such as an about page at content/about.md. Do NOT create post bundles under content/ since, for example, content/about/ will be considered a section instead of a post bundle.
  • content/directory/ can define either a section or a taxonomy, depending on whether the directory has the name of a taxonomy defined in Hugo’s config (which will be explained later).
  • content/directory/_index.md defines the markdown content of the section or taxonomy. It’s typically used for setting page variables like .Title or .Description.
  • content/taxonomy/term/_index.md defines the markdown content of a term in a taxonomy (e.g. tags/first/ is a term under the taxonomy tags/). It’s typically used for setting page variables.

Hugo is case-sensitive, even if HTTP/DNS is not, so if you create content/Blog/ the .Permalink URL will be output as https://basic.tld/Blog.

For this next part, you’ll be creating various markdown files for an about page, a section named “Blog”, a post named “My First Post”, and a tag named “First”. Remember that you can use hugo new content to do this (e.g. hugo new content content/example.md or hugo new content blog/_index.md).

Markdown for content/about.md.

---
title: "About"
description: "An explanation of what this place is"
date: 2023-05-10T12:00:00+00:00
---

# About

This is my website!

Markdown for content/blog/_index.md.

---
title: "Blog"
description: "All blog posts"
---

Markdown for content/blog/my-first-post/index.md.

---
title: "First blog post"
description: "What's a better way to start a blog?"
tags:
- First
date: 2023-05-10T12:30:00+00:00
---

# First blog post

Yay!

Markdown for content/tags/_index.md.

---
title: "Tags"
description: "All tags"
---

Markdown for content/tags/first/_index.md.

---
title: "First"
description: "First post!"
---

Layout

Partials

Before we get to the HTML files, let’s create some partials to act as building blocks:

HTML for layouts/partials/header.html.

<!DOCTYPE html>
<html lang="en">
<head>
	<title>{{ .Site.Title }}</title>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>

<header>
	<h1>{{ .Site.Title }}</h1>
</header>

Notice that </body> and </html> aren’t present in the header partial, they’re instead included in the upcoming footer partial.

Hugo uses Go’s template language for inserting content into HTML & Markdown files, like for inserting the variable .Site.Title into the header partial, which can be both a powerful tool and a moderate headache to deal with.

HTML for layouts/partials/footer.html.

<header></header>

<footer>
	<p>&copy; {{ .Site.Title }} {{ now.Year }} • {{ .Site.Copyright }}</p>
	<p>&#8594; <a href="/about" title="{{ with .GetPage "/about" }}{{ .Description }}{{ end }}">About {{ .Site.Title }}</a></p>
</footer>

</body>
</html>

There’s a bunch more template stuff in this footer partial compared to the header partial, so I’ll try to break it down:

  • .Site.Title (variable): Takes the value of title from the Hugo config file (which I’ll explain later). Similar story for .Site.Copyright.
  • now.Year (variable & function): Takes the current date (now), and formats it as a four digit year number (.Year).
  • with (function): If its variable or function is absent, empty, or returns false, its content is excluded. Unlike the if function, which behaves similarly, with rebinds its content’s context to the variable or function it’s acting on.
  • {{ with .GetPage "/about" }} content {{ end }}: “content” will be inserted if /about exists.
  • {{ with .GetPage "/about" }}{{ .Description }}{{ end }}: The context is rebound to /about, so .Description will be the about page’s description, and not the current page’s description.

Managing context rebinding for functions like with or range is tricky to summarise, so don’t worry about it for now; the next blog post will dive deeper into how it works.

Layout Content

Now, let’s use those partials in the homepage:

HTML for layouts/index.html.

{{ partial "header.html" . }}

{{ partial "footer.html" . }}

The dot defines the partial’s context. You don’t need to worry about the details, I’ll cover this in the next blog post, but, don’t leave it out.

Other pages, like sections, taxonomies, terms, and single (blog posts) are defined under layouts/_default/, however, we’re only going to define single.html since that’s all we need for a functioning preview of a post.

HTML for layouts/_default/single.html.

{{ partial "header.html" . }}

<header><h2>{{ .Title }}</h2></header>

<main>
	<article>
{{ .Content }}
	</article>
</main>

{{ partial "footer.html" . }}

.Content is the content of the markdown post you created.

To define term pages, you can either create layouts/_default/term.html to define a layout for all terms, or, using the taxonomy “Tags” as an example, you can create layouts/_default/tag.html (notice how it’s singular, I’ll get to that later).

Website Config

Since I’ve put off explaining some of the weird details, like how Hugo determines the difference between sections or taxonomies, or why tag.html is singular, let’s talk about Hugo’s config file:

Example Hugo config at Public/Basic/config.toml.

baseURL = 'https://basic.tld'
title = 'Basic'
copyright = 'All rights reserved unless stated otherwise.'
languageCode = 'en'

[params]
keywords = ['Basic, Simple, Starter, Hugo']
description = 'Homepage'

[taxonomies]
tag = 'tags'
series = 'series'

Hugo supports JSON, YAML, or TOML configs, both for the config file and markdown front matter. All config examples you see will use TOML, see the Hugo configuration documentation for more information.

To quickly explain the most important options:

  • baseURL defines the root URL for your website, which is used in absolute links like https://basic.tld/header.png, as opposed to relative links like ./header.png.
  • title sets the .Site.Title variable’s value.
  • [params] defines site-wide variables accessible at .Site.Params.param-name.
    • ['Item1, Item2'] defines a list of items.
  • [taxonomies] defines which directories under content/ should be treated as taxonomies.
    • Practically speaking, the first value is the name you specify under layouts/ (e.g. layouts/tag.html), while the second value (in single-quotes) is the name you specify under content/ (e.g. content/tags/_index.md).

Seeing the result

Everything should be setup now. So, check that your working directory is set to Public/Basic/, and then, run hugo server and navigate to the URL it prints to see a preview of your website.

The website should update whenever you make changes, however, there are a number of cases where it doesn’t (resource copying, browser caching, etc). If your changes aren’t showing up, quit hugo server, restart your browser, and then restart the server to ensure the preview is correct.

Since there’s no navigation links yet, besides the about page link in the footer, you’ll have to manually enter the URL for blog/my-first-post to navigate to it.

From here, your imagination is the limit (excluding functions Hugo denies for security reasons)! I personally recommend looking at Eric Murphy’s starter theme if you want a more ready-to-use starter to base your website on. In the next blog post, I’ll show how to use the range function to create navigation pages, and how to handle context rebinding issues.