Concepts of Hugo

a what-is-what introduction

When you start with Hugo The world’s fastest framework for building websites, it’s difficult to make sense of all the new vocabulary and concepts.

This article is an attempt to give Hugo beginners a what is what introduction to Hugo that will make the rest of the learning experience easier. The objective is to explain 90% of how Hugo works in less than 25 key points.

Clarity is prioritized over precision, so always refer to the official Hugo Docs for the most precise and up-to-date information.

All the examples were tested at github.com/acanalis/concepts-of-hugo. If you want to give feedback, message me at the Hugo Discourse!

Table of Contents

1. Info for Everyone

1.1. Hugo is a static site generator

A static site is one which can be served from a static server. The only job of a static server is to provide the necessary files *.html, *.js, *.css, etc to the clients.

Every client gets the same files: there’s no server-side intelligence or interaction. Interaction is possible, but using JavaScript on the client-side.

1.2. Hugo is a CLI program

Hugo itself is an executable file. You interact with it via the command line (PowerShell, cmd, bash). See Install Hugo Docs.

The two most important commands are hugo, and hugo server. If there’s a valid Hugo project in the current directory, hugo will generate the site, and hugo server will host it on a local server so that you can see it in your browser.

The -h flag displays help for any given command.

1.3. Hugo’s separates content from layouts

In principle, you can write HTML/CSS/JS by hand to do a static site. For many reasons this is inconvenient:

  • HTML is verbose compared to a plain document. For a link1 you will type 57 characters plus the actual link. It’s fine for one page, but it scales poorly.
  • Typically some parts of the code are the same for all the pages, like styles, fonts, links, etc. Even if you copy and paste everything it’ll be tedious to do.
  • If you want to make a change you’ll have to do it on every file.
  • Focus is divided between writing relatable content and handling the looks of the site.
  • Web design is its own field of knowledge and not everyone wants to invert time and effort on it.

Hugo makes the process much easier by allowing you to separate static web design into two activities: content writing and template writing.

Content is written in markdown, which conceptually is a syntactic sugar for HTML. Markdown is very easy to use.

Templates are written in a HTML + a special templating language. Hugo generates the site by inserting the data from your content files into those templates.

1.4. Society is divided into Theme Users and Theme Writers.

If you just want to use a nice theme from the Hugo Themes Page, you can start with the example site –every theme has one– and use your intuition to add files and configurations. No knowledge of the templates or even Web Design is required, all you need to do is to write all your content in markdown and browse Content Management Docs and the theme documentation every once in a while.

But the day will come that you’ll you need to tinker with the themes or write your own.

2. Info for Theme Users

2.1. Hugo projects have a definite directory structure

The top level of a Hugo project may have these:

  • config.yaml or config.toml or config.json
  • archetypes
  • assets
  • config
  • content
  • data
  • i18n
  • layouts
  • public
  • resources
  • static

You can read about each of them at Directory Structure Docs. A config file is mandatory2, and the rest directories are optional and added as needed.3

/public is created to store the generated files. The files stand on their own: you can upload them to the static server and forget that you ever used Hugo to create them.

2.2. Merge other Hugo projects to yours with Modules

Hugo Modules offer a very flexible and understandable way of importing from other project.

Suppose that you want to add the Ananke theme to your site. You could clone the repository from https://github.com/theNewDynamic/gohugo-theme-ananke and copy and paste files into your project.

But a cleaner method, more maintainable method is to:

  1. Run the command hugo mod init path/to/your/project which will initialize your project as a Hugo Module. If you don’t intend people to import your project, then path/to/your/project doesn’t matter, you can put a _ if you want.4
  2. Include the following lines in the config:
    baseURL: https://example.com/ 
    # ... and everything else
    
    module:
      imports:
        - path: github.com/theNewDynamic/gohugo-theme-ananke
    

The result is almost the same as copy and pasting: the files from Ananke behave as if they were your own. Note that only these folders are merged: archetypes, assets, content, data, i18n, layouts, static.

The advantages of doing this:

  • You can update the theme easily with the command
    hugo mod get -u github.com/theNewDynamic/gohugo-theme-ananke
    
  • When two files are in conflict (have the same path and name) your files are used. You can always override a theme file with one of your own.
  • You can mount specific directories instead of the whole project.

2.3. Content files are “front matter” + markdown

Any markdown .md file inside /content is called a content file5. Each content file is used to render a corresponding page.

