Omegion

How to Host a Personal Blog with Hugo on GitHub Pages

Introduction

I run EKS clusters and edge infrastructure for a living, and I still don’t want to run or pay for any of it on my own time. That’s a rule I keep for side projects: no self-hosting, no Kubernetes, no Cloudflare Workers, nothing with a bill attached. When I started this blog in 2020 that rule meant two decisions: what to write it in, and where to put it. I landed on Hugo and GitHub Pages, and this post is the whole path from an empty folder to a live site, the same setup omegion.dev runs on today.

Prerequisites

  1. Hugo Extended , 0.146.0 or newer. On macOS: brew install hugo.
  2. Git.
  3. A GitHub account, and an empty repository created there to push this site into.

What is Hugo

Hugo is a static site generator written in Go. You write content in Markdown, Hugo runs it through a set of templates, and the output is plain HTML files, nothing server-side, no database, no request-time rendering. It ships as a single binary, no Node toolchain, no Ruby, nothing to install beyond the binary itself. I’m a Go person, so that was an easy sell on its own, but the part that actually matters for “no infrastructure” is the output: a folder of HTML files is the cheapest possible thing to host, because it doesn’t need a running process at all.

Create a new site

shell
❯ hugo new site myblog
Congratulations! Your new Hugo project was created in /Users/hakan/myblog.

Just a few more steps...

1. Change the current directory to /Users/hakan/myblog.
2. Create or install a theme:
   - Create a new theme with the command "hugo new theme <THEMENAME>"
   - Or, 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".

See documentation at https://gohugo.io/.

That scaffolds content/, layouts/, static/, archetypes/, and a hugo.toml with just a baseURL, title, and locale. cd myblog and turn it into a git repo:

shell
git init
git add .
git commit -m "Initial commit"

Write your first post

hugo new content uses the default archetype to stamp out a new page with frontmatter already filled in:

shell
❯ hugo new content posts/hello-world.md
Content "/Users/hakan/myblog/content/posts/hello-world.md" created
toml
+++
date = '2026-08-27T08:56:30+02:00'
draft = true
title = 'Hello World'
+++

Add a line or two of body text under that frontmatter, and flip draft to false once you actually want it live, Hugo skips draft content by default outside of local preview.

Build it locally

hugo server --buildDrafts runs a local dev server with live reload:

shell
❯ hugo server --buildDrafts
Watching for changes in /Users/hakan/myblog/{archetypes,assets,content,data,i18n,layouts,static}
Watching for config changes in /Users/hakan/myblog/hugo.toml
Start building sites …

WARN  found no layout file for "html" for kind "home": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.
WARN  found no layout file for "html" for kind "page": You should create a template file which matches Hugo Layouts Lookup Rules for this combination.

                  │ EN
──────────────────┼────
 Pages            │  5

Web Server is available at http://localhost:1313/
Press Ctrl+C to stop

Those WARN lines are expected right now. A freshly scaffolded site has no theme, so there’s nothing telling Hugo how to turn a post into a page. It’ll build, the server will start, and the browser tab will be blank. That gets fixed in the last section, once the deploy pipeline is in place.

For a real build instead of the dev server:

shell
❯ hugo --gc --minify

That writes the finished site to public/, the same directory the deploy workflow below picks up and publishes.

Deploy it to GitHub Pages with GitHub Actions

Push the repo to GitHub, then add a workflow that builds the site and deploys it on every push, this is the exact file I run for this blog:

yaml
# .github/workflows/deploy.yaml
name: Deploy Hugo site to Pages

on:
  push:
    branches:
      - master
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: "pages"
  cancel-in-progress: false

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: 0.165.0
    steps:
      - name: Install Hugo CLI
        run: |
          wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb
          sudo dpkg -i ${{ runner.temp }}/hugo.deb
      - name: Checkout
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v6
      - name: Build with Hugo
        env:
          HUGO_ENVIRONMENT: production
          HUGO_ENV: production
        run: |
          hugo --gc --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v5
        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@v5

fetch-depth: 0 in the checkout step matters if you turn on enableGitInfo later. Without full history Hugo can’t read a page’s last commit date. One setting has to happen by hand: in the repo’s Settings > Pages, set Source to GitHub Actions, not Deploy from a branch. That’s what lets actions/deploy-pages publish without a gh-pages branch or a committed CNAME file at all. A custom domain is set in that same Pages screen. GitHub writes and manages the CNAME file for you from there.

Push, and the Actions tab shows the build and deploy jobs running. A few minutes later the site is live at your GitHub Pages URL, or your custom domain if you set one, no server, no SSH key, no FTP, just Actions minutes GitHub already gives away for free on a public repo.

Add a theme

This is the piece that fixes the blank page and WARN lines from earlier. Hugo doesn’t ship with a default look, a theme is what supplies the actual templates. themes.gohugo.io has a few hundred to pick from, or you can build your own the way I did: hugo-omegion , the theme this blog runs. Sidebar-first layout, dark/light mode, client-side search with no backend, Mermaid diagrams, syntax-highlighted code blocks with a copy button, RSS, and the SEO/OpenGraph/JSON-LD metadata a blog actually needs. MIT licensed, free to take.

Whichever theme you pick, I’d recommend installing it as a Hugo Module instead of the classic git submodule under themes/, it’s one version pinned in go.mod like any other dependency, instead of a submodule pointer nobody remembers to update:

shell
hugo mod init github.com/<you>/myblog
toml
# hugo.toml
enableEmoji = true
enableGitInfo = true

[[module.imports]]
path = 'github.com/omegion/hugo-omegion'
shell
❯ hugo mod tidy
go: added github.com/omegion/hugo-omegion v1.0.16
hugo: collected modules in 607 ms

Rebuilding after that pulls in the theme’s layouts/, assets/, and static/, the WARN lines are gone and the page count jumps once there’s an actual template rendering each post:

shell
❯ hugo --buildDrafts
                  │ EN
──────────────────┼────
 Pages            │ 11

What a theme doesn’t inherit automatically is the site’s own config. enableGitInfo, the searchindex output format a search box needs, and goldmark.renderer.unsafe = true for raw HTML in posts all have to be set in your own hugo.toml, not the theme’s. hugo-omegion’s README spells out the exact block to copy. Skip one and the symptom is quiet: a blank search box, no “Updated” date on a post, nothing that looks like an error.

Bumping the theme later is hugo mod get -u github.com/omegion/hugo-omegion, a normal diff in go.mod and go.sum, the same motion as updating any other Go module.

Conclusion

That’s the whole setup: a new Hugo site, a first post, a GitHub Actions workflow that builds and deploys it on every push, and a theme on top. No server to run, no database, nothing to patch, and no bill at the end of the month. Push a commit and the site is live a few minutes later. For a personal blog, that’s exactly the amount of infrastructure I wanted: none.