Getting Started with Hugo (Part 1)

By Craig Halley on August 7, 2022

GitHub Repo: https://github.com/craighalley/hugo-starter

Hugo is a popular static site generator, normally compared to Jekyll, Gatsby, and Eleventy, among others. I’m using it for the Steelyard.io site and for a few other websites that I manage, and overall I’ve been pleased with how it works and how quickly it can generate a static site. What I’m less pleased with is the state of Hugo’s official documentation, which I find to be confusing and poorly organized, especially for beginners wanting to learn the basics of creating a simple website.

For a while I thought it was just me, that I wasn’t grasping some of the key concepts, and that one day the clouds would part and all would be revealed. But then I found this article, which pretty much says exactly what I’ve been feeling:

https://sagar.se/blog/hugo-documentation/

Every time I ran into something in the documentation that didn’t make a bit of sense, I’d throw my hands up and start searching around for a “better” static site generator, one that’s easier to learn or has better documentation, examples, and starter templates. But none of the others seemed that much better, honestly. I suggest you just pick one and spend the time to figure out how it works.

That’s what I’m doing with Hugo, and while I’m still learning some of the more advanced features, I think this article is a better introduction to creating a simple site with Hugo than what you will find in the official docs, and hopefully it will help some of you get started as well.

Note that this is not meant to take anything away from what the Hugo authors and maintainers have done and are doing. Writing documentation is hard, and they have produced a lot of very helpful content already, some of which I’ll reference as we go, so kudos to them for creating a great tool and the resources behind it.

Preliminaries

Install Hugo

If you haven’t already, the first step is to install hugo on your system. For example, if you’re using a Mac, you can install hugo using Homebrew, as shown below. If you’re not on a Mac or need to check for some other options, see the official docs here: Install Hugo.

$ brew install hugo

Generate a New Hugo Project

Next, let’s use the Hugo CLI to generate a new project for us, called hugo-starter.

$ hugo new site hugo-starter

Hugo will dutifully create a new directory called hugo-starter and output some helpful information to get you on your way.

Congratulations! Your new Hugo site is created in ../hugo-starter.

Just a few more steps and you're ready to go:

1. Download a theme into the same-named folder.
   Choose a theme from https://themes.gohugo.io/ or
   create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
   with "hugo new <SECTIONNAME>/<FILENAME>.<FORMAT>".
3. Start the built-in live server via "hugo server".

Visit https://gohugo.io/ for quickstart guide and full documentation.

The output above makes it seem really easy to get up and running, and Hugo has generated a folder structure for you, with a couple bare minimum files, but it’s not enough to even generate a single HTML page yet. In fact, if you start the hugo server now (by typing hugo serve) you’ll just see a blank website.

This is what your project directory will look like after running the new site command.

hugo-starter/
  archetypes/
  config.toml
  content/
  data/
  layouts/
  static/
  themes/

I haven’t left out the contents of those directories, they’re all empty! So it makes sense that our website is blank, but it would be really nice if the Hugo site generator had an option to at least generate a plain “Hello World” page so we had something to look at. Instead, it recommends that you find and install a theme (there aren’t very many good ones) or create your own theme (whoa, LOTS of work).

Have no fear, we’re going to fix that Hello World problem right now.

Building a Site

Create a Home Page

Let’s start small and just get our website to say hello. Create a file named layouts/home.html with the code below:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
  </head>
  <body>
    <h1>Hello World</h1>
  </body>
</html>

Now run the hugo serve command and point your favorite browser to http://localhost:1313. You should see a proper Hello World home page for your website, like this:

That wasn’t so hard, right? I still think Hugo should provide something like that right out of the gate rather than sending us down the rabbit hole of themes, or at least describe how to easily create the Hello World page in the official Getting Started documentation.

Just for kicks, create another file called layouts/index.html and populate it with this slightly different code:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
  </head>
  <body>
    <h1>Hello Great Big World</h1>
  </body>
</html>

After you save that file (assuming you still have the hugo serve process running), you should notice that your website has updated to use the new index.html layout instead of home.html for the home page, like some kind of strange magic. Your home page should now look like this:

What’s going on here? How does that work?

It turns out that Hugo has a lookup order that it follows when trying to figure out which layout to use to generate each page of your website. You can read more about the Lookup Order in Hugo’s official documentation.