Content files start with metadata variables defined in YAML, JSON, or TOML. This part of the file is called front matter. Typical variables are title and date, among others. The rest of the file is in markdown.

Front matter variables have different effects on the generated page, some are Hugo-specific, and some are theme-specific. A big part of working with Hugo is figuring out which variables do what you want.

The following three versions of /content/hello-world.md render the same page. Notice the different delimiters for each format:

  • YAML Front Matter
    ---
    title: Hello World
    date: 2021-01-13T11:00:00-03:00
    ---
    ## Hello World!!
    In this article, I will...
    
  • TOML Front Matter
    +++
    title = "Hello World"
    date = "2021-01-13T11:00:00-03:00"
    +++
    ## Hello World!!
    In this article, I will...
    
  • JSON Front Matter
    {
      "title" : "Hello World",
      "date" : "2021-01-13T11:00:00-03:00"
    }
    ## Hello World!!
    In this article, I will...
    

2.4. Shortcodes extend markdown

Markdown falls short in some aspects. Some page elements like galleries or iframes can’t be conveniently represented.

While the specification of markdown says that pure HTML is valid, Hugo skips it for safety reasons. If you write <h2> Hello </h2> in a content file, you’ll get <!-- raw HTML omitted --> at the generated file. You can configure markup to make it “unsafe” and then add all the elements you need. But the advantage of markdown was to avoid writing plain HTML in the first place.

Another option is to use shortcodes: templates you can call from the content files to insert HTML into the page.

Here’s an example using the built-in youtube shortcode:

---
title: Hello World
---
## Hello World!!
Before we start, see this video:

{‎{< youtube w7Ft2ymGmfc >}‎}

This produces the typical embedded code for Youtube videos:

<p>Before we start, see this video:</p>

<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
  <iframe src="https://www.youtube.com/embed/w7Ft2ymGmfc" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;" allowfullscreen title="YouTube Video"></iframe>
</div>

There are different ways to use shortcodes:

  • with named parameters
    {‎{< youtube id="w7Ft2ymGmfc" autoplay="true" >}‎}
    
  • raw string parameter
    {‎{<  rawstringshortcode `This is some <b>HTML</b>,
    and a new line with a "quoted string".` >}‎}
    
  • enclosing text, result is inserted into page directly
    {‎{< highlight go >}‎} A bunch of code here {‎{< /highlight >}‎}
    
  • enclosing text, result is inserted to content file, and then gets rendered as markdown.
    {‎{% markdownshortcode %}‎}
    Stuff to `process` in the *center*. 
    {‎{% /markdownshortcode %}‎}
    
  • self-closing (the following are equivalent)
    {‎{<myshortcode>}‎}{‎{</myshortcode>}‎}
    {‎{< myshortcode />}‎}
    

Hugo comes with many built-in shortcodes and you can define your own by adding templates to /layouts/shortcodes.

For reference on how to use and create shortcodes:

2.5. The generated site has the structure of /content

Suppose /content looks like this:

  • content
    • _index.md
    • blog
      • _index.md
      • hello-world.md
      • see-you-later-world.md
    • presentations
      • _index.md
      • why-hugo.md
      • why-go.md
      • math
        • _index.md
        • quadratics.md
        • linear-algebra.md

Then output has a similar structure:

  • public
    • index.html
    • blog
      • index.html
      • hello-world
        • index.html
      • see-you-later-world
        • index.html
    • presentations
      • index.html
      • why-hugo
        • index.html
      • why-go.md
        • index.html
      • math
        • index.html
        • quadratics
          • index.html
        • linear-algebra
          • index.html

And the final site will have these URLs once it’s hosted:

  • https://example.com/
  • https://example.com/blog/
  • https://example.com/blog/hello-world/
  • https://example.com/blog/see-you-later-world/
  • https://example.com/presentations/
  • https://example.com/presentations/why-hugo/
  • https://example.com/presentations/why-go/
  • https://example.com/presentations/math/
  • https://example.com/presentations/math/quadratics/
  • https://example.com/presentations/math/linear-algebra/

There’s a clear correspondence between /content structure, output files, and URL structure.6

2.6. Pages are list pages or regular pages

Some important definitions:

List pages are pages that contain children pages, and define the hierarchy of the site. The content file of a list page is its corresponding _index.md7.

