After years with WordPress, I have moved my blog to Hugo. The trigger was not a single event but a sober assessment: I no longer wanted PHP in live operation, no database that constantly needs patching, and above all I wanted a system in which AI tools can edit, translate and maintain content automatically without me having to worry about attack surfaces.

In this post I first explain why I chose Hugo over WordPress, with a focus on security and AI automation. After that comes the practical guide: how I set up Hugo locally on Windows, including the PaperMod theme, and how the site gets onto the server via GitHub Actions.

Hugo vs. WordPress: Why I Switched

WordPress runs a large part of the web, and that is precisely why it is the preferred target of automated attacks. Its real weakness lies less in the WordPress core than in the ecosystem around it. According to Patchstack’s report State of WordPress Security in 2026, 11,334 new vulnerabilities were found in the WordPress ecosystem in 2025, 42 percent more than the year before. 91 percent of them were in plugins, 9 percent in themes and only six in the WordPress core itself. For 46 percent of the vulnerabilities, no patch was available at the time of disclosure. Speed is particularly critical: according to Patchstack, serious vulnerabilities are exploited for the first time a median of just five hours after disclosure, and about half of them within 24 hours. If you don’t update practically all the time, you are too slow.

This susceptibility can be explained by the architecture. A WordPress site usually runs with many plugins, and each of them is independent code from a different author with its own update cycle. Every plugin is an additional door into the installation, and every door needs its own lock that has to be maintained permanently. On top of that, attackers now also use AI to find and exploit known vulnerabilities faster.

Why Hugo Is Structurally Hard to Attack

Hugo takes a fundamentally different approach. There is no database server, no PHP and no admin interface reachable from the internet. Hugo generates finished static HTML files from Markdown files, templates and front matter, which are then simply delivered by a plain web server. So no program code runs on the server in which a vulnerability could be exploited: no SQL injection, no PHP remote code execution, no vulnerable plugin ecosystem. Essentially only the web server itself remains open to attack, and you have to harden it and keep it up to date anyway, regardless of which CMS sits behind it.

This does not mean that Hugo sites are invulnerable. But the entire category of attacks that makes WordPress so vulnerable disappears, because the attack surface for it simply does not exist.

Speed: Delivering Instead of Rendering

One point you notice immediately in everyday use is speed. WordPress assembles every page anew on every request: PHP is executed, the database is queried, the result is rendered and only then delivered. For every single visitor, again and again. Caching plugins mitigate this, but at heart they are just a plaster over an elaborate architecture.

Hugo reverses the principle. The pages are generated once during the build; after that the server only delivers finished HTML files, without rendering, without database queries, without server-side processing. There is hardly a faster way to deliver a website, and it can be distributed worldwide via a CDN with no effort. Then there is the build speed itself: Hugo is written in Go and is one of the fastest static site generators. A blog with a few hundred pages is built in a few seconds. For the local workflow this means: I save a change and see it in the browser practically at the same moment.

The AI Factor: Markdown as an Automation Interface

The third big advantage for me lies in how the content is stored. WordPress stores posts in a relational database that an AI can only access through an API or a plugin, with all the associated risks and dependencies. Hugo content, by contrast, lives as simple Markdown files in the file system, versioned with Git.

This enables a completely different kind of automation: an AI can read and edit Markdown files directly, translate posts into several languages, adjust front matter consistently or maintain SEO metadata. Each of these changes can be traced as a Git commit instead of disappearing invisibly into a database. Changes can be reviewed via pull request before publication and, if in doubt, undone with a Git revert. In my case, for example, two small local Python scripts automatically translate every post into English and Spanish and place the translation as index.en.md and index.es.md next to the original. For a workflow in which AI tools regularly work on the content, this is a much more robust and transparent foundation than a CMS with a database in the background.

The Drawbacks of Hugo by Comparison

The switch also comes at a price. Hugo has no editorial interface. Authors without technical background need an additional headless CMS or help with deployment. Dynamic features such as comments, search or forms cannot be handled on the server; they can only be added via external services or JavaScript. Search in PaperMod, for example, runs entirely in the browser using a JSON index that Hugo generates during the build. And there is always a build step between a change and its publication on the live server. Locally this hardly matters: thanks to live reload, the built-in development server shows every change in the browser immediately, often faster than the preview in the WordPress editor.

For a technically savvy one-person blog focused on performance, security and AI-assisted maintenance, the advantages clearly outweigh the drawbacks for me. For an editorial team without Git knowledge, the decision would have turned out differently.

Prerequisites: Installing Git and Hugo

Before you start, you need two tools on your Windows computer, which serves as the development and test environment in this setup. I assume that neither of them is installed yet.

1. Git for Windows

Git is needed for version control and for including the theme as a submodule. You can download the official installer here:

https://git-scm.com/download/win

Run the downloaded .exe and go through the installation wizard. The default settings are sufficient. Just make sure that Git is added to the system PATH; this is already the case in the default selection.

2. Hugo (Extended Edition)

