Setting up a blog using Hugo
Important note: This guide uses the Microsoft hosted DevOps agent, before you can use this you need to request “hosted parallelism”. If you already have an Azure DevOps organization save yourself some time and request it now before continuing, this can be done by filling in a short form. More information about this can be found here: Configure parallel jobs
Alternatively: If you want to host the agent yourself, you can follow these docs to set one up: Azure Pipelines Agents
What is Hugo?
Hugo is an Open Source static web page generator, it allows you to create a website using simple markdown files. It converts the files to HTML and presents them with a theme of your choosing. Hugo has a wide variety of themes, which are pre-designed templates that determine the visual appearance of your website. You can pick from a wide range of themes available in the official Hugo Themes repository, or create your own custom theme.
Where can I get Hugo and themes:
- Installation Link: Installation | Hugo (gohugo.io)
- Themes: Complete List | Hugo Themes (gohugo.io)
Setup Hugo (Windows)
The fastest way to install Hugo on Windows is by using winget:
winget install Hugo.Hugo.Extended
Test Hugo installation
Once the installer is done, you can test if Hugo was installed correctly by using the following command:
hugo version
This should output: (your version may vary)
hugo v0.124.1-db083b05f16c945fec04f745f0ca8640560cf1ec+extended windows/amd64 BuildDate=2024-03-20T11:40:10Z VendorInfo=gohugoio
Checklist
Now we should have the following in place:
- ✅ Hugo Installed without configuration.
Next:
- Create and configure our “workbench” repository.
- Create and configure our “Public” repository as submodule.
- Picked a theme and download it.
- Configure Hugo.TOML
- Launch Hugo in preview mode
- Create a first post.
- Push content to public.
- Create azure static web app
- Add a custom domain.
Create repositories
Now that we have Hugo installed, we need to take a small detour before we can dive into picking a theme and creating some content. We need to create 2 repositories. We’re going to use Azure Devops to host our repositories.
Repository creation
Go to your DevOps page and navigate to the project you want to use for the repositories, we’re going to use the Blog
project.
Now use the drop-down next to Blog
and click on Manage Repositories
In this overview, we can quickly create our 2 needed repositories:
First repository
Our first repository will be WizardsOfTheCloudWorkBench
, this will contain our configuration, markdown files, our Hugo site, and themes.
Second repository
Our second repository will be named WizardsOfTheCloudPublic
this repository will be used as a submodule for the first repo.
Hugo creates a public folder that contains the files generated by Hugo. The content of this folder is your static website. By placing this folder in a “child repository”
we can separate the “live version” of the static website from the “development version”

Cloning our empty repositories
we should have these two repositories created now:
Clone the workbench repository
Navigate to the workbench repository and clone it to a place of your choosing.
We’re going to clone the repository using Visual Studio Code to a location of our choise.

Clone the public repository but …
We need to add it as a submodule by using the following commands.
Place the link on your clipboard:
public
folder will be created in the workbench folder when cloning it. The -b parameter states that we want to clone the main branch.
git clone "https://WizardsOfTheCloud@dev.azure.com/WizardsOfTheCloud/Blog/_git/WizardsOfTheCloudPublic" public -b main
Output:
Cloning into 'public'...
remote: Azure Repos
remote: Found 5 objects to send. (11 ms)
Unpacking objects: 100% (5/5), 899 bytes | 21.00 KiB/s, done.
with our repository cloned in the main repository, we need to “tell” git that this is a submodule by using this command:
git submodule init
git submodule update
Our folder structure should be as follows:
└───WizardsOfTheCloudWorkBench (our WizardsOfTheCloudWorkBench repo)
└───public (our WizardsOfTheCloudPublic repo)
This will look like this in Visual Studio Code:
File > add folder to workspace > browse to your public folder
Checklist
Now we should have the following in place:
- ✅ Hugo Installed without configuration.
- ✅ Create and configure our “workbench” repository.
- ✅ Our “Public” repository as submodule.
Next:
- Picking a theme and download it.
- Configure Hugo.toml
- Launch Hugo in preview mode
- Create a first post
- Push content to public
- Create azure static web app
- Add a custom domain
Configuring Hugo
With our needed prep work out of the way, we can create the actual site and pick a theme for it.
Create a new Hugo site in the workbench folder
hugo new site WizardsOfTheCloud
Output:
Congratulations! Your new Hugo site was created in V:\GIT\blog\WizardsOfTheCloudWorkBench\WizardsOfTheCloud.
Just a few more steps...
1. Change the current directory to V:\GIT\blog\WizardsOfTheCloudWorkBench\WizardsOfTheCloud.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Install a theme from https://themes.gohugo.io/
3. Edit hugo.toml, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content <SECTIONNAME>\<FILENAME>.<FORMAT>".
5. Start the embedded web server with the command "hugo server --buildDrafts".
With this done, we can move our public folder into the newly created site folder. I’m sure there’s a better way to do this, but as for now this seems to work. Note: VS Code might ask for the location of the public folder after moving it. Just point to the folder and we’re good to continue.
Our folder structure looks like this now:
└───WizardsOfTheCloud
├───archetypes
├───assets
├───content
├───data
├───i18n
├───layouts
├───public (this is our sub repository)
├───static
└───themes
Picking and installing a theme
Pick a theme that you like from the themes page, hit the download button and get the git URL from the GitHub repo:

