Getting Started with Hugo

Hugo is a static site generator (SSG). Simply put, an SSG prepares everything needed to display a website. In contrast, dynamically assembled websites obtain their assets only after they get accessed. An SSG thus minimizes the computational demand on the server, which allows providers, such as GitHub Pages and others1, to host your static sites for free. More importantly, SSGs emanate a philosophy of simplicity-by-design stemming from a number of limitations. Being constraint forces content creators to focus on the content itself and minimizes the distractions of layout and design. SSGs are thus especially suited for blogs, landing pages, documentation and portfolio sites.

The following is based on Ryan Schachte’s excellent video2 and similar tutorials (see links below).

Install Hugo

On MacOS install Hugo from the command line using the Homebrew package manager.

brew install hugo

On Debian Linux install Hugo with:

sudo apt-get install hugo

More installation instructions can be found here.

Setting Up Two Repositories

We will use GitHub Pages to host our static site for free. GitHub Pages are easily set up by creating a new public repository named username.github.io, where username is your GitHub username. If you were do save a simple HTML page as index.html in such a repo, you’d be able to view it at https://username.github.io.

However, we’ll be slightly more considerate than dumping all of Hugo’s configuration code, templates, themes and markdown content into the GitHub repository. After all, to display the website correctly only the static site assets that Hugo compiles are needed. We are going to track all the rest in a separate repository called “blog”.

So please head over to GitHub and create two repositories:

  • username.github.io: A public repository for the static site assets.
  • blog (can have a different name): A private repository for Hugo containing configurations, themes, and unpublished posts.

Setting up Hugo

Once we’ve created two repositories, we’ll clone them on our local system (git clone git@github.com:username/blog.git) and enter the “blog” repository (cd blog).

Here we create the scaffolding for our website. In this case Hugo will create a new directory hugo-blog that contains all the Hugo assets.

hugo new site hugo-blog

Adding a Theme

Enter the hugo-blog/themes directory and clone your preferred Hugo theme. For example, Hugo Bear Blog from Jan Raasch. Click on the “Download” button, copy the repository URL, and clone the theme into your themes/ folder using the submodule action. The submodule action avoids problems arising from nested cloning3.

# Option 1: Original theme repo
git submodule add https://github.com/janraasch/hugo-bearblog.git themes/hugo-bearblog

Alternatively, you can first fork the theme directory by going to GitHub and clicking ‘Fork’. This has the advantage of being able to push changes to your fork of the themes directory later on.

Don’t forget to add the fork as a submodule instead of the original repo. Replace username and hugo-theme with the respective values.

# Option 2: Forked theme repo
git submodule add https://github.com/username/hugo-theme.git themes/hugo-theme

Then we add the theme’s name to the config.toml in blog’s base directory, e.g. hugo-blog/. The file might look like this:

baseURL = 'https://username.github.io/'
languageCode = 'en-us'
title = 'My Blog Title'
theme = 'hugo-bearblog'

The values of baseURL and theme obviously depend on your choice of host and theme.

Adding the static site content repo as a submodule

As mentioned above, we are decided to create a separate repository for the compiled static site content. In essence, we will let the username.github.io repository only track Hugo’s public/ output directory and nothing else. This way the commit history will purely reflect content updates. Plus, we can track any unpublished content in a private repo hidden from public view. The downside is that we have to remember to update both repositories.

Run the following command from within the blog’s base directory (e.g. hugo-blog/) to link your public GitHub Pages repo to the public/ directory of your private blog repo. (If you receive an ‘already exists’ error you might have to remove the public/ directory or its contents first.)

git submodule add -b main https://github.com/username/username.github.io.git public

Building the site

To perform the site build, run the following commands

# create static assets in the 'public/' directory
hugo

# commit the changes to the public submodule repository
cd  public
git add .
git commit -m "initial build"

# commit references to submodule changes
cd ..
git add .
git commit -m "initial build - update submodule references"

# push both the source project and the submodule to remote
git push -u origin main --recurse-submodules=on-demand

Remember these command, you’ll have to repeat them for most changes to the site.

This concludes the basic Hugo setup. In the following we’ll go through posting, theme customization, adding images, and analytics. Let’s create our first post.

Creating a Post

Hugo posts are created in Markdown. The limited formatting options force authors to focus on the content. In general, it’s good advice not to try to change the formatting beyond what markdown offers. The theme’s template and CSS files provide better options while ensuring that the layout stays consistent across the site.

To create a new post, run:

hugo new blog/mypost.md

This will create a markdown file in the content/ directory, e.g. content/blog/mypost.md. Depending on your theme, you might have to substitute blog/ with post/ or posts/. Consult your theme’s documentation.

Open the freshly created post with your favorite editor. Hugo posts start with a preamble, the key: value pairs between the lines (---). The preamble sets various variables that Hugo will use to compile the site. Depending on the theme, you can add cover images, tags, and category labels. The draft status and publication date affect whether a post is published. You can add content below the preamble, like so.

