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.

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:
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
- Join the Hugo forum.
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
-
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. ↩︎
-
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 theextend_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. ↩︎ -
Adding a Custom Domain to your Hugo Site on Github Pages. ↩︎