GitHub Pages is a natural fit for Hugo sites — both are built around static files, and GitHub Actions makes the build-and-deploy loop nearly effortless. Here’s how to wire it all together.

Prerequisites

  • A Hugo site in a GitHub repository
  • GitHub Pages enabled for the repo (Settings → Pages)
  • Pages source set to GitHub Actions (not a branch)

The Workflow

Create .github/workflows/deploy.yml:

name: Deploy Hugo site to 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
    env:
      HUGO_VERSION: 0.124.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@v4
        with:
          submodules: recursive
          fetch-depth: 0

      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v5

      - 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@v3
        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@v4

Breaking It Down

Trigger: The workflow runs on every push to main and can also be triggered manually via the Actions tab (workflow_dispatch).

Permissions: The pages: write and id-token: write permissions are required for the deploy step to work with the newer OIDC-based GitHub Pages deployment.

Concurrency: The concurrency group ensures that if a second push comes in while a deploy is running, the in-flight deploy is cancelled and the new one takes over. This prevents stale deploys from overwriting newer ones.

Hugo version: Pin a specific Hugo version rather than using latest. Unpinned versions can cause your build to silently break when Hugo ships breaking changes.

submodules: recursive: Required if your theme is installed as a Git submodule (common with themes from Hugo’s theme gallery).

fetch-depth: 0: Fetches the full git history, which Hugo uses to populate .GitInfo and .Lastmod on pages.

baseURL: The configure-pages action outputs the correct base URL for your repo. Passing it at build time ensures internal links work correctly whether you’re on a custom domain or the default username.github.io/repo-name/ path.

Enabling Pages in the Repo

Go to Settings → Pages in your GitHub repo:

  1. Under “Build and deployment”, select GitHub Actions as the source
  2. Save

The first time the workflow runs, it creates the github-pages environment and sets the deployment URL.

Custom Domains

If you have a custom domain, add a CNAME file to your static/ directory containing your domain:

example.com

Hugo copies it to public/ as-is, and GitHub picks it up automatically.

Troubleshooting

Build succeeds but styles are broken: Usually a baseURL mismatch. Check that the URL passed to Hugo matches where the site is actually served.

404 on all pages: Make sure Pages source is set to “GitHub Actions” not a specific branch.

Submodule errors: If you use a theme as a submodule, ensure submodules: recursive is set in the checkout step.