The index.html layout comes before the home.html layout in the lookup order, so when generating the home page, index.html wins.

Make It Data Driven

We’ll come back to the concept of layouts later, but for now, let’s just focus on making our home page a little more dynamic and attractive. First, let’s explore the content/ folder that Hugo provides for storing the content of our site.

Create a new file named content/_index.md (the underscore in the filename is required) with the content below:

---
title: "Hello World"
---
### Content as Data

Hugo allows you to define the data for a page using configuration
files like this one, written in Markdown, HTML, or a variety of
other formats.

Next, update the layouts/index.html file with the content below:

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
  </head>
  <body>
    <h1>{{ .Params.title }}</h1>
    <p>{{ .Content }}</p>
  </body>
</html>

Now check your updated webpage in the browser. It should look like this:

That’s pretty cool!

Introducing Content Files

Hugo is working its magic to combine the layouts/index.html template with the content/_index.md data file to create the web page that we’re now seeing. There are a few things to unpack with what just happened.

First, why is the content file called _index.md? For now, you’ll just have to take this as the way things are for Hugo, as the concept is fairly confusing in the official documentation. The home page that we’re working on here is considered to be a “list” page by Hugo, and list pages get their content from an _index.md file. That’s just how it works.

Next, notice the first three lines in the _index.md file; that’s called the “front matter”. Hugo allows you to define variables in your content files, in a special front matter area at the top of each file, and you can pull those values out using the {{ .Param }} syntax that you can see in the updated index.html file. This is incredibly useful when using using the same layout file to generate more than one page in your site, as you’ll see later.

Also in the content/_index.md file, you’ll see some Markdown text that includes a header and some paragraph text, and you can see how that content was rendered into the final HTML on our updated home page. But how did Hugo know to do that?

That’s where the special {{ .Content }} variable comes into play. It’s a special Hugo page variable that represents everything below the front matter in our _index.md file.

You can read more about all of Hugo’s page variables here: Page Variables

Given how common it is to use a title attribute in pages like this (especially in blogs and similar content), Hugo has defined a special page variable called .Title that you can use instead of .Params.title. They work the same as far as I know, and there are a few more page variables that work in a similar fashion, like .Date and .Description.

Replace {{ .Params.title }} with {{ .Title }} in index.html, as shown below, and verify your site still looks the same.

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
  </head>
  <body>
    <h1>{{ .Title }}</h1>
    <p>{{ .Content }}</p>
  </body>
</html>

Add Styling with Bootstrap

I’m not sure about you, but I’d rather not look at ugly web pages all day, so we’re going to take a short detour and add some style to our site, while noting a couple of things about how Hugo handles static content like images and CSS files.

First, let’s add Bootstrap to our site so we don’t have to reinvent the wheel with styles. If you happen to be one of those folks who hates Bootstrap and loves Foundation or some other similar library, your homework assignment can be to replace all of the Bootstrap stuff after we’re done. The rest of us will take a nap.

At the moment, Bootstrap 5.2 is the latest version, and this page, Install Bootstrap, describes how to include it via a CDN, which is what we’re going to do.

Update index.html with the code below, which includes the Bootstrap CSS and JS, plus some updated styling of our .Title and .Content variables.

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <div class="px-4 py-5 text-center">
      <div class="py-5">
        <h1 class="display-3 fw-bold">{{ .Title }}</h1>
        <div class="col-6 mx-auto">
          <p>{{ .Content }}</p>
        </div>
      </div>
    </div>
  </body>
  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
    crossorigin="anonymous"
  ></script>
</html>

Now refresh your site and verify that our Bootstrap styling is making things a bit nicer looking! Your new home page should look like this:

Custom CSS and Background Images

Let’s keep going and add a bit of custom CSS to get rid of our boring white background. First, create two folders, static/css and static/img. Then create a new file under static/css, called default.css, with the code below:

body {
  background-color: transparent;
  color: #eee;
}

body:after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  background-color: transparent;
  background-image: url(../img/pattern.png);
  background-size: 4.6875rem;
  background-repeat: repeat;
  background-attachment: initial;
  z-index: -5;
}

html:after {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  z-index: -10;
  background-image: url(../img/background.jpg);
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  background-attachment: initial;
  transition: background 0.2s linear;
}

