Hugo, Github, and Azure Static Web Apps - Perfect For Blogs
Hero image credit: Photo by Rodrigo Santos
I wasted a lot of time on getting my blog running on WordPress. I spent years and years on effort, and thousands of dollars all-told on hosting over the years. And I finally came to the conclusion that it was all a complete waste of time. I thought I needed all the fancy features that WordPress offered with it’s deep, involved engine and database back end. But you really don’t need all that. Using a simple static website generator like Hugo works pretty much just as well for 99% of us people who just want a place to host our blog posts. And WordPress hosting can get really expensive, really fast, if you’re not careful. So just drop all that. You can host a blog, complete with search functionality, and do it for free. Let me walk you through my setup.
Hosting
I’m hosting my site on Azure Static Web apps. It’s easy to set up, and there’s a free tier available. I have yet to come even close to hitting the limits and my site gets a couple thousand unique users, and hundreds of thousands of hits per month. For a static website, it works great, and it integrates well with Github. In fact, when you set up your Static Web App, you can point it at a Github repo and tell it you’re hosting a Hugo site and it will handle all the basic setup and config for you. However, there are a couple of pieces that are a little broken when you set up your deployment pipeline in Github that keep it from working correctly out of the box. But I’ll get to that in a minute.
What’s more, even at the free tier, Azure Static Web Apps support custom domains and they provide free SSL certs for those custom domains, so there’s no excuse for you to not have your site serve over secure TLS connections.
Add to that the fact that it supports a lot of other Azure features if you do need a more complex set up, like custom routes, preview environments with traffic splitting, database connections, and Azure functions. And if you do need to upgrade to a paid plan for something like custom auth and private endpoints, it’s not that much.
Hugo
Hugo is a great tool for building static web apps. And unlike many of the static web app tools I tried (and I tried at least 8 different ones), it actually works on Windows without any issues whatsoever. In fact, it was the ONLY tool I tried that worked without issues.
And if you’re like I was and was migrating from WordPress, the conversion was very straightforward. There is a tool called wordpress-to-hugo-exporter that makes it very easy to take your WordPress content and convert them to markdown for use in Hugo. You’ll probably have to do some tweaking, but it gets you 80%-90% of the way there.
Like most of these tools, there are a bunch of templates available for you to use if you’re not a design-savvy person (which I’m not). The template I use is called toha. I like the style and look and it’s pretty easy to customize it.
I do all my development in VS Code. You can run Hugo from the terminal in Code, and for most changes it supports hot-reload, so you don’t have to restart the local instance to see those changes. The only time you have to restart is if you make changes to the core config files.
The biggest issue with Hugo, as will all things JavaScript based, is version issues. You can easily get into situations where the various dependencies that Hugo or your theme rely on don’t support newer versions of things and you get build errors and things don’t run. For instance, the newest version of Hugo had a number of breaking changes for certain features, which completely broke my build pipeline. Thankfully, you can force the pipeline to stick with a certain version of Hugo.
The exact structure of your Hugo app will depend on the template you chose to use, but in general you’ll have folders for assets, content, data, internationalization, images, layout templates, and static resources. You’ll have to follow whatever guides and examples your template creator provides to know what kinds of content goes where. It’s one of the bigger downsides to Hugo, as there really isn’t a specific requirement to how it all gets structured, so it can vary from template to template.
It can be a bit confusing at times. For instance, with the setup I’m using, images like backgrounds for the main pages have to be in the main images folder and images for content posts have to be in a subfolder of the post itself, while images for the dynamically generated blocks have to be in an images folder that sits in the assets core folder, and images for certain content in the main pages have to be in the images subfolder of the static core folder. It’s not ideal, but you get used to weird quirks like that. Like I said, just follow the example and documents of the template creator and you’ll be fine.
Another issue with Hugo is that my experience (and others) with getting support in the forums is a mixed bag. Sometimes people are helpful, sometimes they make you feel stupid for not being an expert and already knowing the answer yourself. It’s pretty much just like StackOverflow.
You will probably have a central config file called something like hugo.yaml. This file is where a lot of your key configuration for the site will be placed. It’s structure will look something like this:
# Manage languages
# For any more details, you can check the official documentation: https://gohugo.io/content-management/multilingual/
languages:
en:
languageCode: en
languageName: English
title: "Barret Codes"
weight: 1
# default language for the content
defaultContentLanguage: en
# Allow raw html in markdown file
markup:
goldmark:
renderer:
unsafe: true
tableOfContents:
startLevel: 2
endLevel: 6
ordered: false
# At least HTML and JSON are required for the main HTML content and
# client-side JavaScript search
outputs:
home:
- HTML
- RSS
- JSON
# Enable global emoji support
enableEmoji: true
# Site parameters
params:
author:
name: Barret
# Background image of the landing page
background: /images/pexels-pixabay-270348.jpg
# Provide logos for your site. The inverted logo will be used in the initial
# transparent navbar and the main logo will be used in the non-transparent navbar.
logo:
main: /images/barret-blake-logo.svg
inverted: /images/barret-blake-logo.svg
favicon: /images/favicon.png
This is a partial clip from my own config. But you can see things like logos, background image, supported languages, etc. The config files can get quite long, depending on your template.
The best part of Hugo is it’s easy to customize. For instance, the template I use did not support ALT text for the hero image of a blog post out of the box. It was a simple matter for me to locate the template for a blog post in the layouts folder and update the HTML to include an alt block to the img tag.
BEFORE:
<img class="card-img-top" src='{{ partial "helpers/get-hero.html" . }}'>
AFTER:
<img class="card-img-top" src='{{ partial "helpers/get-hero.html" . }}' alt="{{ if $.Params.heroalt }}{{ $.Params.heroalt }}{{ else }}{{ "hero image" }}{{ end }}">
Then all I had to do was add the heroalt parameter to the header of each of my blog post markdown files along with appropriate alt text for the image.
One other thing to remember is that most of the templates will have a full sample site in a subfolder. That’s a lot of extra files that you don’t need when you deploy your own site, so be sure to delete that subfolder. You can always make a copy of it somewhere to use as a sample to learn from if you want.
Github
Github Actions provide the perfect tool for deploying your blog to Azure. The biggest thing with it is that the default setup that you get when you create the Static Web app doesn’t work right. At least, that used to be the case. Maybe it’s fixed now. But just in case it isn’t, here’s a copy of my current version so you can see an example that I know works.
I’ve added comments to explain each section
name: Azure Static Web Apps CI/CD #action name
on: #when to trigger a build and deploy. You can have multiple triggers for your action
push: #when code is pushed to the main branch
branches:
- main
pull_request: #when a pull request is completed
types: [opened, synchronize, reopened, closed]
branches:
- main
schedule: #on a schedule - doesn't actully run the build and deploy, it just checks to see if a build and deploy is needed
- cron: "0 15 * * *"
jobs: #the jobs to run for the action
build_and_deploy_job: #build and deploy my code
# if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest #the environment to run in
name: Initialize job
steps: #the steps to run in order
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
### Using alt build process
- name: Setup Hugo #define the Hugo version to use
uses: peaceiris/actions-hugo@v3 #the default Hugo build has some issues. User peaceiris created an improved version that works great
with:
hugo-version: '0.145.0' #the version of Hugo to build with. If it's left out, it uses the latest version. My current template version doesn't build with Hugo versions newer than 0.145, so I force it to that version
extended: true #use the extended features
- name: Cache Hugo modules #Get the Hugo modules
uses: actions/cache@v4
with:
path: public
key: ${{ runner.os }}-hugo-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-hugo-
- uses: actions/setup-node@v4 #Define the node/npm version to use
with:
node-version: '20'
cache: 'npm'
# The action defaults to search for the dependency file (package-lock.json,
# npm-shrinkwrap.json or yarn.lock) in the repository root, and uses its
# hash as a part of the cache key.
# https://github.com/actions/setup-node/blob/main/docs/advanced-usage.md#caching-packages-data
cache-dependency-path: '**/package-lock.json'
- run: npm ci
- name: Build site with Hugo #Run the actual Hugo build
run: hugo --minify --cleanDestinationDir
### Oryx is buggy bypassing the build step for now
- name: Deploy #The deploy step. This is the part that's broken and the reason for using the peaceiris Hugo build steps. So we only use it for deploying and not building
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_sanitized }} #You need a token to connect Github to your Azure Static Web app
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "public" # App source code path
api_location: "" # Api source code path - optional
output_location: "public" # Built app content directory - optional
skip_app_build: "true"
skip_api_build: "true"
###### End of Repository/Build Configurations ######
close_pull_request_job: #If there is an associated PR, close it
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_sanatized }}
action: "close"
Conclusion
That’s really all you need to know to get started. Learning Hugo is fairly straightforward and left up to you. And if you’ve done anything with HTML and JavaScript, it’s very straightforward. But even if you haven’t, the tutorials are excellent.