Sections, a special case of list pages, are subdirs of /content that have an _index.md.8 Its children are the pages under the section’s directory, including other sections.

Regular pages are pages that can never have children.

In the previous example blog, presentations and presentation/math are sections. The children of presentations are presentations/why-hugo, presentations/why-go, and presentations/math.

The regular pages are:

  • hello-world
  • see-you-later-world
  • presentations/why-go
  • presentations/why-hugo
  • presentations/math/quadratics
  • presentations/math/linear-algebra

3. Info for Theme Writers

3.1. /layouts contains the HTML templates.

Each content file is rendered with a specific template from /layouts that is chosen using a set of rules.

A subset of rules I find convenient 9 is:

  • A regular page like /content/my_type/**/foo.md will be rendered using /layouts/my_type/single.html
  • A section page like /content/my_type/**/_index.md will be rendered using /layouts/my_type/list.html.
  • /layouts/_default/single.html and /layouts/_default/list.html are used if the others are missing.

3.2. Templates are HTML with {{...}}

Hugo’s templating language is borrowed from Go Templates. It has types, variables, functions, and statements (if, with, range, define and block).

In this example a template renders a single page:

  • config.yaml
  • content
    • blog
      • hello-world
        ---
        title: Hello World
        ---
        ## Hello World!!
        In **this** article, I will...
        
  • layouts
    • blog
      • single.html

        <html>
        <head>
        <title> {{ upper .Title }} </title>
        </head>
        <body> 
        {{.Content}} 
        </body>
        </html>
        

        The {{...}} are placeholders.

        In the line 3, the upper function receives the .Title variable as an argument. The value of .Title is set in the front matter of the content file.

        Line 5 inserts the variable .Content. It contains the markdown converted to HTML, so **this** becomes <strong>this</strong> in the variable.

  • public
    • blog
      • hello-world
        • index.html
          This is the final result:
          <html>
          <head>
          <title> HELLO WORLD </title>
          </head>
          <body> 
          <h2 id="hello-world">Hello World!!</h2>
          <p>In <strong>this</strong> article, I will&hellip;</p>
                  
          </body>
          </html>
          

3.3. The dot has the current context

The dot stores the variables that can be accessed at a given point in the program, which is called “the context”. That’s why in the previous example .Content and .Title begin with a dot.

The dot changes meaning inside range and with blocks. The context inside and outside of those blocks is different.

3.4. Hugo gives to variables to use in your templates

Hugo provides variables for you to use in your templates, like .Title and .Content. Variables depend on front matter, config, or the relationship between pages in the site.

3.4.1 How to access front matter variables

Some of the variables at the front matter of content files can be used. As seen before, the variable title is accessed as .Title. Read about the full list at Page Variables.

The title example seems to suggest that any variable is used with the first letter uppercased. However, my_custom_variable cannot be accessed as .My_custom_variable, because it doesn’t have a special meaning to Hugo. Anything that isn’t Hugo-related will go to the .Params dictionary, so the correct way to use it in a template is .Params.my_custom_variable.

Here’s a full example:

  • config.yaml
  • content
    • blog
      • hello-world.md
        ---
        title: Hello
        my_custom_variable: im a custom variable
        ---
        In this article, I will...
        
  • layouts
    • blog
      • single.html
        <html>
        <body>
          <p> The value is: {{.Params.my_custom_variable}} </p>
        </body>
        
  • public
    • blog
      • hello-world
        • index.html
            <html>
            <body>
              <p> The value is: i'm a custom variable </p>
            </body>
            </html>
          

Custom front matter variables allow the theme writer to give a special configurations to the theme user. The theme documentation could say:

You can set your page to night mode by setting nightmode: true in the front matter and then use .Params.nightmode to load the appropriate CSS. This is a very common pattern.

3.4.2 How to access variables from the config

Again, some variables of the config can be accessed as listed in Site Variables. These variables are global so they can be used in any template. For example, if at config.yaml you define author: John Doe, you can use that variable in the templates as .Site.Author.

The behaviour of custom variables is different from front matter params. Top level variables that don’t have a Hugo-specified behaviour are ignored. Instead, custom variables must be defined under the param object and then used from the .Site.Params dictionary.