Hugo is the actual static site generator. I use the Extended Edition. It additionally includes LibSass. This is a software library that translates Sass into CSS. Sass is an extension of CSS that lets you write stylesheets in a shorter and clearer way, for example with variables for colors or with nested rules. Browsers do not understand Sass, so Hugo has to convert it into plain CSS during the build. SCSS is the syntax of Sass that is common today: it looks like CSS with curly braces and semicolons, and the files end in .scss. PaperMod itself works with plain CSS, but many other themes require the Extended Edition. If you use it from the start, you can switch themes later without reinstalling Hugo. The suffix +extended in the version output tells you whether the right variant is installed.

You can find the current version on the official releases page. The page lists several dozen files for each version. For a normal Windows PC you need the file following the pattern hugo_extended_<version>_windows-amd64.zip, so for version 0.167.0 that is hugo_extended_0.167.0_windows-amd64.zip. The file with withdeploy in its name additionally contains functions for uploading to cloud storage such as AWS S3 and is not needed here. The files without extended in their name are the Standard Edition without LibSass:

https://github.com/gohugoio/hugo/releases

Extract the ZIP file into a permanent folder, for example C:\Hugo\Bin. For the hugo command to work in every command prompt, you have to add this folder to the PATH environment variable:

  1. Search for “environment variables” in the Start menu and open “Edit the system environment variables”
  2. Click “Environment Variables…”
  3. Under “System variables”, select the variable Path and click “Edit”
  4. Click “New” and enter the path to the Hugo folder, e.g. C:\Hugo\Bin
  5. Close all windows with “OK”

Alternative via a package manager: It is quicker on the command line. The Windows Package Manager winget is usually preinstalled on current Windows versions; otherwise it can be installed from the Microsoft Store as “App Installer”:

winget install --id Git.Git -e --source winget
winget install --id Hugo.Hugo.Extended -e --source winget

If you use Chocolatey, install Hugo with choco install hugo-extended. After each installation, open a new terminal so that the changed PATH variable takes effect.

Checking the Installation

You can then check in the command prompt whether everything is set up correctly:

git --version
hugo version

On my machine the output looked like this:

C:\Users\info>git --version
git version 2.54.0.windows.1
C:\Users\info>hugo version
hugo v0.163.2-19a5cec0b9618163bb519487382e861d29edf383+extended windows/amd64 BuildDate=2026-06-15T14:55:00Z VendorInfo=gohugoio

By the way, Go does not need to be installed. Hugo is a ready-compiled program; you only need Go if you want to build Hugo yourself or include themes as Hugo Modules instead of Git submodules.

Step 1: Creating the Repository and Project Structure

I created the Git repository in the folder C:\sources\aaron_de and put the Hugo site into the subfolder hugo. This leaves room next to the site for auxiliary files such as the pipeline configuration under .github:

cd C:\sources\aaron_de
git init
hugo new site hugo

The command hugo new site creates the complete folder tree that Hugo needs for a project: content, layouts, static, themes and the central configuration file hugo.toml.

Step 2: Including the Theme as a Git Submodule

PaperMod is not copied but included as a Git submodule. The theme’s files do not end up in my repository. Git only stores two pieces of information there: the address of the PaperMod repository on GitHub (in the file .gitmodules) and the ID of the commit, i.e. the exact version of the theme that my site uses. The commands are run in the root directory of the repository, i.e. where the .git folder is located:

cd C:\sources\aaron_de
git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git hugo/themes/hugo-PaperMod

The parameter --depth=1 ensures that only the current state of the theme repository is cloned, without the complete commit history. This saves time and disk space.

Pitfall when checking out again: If you later clone or check out the repository on another computer, you will initially find the folder hugo/themes/hugo-PaperMod empty. Git does not load submodules automatically. Hugo then aborts at startup with an error about the missing theme. The fix is this command in the root directory of the repository:

git submodule update --init --recursive

Alternatively, you can load the theme right away when cloning. To do this, specify the address of your own repository on GitHub and the target folder:

git clone --recurse-submodules https://github.com/<user>/<repository>.git C:\sources\aaron_de

Replace <user> and <repository> with your GitHub user name and the name of your repository. GitHub shows the matching address on the repository page under the green “Code” button.

Step 3: Adjusting the Configuration

The file hugo/hugo.toml defines which theme is active and which basic settings apply. The value of theme must exactly match the folder name under themes:

baseURL = 'http://localhost:1313/'
locale = 'de-DE'
title = 'aaron.de'
theme = 'hugo-PaperMod'

[markup.goldmark.renderer]
  unsafe = true

About the setting unsafe = true: by default, Hugo does not output HTML written directly in Markdown and replaces it with a comment. My posts carried over from WordPress, however, contain HTML snippets such as <br/>. Only with unsafe = true do they appear on the page. The name sounds more dangerous than it is here: the setting allows HTML from my own Markdown files, not from input by outside visitors.

Step 4: Starting the Local Server

Finally, start the built-in development server in the Hugo folder:

cd C:\sources\aaron_de\hugo
hugo server -D

The flag -D makes sure that posts with draft: true are shown as well, which is handy for local testing. The console then shows, among other things, how many pages were built and where the server can be reached:

Web Server is available at http://localhost:1313/ (bind address 127.0.0.1)
Press Ctrl+C to stop