body:before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 100%;
  z-index: -5;
  background: linear-gradient(180deg, rgba(50, 70, 80, 0.9) 0, #0d101b 100%);
}

Our custom CSS requires two image files, background.jpg and pattern.png, which can be found in the github repo for this article. Feel free to replace background.jpg with any other background image that you think is worthy (check pexels.com).

The pattern.png file is a bit unique in that it is applying a semi-transparent grid on top of your background image, which creates a nice but ultimately unnecessary effect. If you’d rather not deal with it, just delete the body:after CSS code where it is being referenced, and Bob’s your uncle.

Download the images and put them into the static/img folder.

The last step is update index.html to include default.css, so update the <head> element of index.html to look like the code below (we’re just adding the second <link> element):

<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Hugo Starter</title>
  <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
    rel="stylesheet"
    integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
    crossorigin="anonymous"
  />
  <link href="/css/default.css" rel="stylesheet" />
</head>

Check out your updated site and enjoy all of your hard work! It should now look like this:

Expanding Our Site

Moving on, let’s explore a bit more of what Hugo has to offer. Let’s imagine that our goal is to extend our simple website to have a home page with some additional content areas below what we’ve already done. In Part 2 of this series, I’ll cover how to add an About Us page, a Blog with a couple blog entries, and a nav bar. Stay tuned for that!

Even this simple additional content area on the home page will highlight several important concepts in Hugo.

Introducing Base Templates and Blocks

First, let’s back up a bit and prepare our site with some better organization. So far, we’ve just been dealing with a single template, index.html. We’re going to need something more flexible to grow our site over time, and Hugo provides the concepts of Base Templates and Blocks to make this easier.

To convert the index.html template into something better, start by creating a new layouts/_default/baseof.html file, with the contents below. You’ll have to create the _default folder as well, obviously. That particular folder is very important to Hugo’s template lookup order concept that I mentioned above, the one that Hugo uses when searching for files to generate your site.

Here’s the content for your baseof.html file (almost the same as our current index.html file):

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hugo Starter</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx"
      crossorigin="anonymous"
    />
    <link href="css/default.css" rel="stylesheet" />
  </head>
  <body>
    {{ block "main" . }}
    {{ end }}
  </body>
  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa"
    crossorigin="anonymous"
  ></script>
</html>

Now update your index.html to get rid of all of the HTML boilerplate, and leave only the real content as shown below:

{{ define "main" }}
<div class="px-4 py-5 text-center">
  <div class="py-5">
    <h1 class="display-3 fw-bold">{{ .Title }}</h1>
    <div class="col-6 mx-auto">
      <p>{{ .Content }}</p>
    </div>
  </div>
</div>
{{ end }}

And check out the updated website in your browser. Looks the same, right? Why?

First, it should be fairly obvious that Hugo is combining the _default/baseof.html template and the index.html template when generating our home page (and also the content/_index.md file, of course). But how does it know to do that?

Again, referencing Hugo’s template lookup order concept (Lookup Order) you can see under the “Layout Lookup for Home Page” section, that that there are 12 different layouts for finding the home page template, and 16 different layouts for finding the base template for the home page template. That’s quite a lookup order.

Hugo allows for two layout files to be combined when generating pages, a base template and a second template, and the lookup order documentation describes where both of those files can be found.

At the moment, we’re using base template #16 (layouts/_default/baseof.html) and home page template #4 (layouts/index.html). Hugo combines those together when generating our Home page.

The mechanism by which those two templates are combined relies on Hugo’s concepts of “blocks”, which you can see in use in both baseof.html and index.html.

In baseof.html, we’ve created a “block” with name “main”. You can think of blocks as placeholders for content that is defined elsewhere. In the above code, we’ve created a placeholder for “main” content, based on the label that we provided.

In index.html, we’ve defined what that main content block should contain, which is some Bootstrap classes wrapping our <h1> title and a <div> with content. When Hugo generates the home page, it sees that the baseof.html template has specified some “main” content, and it finds that main content in the index.html file.

The concept of blocks is pretty straightforward, and you’ll find similar concepts in almost every templating libraries you may have used in other web development projects (e.g. Handlebars, Mustache, or Liquid).

You are free to define whatever blocks you want and Hugo will plug them into where they need to go when it generates your pages. You can even define “default” content inside the block definition, and that content will be used if the second template doesn’t provide anything that should be used instead.