Now that you have picked your theme we shall add it as a submodule, this way we can easily update the theme if a new version is available. If you don’t really care about theme updates, you can download the theme and place it in the theme directory, otherwise use the following commands to add the theme as submodule:
Git submodule add -b main https://github.com/526avijitgupta/gokarna.git themes\NameOfYourTheme
Output:
Cloning into 'V:/GIT/blog/WizardsOfTheCloudWorkBench/WizardsOfTheCloud/themes/gokarna'...
remote: Enumerating objects: 1835, done.
remote: Counting objects: 100% (653/653), done.
remote: Compressing objects: 100% (262/262), done.
remote: Total 1835 (delta 459), reused 508 (delta 389), pack-reused 1182
Receiving objects: 100% (1835/1835), 14.85 MiB | 10.14 MiB/s, done.
Resolving deltas: 100% (799/799), done.
Configuring Hugo.TOML
The hugo.toml
file is the main configuration file for your site, this file must be created at the root of your site should it not be automatically created. The following configuration example is based on the theme I picked. Your configuration might be different depending on the theme you have picked. In most cases, theme makers provide documentation and sample configuration files. More info about the configurations can be found here: Configure Hugo | Hugo (gohugo.io)
baseURL = "https://blog.wizardsofthe.cloud"
defaultContentLanguage = "en"
languageCode = "en"
title = "WizardsOfTheCloud"
theme = "gokarna"
# Automatically generate robots.txt
enableRobotsTXT = true
[menu]
[[menu.main]]
# Unique identifier for a menu item
identifier = "posts"
url = "/posts/"
# You can add extra information before the name (HTML format is supported), such as icons
pre = ""
# You can add extra information after the name (HTML format is supported), such as icons
post = ""
# Display name
name = "Posts"
# Weights are used to determine the ordering
weight = 1
[[menu.main]]
identifier = "tags"
name = "Tags"
url = "/tags/"
weight = 2
[[menu.main]]
identifier = "github"
url = "https://github.com"
weight = 3
# We use feather-icons: https://feathericons.com/
pre = "<span data-feather='github'></span>"
Checklist
Now we should have the following in place:
- ✅ Hugo Installed without configuration.
- ✅ Create and configure our “workbench” repository.
- ✅ Our “Public” repository as submodule.
- ✅ Picked a theme and download it.
- ✅ Configure Hugo.toml Next:
- Launch Hugo in preview mode
- Create a first post
- Push content to public
- Create azure static web app
- Add a custom domain
Start Hugo
Now we can run Hugo in preview mode, preview mode updates the content every time a change is detected to the config or content.
Hugo server
Output:
Watching for changes in V:\GIT\blog\WizardsOfTheCloudWorkBench\WizardsOfTheCloud\{archetypes,assets,content,data,i18n,layouts,static,themes}
Watching for config changes in V:\GIT\blog\WizardsOfTheCloudWorkBench\WizardsOfTheCloud\hugo.toml
Start building sites …
hugo v0.124.1-db083b05f16c945fec04f745f0ca8640560cf1ec+extended windows/amd64 BuildDate=2024-03-20T11:40:10Z VendorInfo=gohugoio
| EN
-------------------+-----
Pages | 9
Paginator pages | 0
Non-page files | 0
Static files | 28
Processed images | 0
Aliases | 0
Cleaned | 0
Built in 14 ms
Environment: "development"
Serving pages from disk
Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender
Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop
Now open your web browser and go to the URL provided by the Hugo output. (Alternatively, you can CTRL+click on the URL)