---
title: "My First Post"
date: 2020-01-01T12:15:00+01:00
draft: true
---

# Introduction

Some **bold** text and a [link](example.com).

Now fire up the Hugo server locally in ‘draft’ mode and open the respective URL with your browser, e.g. http://localhost:1313.

hugo server -D

If you’re new to Hugo, check out a particular theme’s example content. Not every theme will display posts the way you might expect. If you get stuck, start with the theme’s configuration files (config.toml) and content directories. The Bearblog example site is a good start if you decided to use this theme.

Drafts and Future Posts

Note that the server doesn’t show blog posts with draft status by default. You can either set draft: true in the markdown file’s preamble or run the hugo server in draft mode. Besides draft status there are two more conditions that might prevent a post from being published, i.e. posts with a publication date in the future and posts with an expiry date4.

# compile posts with draft status
hugo server -D

# compile posts with a future publication date
hugo server -F

Publishing

To publish your site, run Hugo excluding drafts and future posts and push the changes to the remote repositories as described above.

Tracking Visitors

Hugo and various themes make it easy to add analytics. Start by creating a new account or sign in with an existing Google account at https://analytics.google.com/.

Then set up your “Property”, give it a name, and point it to the URL of the site you plan on tracking. Finally, click through the basic options until you land on a page with a Tracking Code which might look something like this: G-XXXXXXXXXX.

Edit Hugo’s configuration file, e.g. config.toml, and add the tracking code.

googleAnalytics = 'G-XXXXXXXXXX'

You should now be able to track visitors after publishing these changes. Then click on ‘Reports’ and then on ‘Realtime’ on https://analytics.google.com/ to track yourself visiting the page. This should work even if you run a server on your own machine and browse the site locally.

Customization

Hugo provides endless possibilities for customization. Virtually everything can be adjusted, including spacing, font sizes, colors, text alignment, and content interaction. The basic principle is to ‘override’ a theme’s default templates. It’s relatively easy to use a browser’s ‘inspection’ tools, click on the ⌖ , and hover over the element that you want to adjust. This should help you understand what CSS properties are being applied to this element. You might also be able to glean a few keywords that you can later use to search for.

Modifying a Theme

Most theme modifications can be done without modifying the theme’s source code repository. Hugo allows overwriting template files with your own modified version that you keep in a separate directory5.

For example, it’s relatively straight-forward to modify the copyright footer because most themes keep its template in a separate file. Let’s assume the footer template is kept in

themes/<THEME>/layouts/<SUBDIR>/footer.html

Obviously, the <THEME> and <SUBDIR> directories depend on your particular theme. If you have trouble finding the right file, browse your theme’s layouts/_default directory or grep for a keyword, e.g. grep footer -R. That should give you a rough idea of where to look.

Once we have identified the right file we make a copy of it and place it in:

layouts/<SUBDIR>/footer.html

You might have to create the directory structure first, e.g. mkdir -p layouts/<SUBDIR> before you copy the template with cp -iv themes/<THEME>/layouts/<SUBDIR>/footer.html layouts/<SUBDIR>/

This copy has precedence over the theme’s ‘default’ templates. You can edit the copy to your heart’s content. The changes should be immediately visible (or any errors) if you’re running the server (hugo server) at the same time. Reverting back to the original theme is as easy as deleting the file.

Override CSS

To override the theme’s style sheet, consult its documentation. Most themes support placing one or several CSS files in the assets directory.

assets/css/extended/custom.css

Images

Images help your site stand out. They are added to the static/ directory or any subdirectory within. For example, after creating a subdirectory (mkdir -p static/images/), images can then be referenced in markdown.

![Alt Description](/images/cover.jpeg)

Alternatively, some themes allow adding a caption (i.e. title) by using the Go syntax.

{{< figure src="/images/cover.jpeg" title="Caption text." >}}

Math Typesetting, Equations and LaTeX

Unfortunately, Hugo does not support math typesetting out of the box (2025-01-20). However, it is possible to add math support with a few modifications. Before proceeding, consult your theme’s documentation as the exact steps might depend on the particular theme.

First, we need to make sure that every site that needs to render math has the required scripts and stylesheets added to its header. Here we use KaTeX, but MathJax and others are also available.

Replace the respective version numbers (@0.16.21) with the latest and save the following chunk as layouts/partials/helpers/katex.html. (Don’t modify the theme’s folder, i.e. themes/<THEME>/layout/!)

<!-- Include external KaTeX resources for math typesetting. Set version numbers to latest. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.css" />
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.21/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body);"></script>
<!-- Add support for rendering inline math using a single '$'. -->
<script>
  document.addEventListener("DOMContentLoaded", function () {
    renderMathInElement(document.body, {
      delimiters: [
        { left: "$$", right: "$$", display: true },
        { left: "$", right: "$", display: false },
      ],
    });
  });
</script>

The second step is to get the theme’s template to include this chunk of code in the header of a page. Search for where your theme defines the “partials” to include in the header6.

