↓ Skip to main content

From WordPress to Hugo: My Setup Explained

·4 mins
Table of Contents
Ten years of WordPress taught me enough to know when to leave. Tuning PHP configs, switching to LiteSpeed, keeping plugins patched — for a blog. Every plugin is another attack surface, and the database is a bottleneck a site with no logged-in users doesn’t need.
Updated September 2026

The site spent a year on Hugo, moved to Astro in November 2025, and came back to Hugo and Blowfish in September 2026. It now builds on Cloudflare Workers Builds rather than GitHub Actions; the pipeline below is what it ran on the first time, and the reasoning that got it off WordPress hasn’t changed.

WordPress vulnerability statistics
Known vulnerabilities, tracked at wpscan.com.

The honest second reason is that the alternative is more interesting to run. A git push triggers a build, and the result deploys globally. Writing a post is a markdown file in a repo. If you spend your day in a terminal, that’s the version of a website you actually want to own.

Hugo
#

merox.dev ran on Hugo for over six months before I wrote this. Static files, no backend, free hosting on GitHub Pages, all of it in git. Hugo is Go, so builds are fast, and the output is clean enough that SEO scores came in above 80% with no extra work.

Hugo Website

I went with the Blowfish theme. Forking it first and adding the fork as a submodule is what lets you take upstream updates later without losing your own changes:

git submodule add https://github.com/YOURUSERNAME/blowfish themes/blowfish

The real cost of Hugo is that there is no dashboard. If you’re not comfortable in a terminal and in Markdown, you will hate this, and WordPress is the correct answer for you.

GitHub Pages and Cloudflare
#

Source in a private repo, output in a public one:

flowchart LR
  src["private repo
Hugo source"] -- push --> ci["GitHub Actions
hugo --minify"] ci -- deploy key --> pub["public repo
gh-pages"] pub --> pages["GitHub Pages"] cf["Cloudflare DNS"] --> pages
  1. 1

    Private repo

    Push the Hugo project to it.
  2. 2

    Public repo

    Enable GitHub Pages on branch gh-pages, folder /.
  3. 3

    Cloudflare

    A CNAME for www → yourusername.github.io, and for the apex either a flattened CNAME to the same name or A records for the GitHub Pages IPs.

Deploys with GitHub Actions
#

Edit in VS Code, push to the private repo, Actions builds and pushes the output to the public one. Crossing the repo boundary needs a deploy key — the default GITHUB_TOKEN can’t write to a different repository.

Generate the pair:

ssh-keygen -t ed25519 -C "github-deploy" -f deploy_key -N ""

The public half goes on the public repo, under Settings → Deploy Keys, with write access ticked. The private half goes on the private repo, under Settings → Secrets, as PRIVATE_KEY. Getting these two backwards is the usual reason the first run fails.

.github/workflows/deploy.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
name: github pages

on:
  push:
    branches:
      - master

permissions:
  contents: read

jobs:
  deploy:
    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:
          deploy_key: ${{ secrets.PRIVATE_KEY }}
          external_repository: meroxdotdev/merox.dev
          publish_branch: gh-pages
          publish_dir: ./public
          cname: merox.dev

submodules: true matters — without it the theme isn’t checked out and the build fails on a missing layout rather than on anything that mentions submodules. cname writes the CNAME file into the published branch on every deploy. A CNAME at the root of the source repo never reaches ./public, so the first deploy removes it from gh-pages and the custom domain drops. The job itself only reads this repo — the push to the other one goes over the deploy key, not GITHUB_TOKEN.

What it scored
#

No SEO configuration beyond what the theme ships with.

Measured with freetools.seobility.net and seositecheckup.com.

Six months in, the maintenance list was empty. No plugin updates, no PHP version to chase, nothing listening that could be broken into — the whole site is files on a CDN, and the only thing that can go wrong is a build I pushed myself.