Getting Hugo to actually work on GitHub Pages

People keep asking about deploying static sites without paying for hosting. Hugo plus GitHub Pages is the route I ended up on after trying Netlify, Vercel, and a bunch of self-hosted solutions that fell apart during updates. What follows is how I actually did it, including the part where it broke and I fixed it. The basic workflow is simple enough, which is why it sounds like something you have to overcomplicate. You build the site locally with Hugo, push the generated files to a gh-pages branch, and GitHub serves them. That is it. The problem isn't the concept, it is the plumbing around it. First, install Hugo. I recommend the extended version because you will eventually need Sass support for theme compilation, and the non-extended version silently fails on SCSS without telling you why your styles are broken. snap install hugo on Ubuntu, or grab the binary from github.com/gohugoio/hugo/releases. The extended release is about 30MB larger but saves you three hours of debugging later.

Create a new GitHub repository. Doesn't need to be private, doesn't matter much. Then initialize Hugo inside it: hugo new site mysite --format yaml. The YAML format is default now, but being explicit keeps your teammates from arguing about config file styles. After that, pick a theme. I use PaperMod because it compiles fast and doesn't try to be anything other than what it is. The real work starts when you try to push. Here is the thing nobody warns you about: GitHub Pages runs Hugo on the server side by default now, but if you pre-build, you can skip that step entirely and save yourself a build queue wait. Pre-building means your CI does the heavy lifting instead of GitHub's shared runners choking on memory during large site compiles. Set up a GitHub Actions workflow. Create .github/workflows/deploy.yml in your repo root. This is what actually matters:

name: Deploy Hugo Site

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: 'latest'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy
        uses: peaceiris/actions-gh-pages@v4
        with:
          personal_token: ${{ secrets.GH_TOKEN }}
          publish_dir: ./public
          publish_branch: gh-pages

The important bits are the submodules: true and fetch-depth: 0

/, which ensures theme git submodules actually download. Without those two lines, your CI builds an empty site and you spend an hour wondering why everything is blank. I hit this exact problem on a Tuesday and it wasn't fun. Create a personal access token with repo scope, store it as a repository secret called GH_TOKEN, and you're set. The peaceiris/actions-gh-pages action pushes the built public folder to the gh-pages branch, which GitHub Pages serves automatically. Enable GitHub Pages from your repository settings. Go to Settings > Pages, select the gh-pages branch as the source, and hit save. Your site should be live at username.github.io/repo-name within about 30 seconds after the next push.

Get the Full Details

Deploy your Hugo Website on GitHub Pages
Deploy your Hugo Website on GitHub Pages

Here is where it gets tricky though. If you are using a custom domain, GitHub needs DNS validation and the CNAME setup is slightly different than you might expect. Put a CNAME file in your static directory with just your domain name in it. GitHub reads that file during the deploy step and adds it to the root of your published site automatically. Without it, your custom domain returns a 404 even though the base URL works fine. I also learned the hard way that Hugo caches aggressively. If you change a theme parameter and the output doesn't reflect it, run hugo --gc to force a clean rebuild. The cache lives in resources/ and can keep stale processed assets around for what feels like forever while you question your entire understanding of how static site generators work. One more edge case that trips people up: hugo mod. If your theme uses hugo module for dependency management, you need to run hugo mod tidy before building, and the GitHub Actions workflow needs Node.js installed alongside Hugo because some modules pull in npm packages. Add this step before the build step:

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Hugo Mod Tidy
        run: hugo mod tidy

This adds maybe 20 seconds to your CI time but prevents the "build succeeded but CSS is missing" problem that makes you think everything is fine until you actually visit the site on a phone. Scaling is basically free with this setup. GitHub Pages serves static files from their CDN, so traffic spikes don't hit you the same way they would a cheap VPS. I had a post that got linked from a major site and took roughly 40,000 requests in four hours. The GitHub Actions runner handled the build in about 90 seconds, and the deployed files served instantly. No configuration changes, no auto-scaling groups, nothing. It just worked because it was already designed for that. The downside is GitHub Pages has a build timeout of roughly 15 minutes on the free tier. If your site takes longer than that to compile, you're either doing something wrong or your content library is genuinely huge. In my experience, a normal blog with a few hundred posts compiles in under 60 seconds on GitHub's runners. Anything past 2,000 pages starts getting risky, and you should look at self-hosting or a service like Netlify at that point.

Another limitation: GitHub Pages doesn't support server-side features. No databases, no APIs, no dynamic content. If you need comments, you use a third-party service like Giscus orutter. I use Giscus because it maps GitHub discussions to comments and stores everything in the repo. It's slower than a dedicated comment system but it doesn't require you to manage another piece of infrastructure. For analytics, Plausible or umami work well if you self-host, or just use the GitHub Pages built-in stats if you only care about rough numbers. I stopped using Google Analytics years ago because the tracking script adds enough JavaScript to noticeably slow down page renders on mobile networks. The whole process, from writing a post to seeing it live, usually takes me about two minutes. That includes running hugo locally to preview, pushing to main, waiting for the GitHub Actions workflow to complete, and refreshing the browser. The workflow itself takes about 45 seconds on a typical commit.

Creating and Deploying a Blog with Hugo on Github Pages 🚀 | Rohtanza
Creating and Deploying a Blog with Hugo on Github Pages 🚀 | Rohtanza

If you're starting fresh and want the exact commands in order without reading through all of this, here's the minimum viable setup: 1. Install Hugo extended version
2. hugo new site .
3. Add your theme as a git submodule
4. Configure hugo.toml with your base URL
5. Create the GitHub Actions workflow file
6. Generate a personal access token
7. Push everything to GitHub
8. Enable Pages on the gh-pages branch That's it. The rest is just content.