Here’s an example for a custom global variable:

  • config.yaml
    baseURL: https://example.com
    params: 
      my_custom_variable: im a custom variable
    
  • content
    • blog
      • hello-world.md
  • layouts
    • blog
      • single.html
        <html>
        <body>
          <p> The value is: {{.Site.Params.my_custom_variable}} </p>
        </body>
        </html>
        
  • public
    • blog
      • hello-world
        • index.html
          <html>
          <body>
            <p> The value is: i'm a custom variable </p>
          </body>
          </html>
          

As before, custom variables are used by theme writers for site configurations for the users. Using the same example, the theme documentation could say

You can set your site to night mode by writing to your config:

params: 
  nightmode: true

And the theme writer uses .Site.Params.nightmode to adjust for each situation in the templates.

3.4.3 Variables that expose the hierarchy

The relationships between the pages are used to produce links so that the site can be navigated from one page to another. For example a blog page might list a summary of the different posts to persuade readers to click on the links and read the full post.

The most important variables for this use is .Pages, that is a slice10 of children of the current (list) page.

In the following example, suppose that you want the section page of the blog at https://example.com/blog to list all the links to the posts so that the users can visit them.

To do this use:

  • .Pages, and .Title discussed previously
  • the range block, which iterates over slices and dicts11.
  • .Permalink, a page variable that contains the full URL to the page.

Here’s the full example:

  • config.yaml

    baseURL: https://example.com/
    
  • content

    • _index.md
    • blog
      • _index.md
        ---
        title: Blog
        ---
        Welcome to the blog! See the posts:
        
      • hello-world.md
        ---
        title: Hello World!
        ---
        In this article, I will...
        
      • see-you-later-world.md
        ---
        title: See you later World!
        ---
        In this article, I will...
        
  • layouts

    • index.html
    • blog
      • single.html

      • list.html

        <html>
        <head>
        <title> {{ .Title }} </title>
        </head>
        <body>
        {{.Content}}
        <ol>
        {{ range .Pages }}
          <li>
          <a href="{{ .Permalink }}">{{ .Title }}</a>
          </li>
        {{ end }}
        </ol>
        </body>
        </html>
        

        Inside the range block, the dot means “the current iteration”.

        As a result, .Title doesn’t mean “the title of the current page”, but instead means “the title of the current iteration page”. This is called rebinding the context.12

  • public

    • blog
      • index.html
        <html>
        <head>
        <title> Blog </title>
        </head>
        <body>
        <p>Welcome to the blog! See the posts:</p>
        
        <ol>
        
          <li>
          <a href="https://example.com/blog/hello-world/">Hello World!</a>
          </li>
        
          <li>
          <a href="https://example.com/blog/see-you-later-world/">See you later World!</a>
          </li>
        
        </ol>
        </body>
        </html>
        

3.6. Template variables are declared with ‎ := ‎ and updated with ‎ = ‎

For convenience during template writing, you can declare internal variables with:

{{ $a := "hello" }}
{{ $a = "HELLO" }}
{{ $a }}

In the first line, $a is declared and set to "hello". Then, it changes its value to “HELLO”. Finally, it’s printed on the template.

It’s an error to use = to a variable that hasn’t been declared with := before: 13

{{ $b }}
$ hugo
Error: 
add site dependencies: 
load resources: 
loading templates: "C:\Users\agust\concepts-of-hugo\3.8-template-variables\layouts\blog\single.html:12:1":
parse failed: 
template: 
blog/single.html:12: 
undefined variable "$b"

3.7. Handle missing values with if, with, and default

The previous example is a bit fragile, because the templates are called using many different content files. If my_custom_variable doesn’t exist in one of them, the whole build fails.

Here are three methods to avoid that:

3.8.1 if

{{ if (isset .Params "my_custom_variable") }}
    <p> The value is: {{ .Params.my_custom_variable }} </p>
{{ else }} 
  <p> my_custom_param not set </p>
{{ end }}

This example is just to show how the if statement works. The {{else}} block is optional. The isset function14 returns true if the second argument is set in the first argument, and false otherwise.

3.8.2 with

{{ with .Params.my_custom_param }}
<p> The value is: {{.}} </p>
{{ end }}

The block is run if .Params.my_custom_param is set, and not run if it’s missing. with rebinds the context: inside the block, the dot means “the variable of the with”, in this case my_custom_param.

3.8.3 default

{{ $a := default "my_default" .Params.my_custom_variable }}
{{ $a }}

The default function outputs a default value if a variable is not set. In this case, if .Params.my_custom_variable is not set, so $a it takes the value of "my_default".