Creating the first post
This post you’re reading is a first post, and is formatted in Markdown, you can use notepad, VS Code, obsidian, and many other tools to create Markdown documents. There’s a great markdown cheat sheet here
This is how it looks in obsidian:
Ok, cool, so what do I do now?
Now you place those markdown files in the Posts\Name-of-your-post
:

Preview and publish
Preview:
when rerunning Hugo in preview mode, you can check if any changes are needed to your text and layout should this be the case you can just edit the markdown and save it, Hugo will update itself.
Hugo server


Publish
Remember the public repository we created? Well now we will make use of it.
When running Hugo
without the server parameter it will save the current state to the public repository/directory, this can now in its turn be pushed to the second repo we created in azure DevOps.
Hugo
Output:
Start building sites …
hugo v0.124.1-db083b05f16c945fec04f745f0ca8640560cf1ec+extended windows/amd64 BuildDate=2024-03-20T11:40:10Z VendorInfo=gohugoio
| EN
-------------------+-----
Pages | 20
Paginator pages | 0
Non-page files | 15
Static files | 28
Processed images | 0
Aliases | 1
Cleaned | 0
Total in 52 ms
When we take a look at the source control panel in VS Code, we can see changes made to our “public” repository:
Use the +
sign to add all changes and enter a description for your commit, then you can commit and push the content to the public repository hosted in Azure DevOps using the Sync Changes button:

Once this is done, if we take a look at our (public) repository in azure DevOps, we will see the following:
Now we should have the following in place:
- ✅ Hugo Installed without configuration.
- ✅ Create and configure our “workbench” repository.
- ✅ Create and configure our “Public” repository as submodule.
- ✅ Picked a theme and download it.
- ✅ Configure Hugo.TOML
- ✅ Launch Hugo in preview mode
- ✅ Create a first post
- ✅ Push content to public Next:
- Create azure static web app
- Add a custom domain
Create an Azure static web app to host our blog
With our sources in place we need a way to host our blog and a mechanism that will update, the content when add or modify our content. For this, we will be using an azure static webpage with an Azure hosted pipeline agent.
The create and update workflow will be as follows:
- Create / modify post
- Push changes to repository
- The push will trigger the pipeline agent
- The agent will download the public repository
- The content of the public repository will be placed as new content for our blog.
To create our web app, go to https://portal.azure.com and create a new resource and pick Static web app:
Configure the static web app as follows
(modify the settings to reflect your environment)
- resource group: rg-wotc-weu-blog
- static web app name: WizardsOfTheCloudBlog
- hosting plan: free
- deployment source: Azure DevOps
- the source repository: WizardsOfTheCloudPublic
- build preset: Hugo
- region: WestEurope
Static web app configuration


Once everything is filled in, we can deploy our static web app, this can take a couple of minutes.
when done we can go to the resource by clicking Go to resource
.
The deployment will trigger the build process, meaning that the DevOps Pipeline Worker will fetch the current content of our public repository and publish it.
Note that this step will fail unless you requested free hosted parallelism, as mentioned at the start of this guide.
If this is the case, you will get an error like this in DevOps > Pipelines > Runs:
A successful run will look like this:
Adding a custom domain name
If we take a look at the resource we created, we can spot an auto generated URL for our blog. While fully functional, it’s kinda ugly:
So to fix this, we can add a custom domain name, this can be done by clicking Custom Domains
in the settings blade:
Note: Make sure you created the correct DNS record (CNAME) for your domain before adding it here.
Add the URL you wish to use for your webpage and click next to start the validation


Our custom domain added to our web app, and now it should be available using our custom URL,
Congratulations, now that you’ve completed the setup process for your Hugo-powered blog, here’s a quick recap of what we’ve accomplished:
- ✅ Hugo Installed without configuration.
- ✅ Create and configure our “workbench” repository.
- ✅ Create and configure our “Public” repository as submodule.
- ✅ Picked a theme and downloaded it.
- ✅ Configured Hugo.TOML.
- ✅ Launched Hugo in preview mode.
- ✅ Created a first post.
- ✅ Pushed content to public.
- ✅ Created an Azure static web app.
- ✅ Added a custom domain.