Originally written December 2022 using the Ananke theme and config.toml. Rewritten April 2026 after migrating to PaperMod with dual CI/CD and a push mirror.

Why Hugo and GitLab Pages

Hugo is a static site generator written in Go. It compiles markdown files into a complete website in milliseconds, requires no runtime, no database, and no server-side processing. The output is plain HTML, CSS, and JavaScript, served by GitLab Pages (and now also GitHub Pages) at zero cost.

Naming the repository username.gitlab.io makes the site available at https://username.gitlab.io/ automatically. Pages is enabled in the project settings, access control is set to “Everyone,” and everything in the public/ build directory is served to visitors.

Theme: PaperMod

The site uses PaperMod, a clean, fast, responsive Hugo theme with dark mode, search, reading time, table of contents, and code copy buttons out of the box. It replaced the original Ananke theme in April 2026.

PaperMod is added as a Git submodule:

git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

Configuration

Hugo configuration lives in hugo.toml (Hugo dropped support for config.toml as the default name in newer versions). The key settings:

baseURL = "https://universalamateur.gitlab.io/"
title = "Universalamateur"
theme = "PaperMod"
paginate = 10

enableRobotsTXT = true
buildDrafts = false

[params]
  env = "production"
  author = "Falko Sieverding"
  defaultTheme = "auto"        # light/dark follows system preference
  ShowReadingTime = true
  ShowBreadCrumbs = true
  ShowCodeCopyButtons = true
  ShowPostNavLinks = true
  showtoc = true

[taxonomies]
  tag = "tags"

[outputs]
  home = ["HTML", "RSS", "JSON"]  # JSON enables PaperMod's search

The [outputs] section is important: PaperMod’s built-in search requires a JSON index, which Hugo only generates if you explicitly request it.

Directory Structure

.
├── hugo.toml                      # Site configuration
├── .gitlab-ci.yml                 # GitLab Pages deployment
├── .github/workflows/hugo.yml     # GitHub Pages deployment
├── content/
│   ├── _index.md                  # Landing page
│   ├── about/index.md             # About page (leaf bundle)
│   ├── post/                      # Blog posts
│   │   ├── the-token-salary-tipping-point.md
│   │   └── ...
│   ├── archives.md                # Archive page (PaperMod layout)
│   └── search.md                  # Search page (PaperMod layout)
├── static/                        # Images and other assets
│   └── images/
└── themes/PaperMod/               # Theme (git submodule)

Posts go in content/post/ with lowercase hyphenated filenames. Each post uses this frontmatter:

---
title: "Post Title"
date: 2026-04-09
draft: false
tags: ["AI", "DevSecOps"]
summary: "One-line summary shown in post listings."
showtoc: true
---

Dual CI/CD: GitLab and GitHub Pages

The site deploys to both GitLab Pages and GitHub Pages from the same repository. GitLab is the primary, and a push mirror syncs every commit to GitHub automatically.

GitLab CI

image: debian:bookworm-slim

variables:
  HUGO_VERSION: "0.160.0"
  GIT_SUBMODULE_STRATEGY: recursive

pages:
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends wget ca-certificates
    - wget -q -O hugo.deb "https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb"
    - dpkg -i hugo.deb
  script:
    - hugo --minify
  artifacts:
    paths:
      - public
  rules:
    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH

The debian:bookworm-slim image matters: Hugo Extended requires glibc, which Alpine does not provide. An earlier version used Alpine and failed with a cryptic binary compatibility error.

GitHub Actions

The GitHub workflow (.github/workflows/hugo.yml) runs the same Hugo version, triggered by the push mirror:

name: Deploy Hugo site to GitHub Pages
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      HUGO_VERSION: 0.160.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
      - uses: actions/checkout@v4
        with:
          submodules: recursive
      - uses: actions/configure-pages@v5
      - run: hugo --minify --baseURL "${{ steps.pages.outputs.base_url }}/"
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./public
  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
    steps:
      - uses: actions/deploy-pages@v5

Push Mirror Setup

The push mirror is configured in the GitLab repository under Settings > Repository > Mirroring. The mirror URL uses a GitHub fine-grained personal access token with contents: write scope on the target repository. Every push to GitLab automatically propagates to GitHub, which triggers the GitHub Actions workflow.

Local Development

Prerequisites

# macOS
brew install hugo git

# Linux (Debian/Ubuntu)
sudo apt install git
# Install Hugo Extended from GitHub releases (apt version is usually outdated)
wget -O hugo.deb "https://github.com/gohugoio/hugo/releases/download/v0.160.0/hugo_extended_0.160.0_linux-amd64.deb"
sudo dpkg -i hugo.deb

Clone and Run

git clone https://gitlab.com/UniversalAmateur/universalamateur.gitlab.io.git
cd universalamateur.gitlab.io
git submodule update --init --recursive
hugo server -D

The -D flag includes draft posts. The local server runs at http://localhost:1313/ with live reload.

Creating a New Post

hugo new post/my-new-post.md

This creates a file in content/post/ with the default frontmatter. Set draft: false when ready to publish, commit, and push. Both pipelines handle the rest.