3.9. Parenthesis clarify how the functions are applied

Like in math, parenthesis are used to disambiguate the order of application in a long sequence of functions:

{{ markdownify (upper (emojify "## hello :‎smiley:")) }}
  1. emojify is applied to "## hello :‎smiley:" which results in "## hello 😀"
  2. upper is applied to "## hello 😀" which results in "## HELLO 😀"
  3. markdownify is applied to "## HELLO 😀", which turns it into "<h2> HELLO 😀</h2>"

3.10. Pipe operator = less parenthesis

The pipe operator ‎ | ‎ is a syntactic sugar that passes the output from the left of the operator to the function to the right, as the last argument.

This is equivalent to the previous example:

{{ "## hello :‎smiley‎:" | emojify | upper | markdownify }}

The output is exactly the same as before. Notice that there are less parenthesis used.

3.11. Partials keep templates DRY

Certain elements of a site appear on multiple templates, such as menus or footers. It would be inconvenient to rewrite the same patterns across all templates.

Partials allow you to reuse code. A partial is a template stored in /layouts/partials that can be called from other templates.

In the following example, a footer is separated into its own partial so that it can be used in many templates:

  • config.yaml
    baseURL: https://example.com/
    
  • content
    • blog
      • hello-world.md
          ---
          title: Hello
          ---
          ## Hello World!
          In this article, I will...
        
  • layouts
    • partials
    • blog
      • single.html

        <html>
        <head> 
          <title> {{.Title}} </title>
        </head>
        <body> 
          {{.Content}}
        </body>
        {{ partial "my_footer.html" . }}
        </html>
        

        The first argument is the path to the template relative to /layouts/partials, and the second argument is the context passed. In other words, inside the partial template Hugo will use the second argument as the dot.

      • my_footer.html

        <footer> 
        Site made with ❤️ and Hugo.  
        <a href="{{.Site.Home.Permalink}}"> Return to Homepage</a>
        </footer>
        
  • public
    • blog
      • hello-world
        • index.html
          <html>
          <head> 
            <title> Hello </title>
          </head>
          <body> 
            <h2 id="hello-world">Hello World!</h2>
          <p>In this article, I will&hellip;</p>
          
          </body>
          <footer> 
          Site made with ❤️ and Hugo.  
          <a href="https://example.com/"> Return to Homepage</a>
          </footer>
          </html>
          

Footnotes


  1. Source: <a href="https://example.com" rel="noopener noreferrer" target="_blank">example.com</a> ↩︎

  2. This isn’t 100% true. The configs can be set at /config as pointed out in the Configuration Directory Docs. ↩︎

  3. The folders can be configured to have dferent names, although this is not common. ↩︎

  4. If you do expect people to import your project then put the path that everyone will use to import your project. This is how it would look like: hugo mod init github.com/johndoe/my_module_project. See Using Modules Docs ↩︎

  5. Files other than markdown can be used, including html, pandoc, etc. See the list of content formats ↩︎

  6. This is a general rule. You can force the URLs to be different, which you can see in URL Management Docs. Also, the pages don’t need to be contained in a single .md. There’s another way to organize content files called Page Bundles which allows you to divide each page on a separate directory where you can also keep the images and resources specific to that page. ↩︎

  7. See Lists of Content Docs. ↩︎

  8. As a special case, the first level children of /content are always sections. See Sections Docs. ↩︎

  9. There are many other ways to organize templates, see Template Lookup Order. It’s a matter of preference. ↩︎

  10. slices are known as “array” in other languages ↩︎

  11. also known as maps ↩︎

  12. When the context has been rebinded, you lose access to the original Page variables. Conveniently, the variable $ is set to the initial value of the dot. You use it from inside the block as {{$.Title}} to get the variables back. See Use $ to access the global context ↩︎

  13. Another common error is to accidentally define a variable twice in different contexts:

    {{ $a := 0 }}
    {{ range (slice 1 2 3) }}
      {{ $a := . }}
      {{ $a }}
    {{ end }}
    {{ a }}
    

    The result here is “1 2 3 0”, instead of “1 2 3 3”. $a is declared in the initial context, and then a different $a gets declared inside the range block. This can lead to unexpected results.

    To get the “1 2 3 3” version, put $a = . at the fourth line. ↩︎

  14. See isset Docs. ↩︎

Agustin Canalis
Agustin Canalis
Mechanical Engineering Student