Step-by-step instructions to build and maintain a Hugo + PaperMod + AsciiDoc static site on GitHub Pages.
Prerequisites
Homebrew (macOS) or equivalent package manager
Git
Hugo Extended (installed via Homebrew)
brew install hugo
hugo versionVerify the output includes extended:
hugo v0.164.0+extended ...
Asciidoctor
Hugo requires asciidoctor to process .adoc files:
brew install asciidoctor
gem install asciidoctorVerify:
asciidoctor --version
Project Setup
Add the PaperMod Theme
git init
git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod themes/PaperModConfigure hugo.toml
baseURL = "https://example.com/"
locale = "en"
title = "My Site"
theme = "PaperMod"
paginate = 10
enableRobotsTXT = true
enableEmoji = true
[params]
description = "Site description"
author = "Your Name"
defaultTheme = "auto"
ShowReadingTime = true
ShowToc = true
ShowCodeCopyButtons = true
[taxonomies]
tag = "tags"
category = "categories"
[menu]
main = [
{ identifier = "posts", name = "Posts", url = "/posts/", weight = 10 },
{ identifier = "about", name = "About", url = "/about/", weight = 20 },
]
[security.exec]
allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^git$', '^node$', '^postcss$', '^tailwindcss$', '^asciidoctor$']
[markup.asciidocExt]
workingFolderCurrent = true
noHeaderOrFooter = true
safeMode = "unsafe"
attributes = { icons = "font", idseparator = "_", source-highlighter = "rouge", "rouge-style" = "monokai", experimental = true, sectanchors = true }Create Content Directory Structure
mkdir -p content/posts
mkdir -p static/images
mkdir -p content/archAdding Images
Images in Hugo are static files placed in the static/ directory. Only files inside static/ are copied to public/ during build.
For a Post with Images
mkdir -p static/images/my-post
cp /path/to/images/*.png static/images/my-post/Then in your AsciiDoc:

Hard-Linking Source Images
For content that is edited outside the project directory, use hard links so changes propagate automatically:
ln /path/to/source/image.png static/images/my-post/image.pngNote | Hard links share the same inode. Editing the source file updates the site copy. Hard links cannot cross filesystem boundaries. |
Setting Up GitHub Actions
Create .github/workflows/hugo-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: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- uses: peaceiris/actions-hugo@v3
with:
hugo-version: "latest"
extended: true
- name: Install asciidoctor
run: sudo apt-get update -qq && sudo apt-get install -y -qq asciidoctor
- run: hugo --minify
- 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:
- uses: actions/deploy-pages@v4Note | The submodules: true checkout option is required for the PaperMod theme. The asciidoctor install step is required because the GitHub Actions Ubuntu runner does not include asciidoctor by default. |
GitHub Pages Settings — Critical
The Pages source setting must match the deployment method:
Go to Settings > Pages
Under "Source", select "GitHub Actions" (not "Deploy from a branch")
If "Deploy from a branch" is selected instead, the workflow will succeed but the site will not update. The old site continues serving from whatever branch was set there. This is the most common deploy failure mode.
Tip | After switching from "Deploy from a branch" to "GitHub Actions", push an empty commit or re-run the workflow manually to trigger a fresh deploy: |
git commit --allow-empty -m "trigger deploy after Pages config change" git push origin main
Custom Domain
When using the GitHub Actions workflow, the custom domain is managed in Settings > Pages under "Custom domain". Enter your domain there (e.g. www.rocasta.com).
A CNAME file in the repo root is optional with the workflow-based deploy — GitHub stores the domain setting in the Pages configuration, not in the repo. However, keeping a CNAME file is harmless and documents the domain.
Local Development
Development Server with Live Reload
hugo server -DThis starts a server at http://localhost:1313/. The -D flag includes draft content.
Hugo watches for file changes and automatically refreshes the browser.
Gotchas and Troubleshooting
Hugo Blocks Asciidoctor
When using AsciiDoc, Hugo’s security policy blocks external executables by default.
Add asciidoctor to [security.exec] allow in hugo.toml:
ERROR asciidoctor is not whitelisted in policy security.exec.allow
Fix: Add ^asciidoctor$ to the allow list.
Asciidoctor Not Found
Hugo: asciidoctor ... unable to locate
Install asciidoctor:
brew install asciidoctor
Theme Not Found
ERROR module "PaperMod" not found
Make sure the theme submodule is initialized:
git submodule update --init --recursive
Image 404 in Development
If images don’t load in the Hugo dev server, check:
1. Images are in static/ directory (not assets/)
2. The :imagesdir: in AsciiDoc matches the directory path
3. Image filenames are case-sensitive
Hugo serves static/ content at the root. An image at static/images/foo.png is served at /images/foo.png.
Bundler 4.x Gemfile Resolution (Jekyll migration only)
Bundler 4.x walks parent directories to find a Gemfile. If running bundle exec in a subdirectory and a parent has a different Gemfile, bundler may resolve the wrong one.
Workaround: Set BUNDLE_GEMFILE explicitly:
BUNDLE_GEMFILE=/path/to/project/Gemfile bundle exec jekyll build
Deploy Succeeds but Old Site Still Shows
If the GitHub Actions workflow succeeds but the live site hasn’t changed, check:
Pages source setting — Settings > Pages > Source must be set to "GitHub Actions", not "Deploy from a branch". This is the #1 cause. When switching back to the Jekyll-Chirpy branch, the old "Deploy from a branch" setting may be silently reselected.
Custom domain cleared — Switching the Pages source can reset the custom domain field. Verify
www.rocasta.comis still entered under Settings > Pages > Custom domain.Diagnose with curl — Bypass browser cache and DNS:
curl -sI https://www.rocasta.com/ | grep -i 'x-cache\|age:'
If this shows a Hugo-generated page but the browser shows the old Jekyll site, the issue is browser cache (see below).
Check raw Pages URL — Bypass custom domain entirely to distinguish DNS from deploy failure:
https://<username>.github.io/
If the raw URL shows the new site but the custom domain doesn’t, it’s a DNS or CDN cache issue.
Browser Cache After Deploy
GitHub Pages serves with Cache-Control: max-age=600 (10 minutes). After a deploy, browsers and CDN edge nodes may hold stale content.
If curl shows the new site but the browser doesn’t:
Safari: Develop menu (enable in Settings > Advanced) → Empty Caches (Cmd+Option+E) → reload
Firefox: Cmd+Shift+R (hard reload), or Dev Tools → right-click reload → "Empty Cache and Hard Reload"
Any browser: Open a private/incognito window — this always fetches uncached content
CI Must Install Asciidoctor
The GitHub Actions Ubuntu runner does not include asciidoctor. Without it, the Hugo build will fail silently or produce no output for .adoc files.
Always add an explicit install step before the hugo command in the workflow:
sudo apt-get update -qq && sudo apt-get install -y -qq asciidoctor
Migration Notes
This site was migrated from Jekyll + Chirpy to Hugo + PaperMod in July 2026. The migration was driven by a Ruby version mismatch: Jekyll requires Ruby ~> 3.1, but macOS 13+ ships Ruby 4.0.5, and rbenv/rvm maintenance was not worth the overhead. Hugo is a single Go binary with no runtime dependencies.
Key migration lessons:
No incremental migration — There is no easy path to slowly transition a Jekyll site to Hugo. It’s a full rebuild: rewrite layouts, convert Markdown to AsciiDoc, restructure config, re-theme.
Back up the old branch — Before deleting or overwriting the Jekyll
mainbranch, save it:git branch -m main jekyll-chirpy git push origin jekyll-chirpy
PaperMod via submodule — Pinning via submodule (not
git clone) keeps the theme versioned with the site. Update withgit submodule update --remote themes/PaperMod.AsciiDoc over Markdown — Hugo supports both. AsciiDoc is more expressive for technical writing (admonitions, cross-references, attribute substitution) and avoids Markdown flavour lock-in.
GitHub Actions must install asciidoctor — The CI runner doesn’t have it pre-installed. See the gotcha above.
Pages source is the #1 deploy gotcha — Switching to a workflow-based deploy requires explicitly setting Settings > Pages > Source to "GitHub Actions".
Hard Links on macOS
Hard links cannot: - Link directories (use symlinks for directories) - Cross filesystem boundaries - Be tracked by Git (Git stores content, not links)
Hard links are useful for keeping source content in sync with the site build directory, but use symlinks or copies for directories.