For example, the Papermod theme already provides a mechanism to add custom code to the header by placing a file in the “override” layout directory (layout/partials/). We can thus avoid modifying the theme folder (themes/<THEME>/layout/partial/), which allows us to keep updating it with git.

Save the following code as layouts/partials/extend_head.html. Use partialCached for faster rendering.

<!-- extend_head.html -->
...
{{ if .Params.math }}{{ partial "helpers/katex.html" . }}{{ end }}
...

Finally, test that the math typesetting works by creating a post and running hugo server -D. Don’t forget to set math: true in the front matter (i.e. markdown header).

---
title: "DRAFT: Math Typesetting"
draft: true
math: true
---

Inline math equations, like $c = \sqrt{\frac{E}{m}}$ are cool.

The block equation below is cool, too:
$$
\frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(x-\mu)^2}{2\sigma^2}}
$$

The above should be displayed like this:

Inline math equations, like $c = \sqrt{\frac{E}{m}}$ are cool.

The block equation below is cool, too:

$$ \frac{1}{\sqrt{2\pi\sigma^2}} e^{-\frac{(x-\mu)^2}{2\sigma^2}} $$

Consult the KaTeX reference for supported function.

Custom Domain

Serving your site from a different domain than username.github.io is relatively straightforward7. You can buy a domain at any registrar, e.g. Google Domains.

Then go to ‘Pages’ section within the ‘Settings’ of your GitHub Pages repository. Here you can add your domain. Both, apex domain (example.com) or a subdomain (www.example.com or blog.example.com) are possible.

Then link your domain to GitHub Pages8. On Google Domains, your DNS resource records should looks like the following table with example.com replaced by the domain you own and username replaced with your GitHub Pages name.

Host Name Type TTL Data
example.com A 1 hour 185.199.108.153
185.199.109.153
185.199.110.153
185.199.111.153
www.example.com CNAME 1 hour username.github.io

For information and the latest IP addresses consult the GitHub documentation.

If everything is set up correctly and the record had enough time to propagate, you should see a green ‘✔ DNS check successful’ below the ‘Custom Domain’ section in the GitHub Pages settings.

Don’t forget to update the baseURL in your config.yaml to your custom domain, e.g. https://www.example.com/.

Social Media

There are various ways to optimize the display and searchability of your page on social media. You can get an idea of how your site looks when shared on social media with Metatags or LinkedIn’s post inspector.

What Next

Conclusion

We’ve barely scratched the surface of what Hugo can do. Setting up a basic static site is not as easy as some commercial offers like Squarespace and Wordpress. But if you’re comfortable on the command line and using an markdown editor this setup offers you full freedom over every aspect of your site. And it’s free!

FAQ

How do I update themes?

If you’ve used the git submodule add command to clone a theme repository into the themes/ folder you can preview possible updates

git fetch origin
git status

or pull updates and merge them automatically

git pull

In case you forked a theme, don’t forget to synchronize the fork before pulling the updates. More info here.

I forgot to add –recurse-submodules

You might not see changes appear on GitHub Pages if you forgot to add the --recurse-submodules switch when pushing the main source repository. You can rectify this by entering the submodules directory (e.g. cd public) and running git push there manually.

My public folder is not clean

The public folder tends to accumulte folders and files form drafts and upublished content when, for example, hugo -D is run. These can be removed with Hugo’s --cleanDestinationDir option. However, this might cause issues when the public/ folder is a git submodule.

fatal: in unpopulated submodule

If you can’t seem to be able to git add files in the public/ folder anymore, you might have to remove and add the public submodule again9. This might be an issue caused by running hugo --cleanDestinationDir.

git submodule deinit public
rm -rv public
git commit -m "remove folder public/"
git push

# add it back, add --force if necessary, do NOT run hugo yet!
git submodule add -b main https://github.com/username/username.github.io.git public
git add -u
git commit -m "add submodule 'public' back"
git push

Links


  1. Static sites can be hosted in a number of locations, such as Amazon S3, Azure, CloudFront, DreamHost, Firebase, GitHub Pages, GitLab Pages, Google Cloud Storage, Heroku, Netlify, Surge↩︎

  2. Creating a Blog with Hugo and Github in 10 minutes ↩︎

  3. Avoiding Git Problems When Installing a Theme to Hugo ↩︎

  4. Hugo: Draft, Future, and Expired Content ↩︎

  5. Customize a Theme ↩︎

  6. For example, the Papermod theme defines the header in themes/<THEME>/layout/partial/baseof.html.

    <!-- baseof.html -->
    ...
    <head>
        {{- partial "head.html" . }}
    </head>
    ...
    

    The head.html file in turn includes the extend_head.html “partial”.

    <!-- head.html -->
    ...
    {{- partial "extend_head.html" . -}}
    ...
    

    Creating this extend_head.html file in the local layout directory avoids touching any of the theme’s files. ↩︎

  7. Adding a Custom Domain to your Hugo Site on Github Pages↩︎

  8. Linking A Custom Domain To Github Pages ↩︎

  9. See also this link↩︎