Environment: Mac M4 24GB · macOS Tahoe 26.3
It’s my birthday. Most people go out for dinner or, I don’t know, have cake. I’m migrating my blog from Pelican to Hugo. I’ve been meaning to do it for a while and apparently “a Saturday where I turn another year older” is the motivation I needed. Happy birthday to me — here’s a static site generator.
Hugo is written in Go, it’s absurdly fast, and it deploys cleanly to GitHub Pages via Actions. Here are my notes.
Install Hugo
On macOS, Homebrew is the path of least resistance:
brew install hugo
hugo version
You want the extended version (Homebrew gives you this by default). It includes SCSS/SASS support, which most themes need.
Create the Site
mkdir -p ~/code/heyjdp.github.io
cd ~/code/heyjdp.github.io
hugo new site . --force
The --force flag lets Hugo initialise in a non-empty directory (useful if you’ve already got a README or .gitignore in there).
This gives you the skeleton:
.
├── archetypes/
├── content/
├── data/
├── hugo.toml
├── layouts/
├── static/
└── themes/
Initialise Git
git init
git add -A
git commit -m "chore: hugo new site"
Add a Theme
I’m using BeautifulHugo [1]. It’s clean, readable, handles code blocks well, and doesn’t try to do too much. Add it as a git submodule so your builds are reproducible:
git submodule add https://github.com/halogenica/beautifulhugo.git themes/beautifulhugo
Pin it to a specific commit so you’re not surprised by upstream changes:
cd themes/beautifulhugo
git log -1 --format="%H" # grab the latest SHA
cd ../..
Now tell Hugo to use it. Edit hugo.toml:
baseURL = "https://heyjdp.github.io/"
languageCode = "en-us"
title = "Dr Jas Powell"
theme = "beautifulhugo"
pygmentsCodeFences = true
pygmentsUseClasses = true
[Params]
subtitle = "Remote CTO · Hands-on Engineer · Team Builder"
selfHosted = true
useHLJS = false
[taxonomies]
category = "categories"
tag = "tags"
[[menu.main]]
name = "Blog"
url = "/"
weight = 1
[[menu.main]]
name = "About"
url = "/page/about/"
weight = 2
[[menu.main]]
name = "Tags"
url = "/tags/"
weight = 3
The selfHosted = true flag is important — it tells BeautifulHugo to serve fonts and icons locally instead of pulling from Google Fonts and CDNs. No third-party requests, no GDPR headaches.
Create Your First Post
mkdir -p content/post/hello-world
cat <<'EOF' > content/post/hello-world/index.md
---
title: "Hello World"
date: 2023-10-07
draft: false
tags:
- meta
description: "First post. Testing the plumbing."
---
First post. If you can read this, Hugo and GitHub Pages are working.
EOF
Hugo uses page bundles — each post gets its own directory under content/post/. This keeps images and other assets co-located with the post that uses them.
Build and Preview Locally
hugo serve
Browse to http://localhost:1313. Hugo’s live reload is genuinely fast — save a file and the browser updates before you can alt-tab to it.
To do a full production build (minified, no drafts):
hugo --minify
Output lands in public/. Don’t commit this directory — GitHub Actions will build it for you.
Add it to .gitignore:
echo "public/" >> .gitignore
echo ".hugo_build.lock" >> .gitignore
Deploy with GitHub Actions
This is the bit that makes it all worth it. No ghp-import, no separate branch, no manual pushing of built files. Just push to main and GitHub handles the rest.
Create the workflow file:
mkdir -p .github/workflows
And add .github/workflows/hugo.yml:
name: Deploy Hugo site to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: 'latest'
extended: true
- name: Build
run: hugo --minify
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
with:
path: ./public
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
A few things to note:
submodules: recursive— essential, otherwise your theme won’t be checked out and the build will fail with a cryptic error about missing layouts.fetch-depth: 0— needed if you useenableGitInfo = truein your config (Hugo can show last-modified dates from git history).extended: true— you want Hugo Extended for SCSS support.
GitHub Repo Settings
One manual step. Go to your repo on GitHub:
Settings → Pages → Build and deployment → Source → GitHub Actions
This tells GitHub to use the Actions workflow instead of the legacy branch-based deployment. If you skip this step, your workflow will run but nothing will actually deploy.
Push and Watch It Go
git add -A
git commit -m "feat: initial hugo site with github actions deployment"
git remote add origin git@github.com:heyjdp/heyjdp.github.io.git
git push -u origin main
Give it a minute or two. Check the Actions tab in your repo to watch the build. Once the green tick appears, your site is live at https://heyjdp.github.io.
What I Like About This Setup
- Speed. Hugo builds this site in under 100ms. Pelican took seconds. For a small blog that’s academic, but it makes the local dev loop genuinely pleasant.
- Single binary. No virtualenv, no pip dependencies, no Python version juggling.
brew install hugoand you’re done. - Git submodule for the theme. Pin it to a commit. Reproducible. No surprises.
- GitHub Actions. Push to main, site deploys. No
ghp-import, no separategh-pagesbranch to manage.
The whole thing took about an hour to set up from scratch, and most of that was fiddling with the hugo.toml config until I was happy with the menu layout.