As long as the server is running, changes to content or configuration appear in the browser automatically, without a restart.

From Local Testing to the Live Server: My Deployment Pipeline

My Windows computer serves exclusively as a test environment. This is where I write and check content locally with hugo server -D before anything goes live. After that, the way to the server is automatic:

  1. Local change: Adjust content or layout on Windows and check it in the browser at localhost:1313
  2. Commit and push to GitHub: As soon as a post is finished, it is committed and pushed to the GitHub repository
  3. CI/CD pipeline: A GitHub Actions pipeline starts on push, builds the site with Hugo and transfers the result to the target server
  4. Live server: The web server delivers the newly built static files without me having to run anything on the server itself

The advantage of this setup: only fully built files end up on the production server. Nobody edits content directly there, and the pipeline does not even build posts with draft: true. This way I apply the principle of staging and production, which I know from classic software projects, to the blog.

The GitHub Actions Pipeline in Detail

The pipeline is a workflow file in the root directory of the repository, not in the subfolder hugo. On my machine it is located at C:\sources\aaron_de\.github\workflows\deploy.yml. GitHub only looks for workflows in the folder .github\workflows directly in the root directory; a file anywhere else would never be executed. The pipeline starts on every push to the main branch and can also be started manually via the “Run workflow” button in the Actions tab on GitHub:

name: Deploy Hugo Site

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  build-deploy:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: hugo
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive
          fetch-depth: 0

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

      - name: Build with Production Settings
        env:
          HUGO_PARAMS_ENV: production
          HUGO_ENV: production
        run: |
          hugo --gc --minify --baseURL "https://example.com/"

      - name: Deploy to Server
        uses: easingthemes/ssh-deploy@main
        with:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          ARGS: "-rlgoDzvc -i --delete"
          SOURCE: "hugo/public/"
          REMOTE_HOST: ${{ secrets.SERVER_IP }}
          REMOTE_USER: "aaron"
          REMOTE_PORT: ${{ secrets.SERVER_PORT }}
          TARGET: "/var/www/aaron/aaron_de/"

A few details that matter for this process:

  • working-directory: hugo: The Hugo site is not in the root directory of the repository but in the subfolder hugo. That is why this folder is set as the working directory for the build steps. The checkout with submodules: recursive ensures that the PaperMod theme is checked out as well. Without this line, the pipeline would have the same problem with the empty theme folder as a fresh clone on your own computer.
  • hugo-version: 'latest' and extended: true: The pipeline always uses the latest Extended version of Hugo. The locally installed version is only for previewing and has no influence on the live site. That is why I keep it up to date with winget upgrade Hugo.Hugo.Extended (or choco upgrade hugo-extended), so that I see the same locally as the pipeline does.
  • Production build: --gc removes files from the cache that are no longer needed after the build, --minify shrinks HTML, CSS and JS. Without further options, the hugo command builds in the production environment, whereas the local hugo server builds in the development environment. PaperMod only includes analytics and verification tags in the production environment and only there allows search engines to index the site. My local test visits therefore do not show up in the statistics. The variables HUGO_ENV and HUGO_PARAMS_ENV additionally set the production environment explicitly in the pipeline. HUGO_PARAMS_ENV overrides the parameter env = "development" from my hugo.toml. Incidentally, drafts are missing from the build simply because hugo is called without -D.
  • easingthemes/ssh-deploy: Transfers the contents of hugo/public/ to the target server via rsync over SSH. The flag --delete in the ARGS removes files on the server that no longer exist in the new build, so that no orphaned old pages are left behind.
  • GitHub Secrets: SSH_PRIVATE_KEY, SERVER_IP and SERVER_PORT are stored under Settings → Secrets and variables → Actions, so that no credentials are in the repository in plain text. The matching public key is in the authorized_keys file of the deploy user on the server.

For the examples in this post I have replaced the real domain with a placeholder. The production file contains the actual address of the blog at this point. If you only want to trigger the workflow manually, remove the push trigger and keep only workflow_dispatch.

Keeping the Theme and Hugo Up to Date

A submodule stays at the version it had when it was added. A simple git submodule update does not fetch a new theme version; it only restores the version the repository points to. For a real update you need the --remote option. The commands are again run in the root directory of the repository:

cd C:\sources\aaron_de
git submodule update --remote hugo/themes/hugo-PaperMod
cd hugo
hugo server -D

If the site looks fine locally, I commit and push the new reference. The pipeline then builds the site with the new theme version and deploys it to the server:

cd C:\sources\aaron_de
git add hugo/themes/hugo-PaperMod
git commit -m "Update PaperMod theme"
git push

Conclusion: Less Complexity, More Control

For me, moving from WordPress to Hugo was above all a step towards fewer moving parts. No database, no server-side execution, no constant security updates for a CMS. Instead, a set of static files that can be versioned with Git and delivered on practically any infrastructure.

Together with the GitHub Actions pipeline, this results in a setup in which the Windows computer serves purely as a test environment, GitHub is the authoritative place for all content, and the live server receives only fully built, static files.

In the meantime I have moved several other projects to Hugo as well and am still very happy with this decision.