Using Partial Templates

Let’s extend our home page with that additional content we were talking about, like something you might see on a real webite. We’re going to add some data-driven content modules in the form of cards, and when we’re done it will look like this:

So our plan is to create a layout with 3 cards (using Bootstrap’s grid and card classes), but we want to do it in the “Hugo way” so that’s easy to maintain and extend going forward.

One of the benefits of using a static site generator like Hugo is that you should not have to update the HTML code directly whenever your site changes. You should be able to add or modify at least some of the content using data-driven files and have the HTML be generated for you.

We’re going to use Hugo’s concept of partial templates and data to generate our updated home page. You can read more about Partial Templates in Hugo’s official documentation.

Let’s start by creating the HTML to render one of those cards.

Create a new file called layouts/partials/card.html and give it the following HTML content (you’ll have to create the partials folder of course).

Note that all of the content you need is shown here, there are no missing <html> blocks or other code. These are “partial” templates, which are really just fragments of HTML that get combined together by Hugo.

<div class="col">
  <div
    class="card border-light bg-transparent mb-3 h-100"
    style="--bs-border-opacity: 0.3"
  >
    <div
      class="card-header bg-primary text-uppercase fw-bold"
      style="--bs-bg-opacity: 0.2"
    >
      Header
    </div>
    <div class="card-body">Body</div>
    <div class="card-footer">Footer</div>
  </div>
</div>

Next, create another new file called layouts/partials/cardlist.html and populate it with this HTML fragment:

<div class="row row-cols-3 g-4 mt-5">
  {{ partial "partials/card.html" . }}
</div>

And finally, update the index.html template that drives our home page with the following content:

{{ define "main" }}
<div class="px-4 py-5 text-center">
  <div class="py-5">
    <h1 class="display-3 fw-bold">{{ .Title }}</h1>
    <div class="col-6 mx-auto">
      <p>{{ .Content }}</p>
    </div>
    {{ partial "partials/cardlist.html" . }}
  </div>
</div>
{{ end }}

Your sharp eye is correct; all we’ve added is that line starting with the {{ partial }} function code. Now, check out your updated website and verify that you have one card showing, like the screenshot below.

VERY fancy, right? Let’s see what Hugo is doing behind the scenes.

Remember that “partials” are just templates that render a bit of HTML which is then inserted into the fully rendered page. In our example, we’ve updated the index.html template file to include the partials/cardlist.html template, just below the content that was already on the page (using the {{ partial }} syntax).

And the partials/cardlist.html template itself creates a wrapping “row” element, and then includes the partials/card.html template, which is how our card ultimately lands on the home page when it’s rendered. Take a minute to trace through those changes to make sure you see how that is working.

Of course, it’s just one card when we wanted three, and the content of the card is hard-coded into the HTML template. That’s not going to work, so let’s fix that.

Using Data to Drive Content

To provide dynamic content for our card components, we’re going to utilize Hugo’s data folder, which allows us to create data-driven elements like these cards we’re trying to build.

First, create a new folder named data/cards, then create a new file called blocks.yaml inside that folder. Populate the blocks.yaml file with this content:

---
header: Blocks
---

Now, update the cardlist.html template file to have this content:

<div class="row row-cols-3 g-4 mt-5">
  {{ range $.Site.Data.cards }}
      {{ partial "partials/card.html" . }}
  {{ end }}
</div>

Check your updated website and notice that it still looks the same, which is good! But, we changed a few things, so we need to understand why it still looks the same.

The big change was that we updated the cardlist.html template to render the card.html partial template dynamically, based on the contents of our data/card folder (which has one card in it, represented by the blocks.yaml file).

The dynamic rendering comes from our use of the {{ range }} function, which iterates over an array, provided in this case by the $.Site.Data.cards variable. You can read more about the $.Site.Data variable in Hugo’s official documentation. The .cards label at the end of that variable tells Hugo to look at the data/cards folder that we just created, and to pull out all of the data files within that folder (which is just blocks.yaml right now).

Of course, blocks.yaml doesn’t really have anything in it (yet), but the simple fact that there is one data file in the data/cards folder means that Hugo will render the partials/card.html template once during the evaluation of the {{ range }} function, which is why we still see one card on our home page.

To test this out, go ahead and create a second file inside the data/cards folder, called templates.yaml, populate it with this content:

---
header: Templates
---

As soon as you save that file, check out your updated home page, which should now look like this:

Two data files in data/cards means the {{ range }} function will loop inside the {{ $.Site.Data.cards }} array twice, so now we have two cards on the home page. Easy peasy.

Let’s go ahead and add a third file to the data/cards folder, called variables.yaml, with this content:

---
header: Variables
---

And now our updated home page has three cards on the home page, just like we wanted. However, the content of the cards isn’t dynamic yet, so let’s look at how to go about fixing that.

Using Page Variables

Each card is being rendered by the partials/card.html template, so let’s update the template with some variables to make our site generation more dynamic. Update card.html to look like this:

<div class="col">
  <div
    class="card border-light bg-transparent mb-3 h-100"
    style="--bs-border-opacity: 0.3"
  >
    <div
      class="card-header bg-primary text-uppercase fw-bold"
      style="--bs-bg-opacity: 0.2"
    >
      {{ .header }}
    </div>
    <div class="card-body">{{ .body | markdownify }}</div>
    <div class="card-footer">
      <a class="text-muted" href="{{ .url }}">Read More</a>
    </div>
  </div>
</div>

You’ll notice that we’ve changed three things:

  • The word Header is now a variable {{ .header }}
  • The word Body is now a variable piped through by a Hugo function {{ .body | markdownify }}
  • There is a new anchor link with the href set to a variable {{ .url }}.

A quick check of your re-rendered home page should reveal that the header of each card is now being dynamically populated (yay!), but the body text is missing, and the card footer now contains a “Read More” link which doesn’t go anywhere.

Hugo is using the contents of each card .yaml file to generate those card UI elements, but it’s only able to partially generate the contents of each card because we haven’t put very much into the .yaml files yet. Each of them only has the “header” variable defined.

To fix that, we just need to add more variables to the .yaml files, specifically the ones that the template is expecting to find (body and url).

Update data/cards/blocks.yaml to look like this:

---
header: Blocks
url: https://gohugo.io/templates/base/
body: The `block` keyword allows you to define the outer shell
  of a page within one or more base templates, and then fill in or
  override portions as necessary.
---

Update data/cards/templates.yaml to look like this:

---
header: Templates
url: https://gohugo.io/templates/
body: Hugo templates are HTML files with the addition of
  `variables` and `functions`. Template variables and functions
  are accessible within double brace marks.
---

Update data/cards/variables.yaml to look like this:

---
header: Variables
url: https://gohugo.io/templates/introduction/#variables
body: In Hugo, each each template is passed a Page data object to provide
  it with context for generating the page. That context is accessible
  within a template using variables like `.Title` and
  `.IsHome`.
---

You’ll notice that the body variable inside each .yaml file contains a small bit of markdown code (the backticks used to denote code elements). That’s why we have to pipe the .body variable through the markdownify function inside the card.html template. The markdownify function is what converts our markdown-formatted body content into the HTML you see on screen.

Your almost final home page should now look like this:

The last change we want to make is to dynamically specify the color of each card header, which is handled by a bootstrap class in the card.html template file. In that file, change this line:

class="card-header bg-primary text-uppercase fw-bold"

to this:

class="card-header {{ .bgColor }} text-uppercase fw-bold"

All we’re doing is changing that bg-primary CSS class to a variable, .bgColor, that we can define in our card .yaml files. Go ahead and add the bgColor variable to each of those files, using values of bg-primary, bg-danger, and bg-info.

For example, blocks.yaml should now look like this:

---
header: Blocks
url: https://gohugo.io/templates/base/
bgColor: bg-primary
body: The `block` keyword allows you to define the outer shell
  of a page within one or more base templates, and then fill in or
  override portions as necessary.
---

Once all of those changes are complete, your updated home page should look just like our goal as defined above, like this:

We covered quite a bit of ground creating this simple website. Hopefully you found it helpful, and you can see how additional content sections could be added to your home page. Hugo offers a lot more features and functionality, so definitely keep exploring the official documentation to uncover some of those gems.

In Part 2 of this series, I’ll show you how to add an About Us page, a Blog page with a few posts, and a navigation bar as well, so keep an eye out for that article if you’re interested.