I’ve been running My Battles With Technology as a WordPress site since December 2020, after starting on Kubernetes with a custom nginx+PHP+git-sync deployment. While WordPress served me well for content management, I kept hitting the same pain points: plugin updates breaking things, security concerns with PHP, and the overhead of managing a database for what’s fundamentally a read-heavy content site.
After reading about static site generators and seeing Hugo mentioned repeatedly in infrastructure circles, I decided to migrate. Here’s what that journey actually looked like.
Why Static Sites?
The core appeal was simple: eliminate the dynamic backend entirely. With WordPress:
- Every page request hits PHP and MySQL
- Plugins need constant security updates
- Caching is complex and error-prone
- Hosting requires database management
With a static site generator like Hugo:
- Pages are pre-rendered HTML at build time
- No server-side processing on requests
- Deploy anywhere (GitHub Pages, S3, Netlify, etc.)
- Security surface area drops dramatically
For a technical blog where 99% of traffic is reading (not writing), this made sense.
Choosing Hugo Over Alternatives
I evaluated three main options:
Jekyll (Ruby-based, GitHub Pages default)
- Mature ecosystem
- Great GitHub Pages integration
- But: Ruby dependencies, slower builds
Gatsby (React-based, JavaScript ecosystem)
- Modern, component-based
- Rich plugin ecosystem
- But: Heavy JavaScript bundle, complex for a blog
Hugo (Go-based, single binary)
- Blazing fast builds (milliseconds for hundreds of pages)
- Zero dependencies (single binary)
- Active development and community
- Strong theme ecosystem
Hugo won for two reasons: build speed and operational simplicity. Installing Hugo is literally downloading one binary. No npm, bundler, or dependency hell.
Initial Setup: Local Installation
I started on macOS. Installation was trivial:
brew install hugo
hugo version
Output:
hugo v0.140.2+extended darwin/arm64 BuildDate=2024-11-08T15:34:18Z VendorInfo=brew
The +extended version is important: it includes Sass/SCSS support that many themes require.
Creating the initial site structure:
mkdir shellnetblog-hugo
cd shellnetblog-hugo
hugo new site . --force
Hugo creates this structure:
.
├── archetypes/ # Content templates
├── content/ # Markdown files (posts)
├── data/ # Data files (JSON/YAML/TOML)
├── layouts/ # HTML templates (if overriding theme)
├── static/ # Static assets (images, CSS, JS)
├── themes/ # Theme(s)
└── config.toml # Site configuration
Theme Selection: PaperMod
Choosing a theme proved harder than expected. Hugo has hundreds of themes, but many are abandoned or poorly documented.
I needed:
- Clean, readable design for technical content
- Good code syntax highlighting
- Dark mode support
- SEO-friendly
- Active maintenance
After testing several themes locally, I settled on PaperMod: https://github.com/adityatelange/hugo-PaperMod
It’s actively maintained, has excellent documentation, supports Hugo modules (important for updates), and has features like:
- Built-in search
- Table of contents
- Multiple layout options (list, grid, etc.)
- Cover images
- Social metadata
Installing PaperMod via Hugo Modules
Modern Hugo themes use modules instead of git submodules. This makes updates cleaner:
hugo mod init github.com/yourusername/shellnetblog
Then in config.yml (I switched from TOML to YAML for readability):
module:
imports:
- path: github.com/adityatelange/hugo-PaperMod
Install the module:
hugo mod tidy
hugo mod get -u
This downloads PaperMod into Hugo’s module cache. Updates are simple: hugo mod get -u pulls the latest version.
The First Build Attempt
With PaperMod installed, I created a test post:
hugo new posts/first-post.md
Hugo generated content/posts/first-post.md:
---
title: "First Post"
date: 2024-09-25T10:00:00-05:00
draft: true
---
Test content.
Build the site:
hugo
Output:
Start building sites …
hugo v0.140.2+extended darwin/arm64 BuildDate=2024-11-08T15:34:18Z VendorInfo=brew
| EN
-------------------+-----
Pages | 7
Paginator pages | 0
Non-page files | 0
Static files | 0
Processed images | 0
Aliases | 0
Cleaned | 0
Total in 45 ms
Problem: The test post didn’t appear. Why?
Answer: draft: true in the frontmatter. Posts marked as drafts don’t build in production mode. Either change to draft: false or build with:
hugo -D # Include drafts
Content Migration: The Hard Part
This is where theory met reality. I had ~134 WordPress posts spanning 2019-2024. Migration options:
Option 1: WordPress Export Plugin
Several exist (hugo-export, wordpress-to-hugo-exporter), but they have issues:
- Often break on custom post types
- Lose image references
- Mangle HTML content
- Don’t handle WordPress shortcodes well
Option 2: Manual Migration
I chose this for my most important posts (top 20 by traffic). Process:
- Copy post content from WordPress
- Convert HTML to Markdown (using Pandoc)
- Download images from WordPress media library
- Update image paths
- Clean up formatting issues
- Add proper frontmatter
Option 3: Hybrid Approach (What I Did)
For the bulk of content, I used the export plugin to get 80% of the way there, then manually cleaned up high-value posts.
Key lesson: Budget more time for content migration than you think. Every post needs review. Frontmatter is critical: incorrect dates, missing tags, or broken image paths will cause issues.
Handling Images
WordPress stores images in /wp-content/uploads/YYYY/MM/filename.jpg. Hugo uses static/ or page bundles.
I used page bundles (recommended approach):
content/
└── posts/
└── my-post/
├── index.md
├── image1.png
└── image2.jpg
In index.md, reference images with relative paths:

This keeps assets together with content, making posts portable.
Configuration Deep Dive
PaperMod requires specific config structure. Here’s my actual config.yml (sanitized):
baseURL: "https://blog.shellnetsecurity.com"
languageCode: "en-us"
title: "My Battles With Technology"
theme: "PaperMod"
enableRobotsTXT: true
buildDrafts: false
buildFuture: false
buildExpired: false
minify:
disableXML: true
minifyOutput: true
params:
env: production
title: "My Battles With Technology"
description: "Technical blog covering AI, cybersecurity, cloud infrastructure, and development"
keywords: [AI, Cybersecurity, Cloud, DevOps, Kubernetes, Security]
author: "Scott Algatt"
ShowReadingTime: true
ShowShareButtons: true
ShowPostNavLinks: true
ShowBreadCrumbs: true
ShowCodeCopyButtons: true
ShowWordCount: true
ShowRssButtonInSectionTermList: true
UseHugoToc: true
disableSpecial1stPost: false
disableScrollToTop: false
comments: false
hidemeta: false
hideSummary: false
showtoc: true
tocopen: false
homeInfoParams:
Title: "My Battles With Technology"
Content: "A technical blog documenting real-world challenges in AI, cybersecurity, cloud infrastructure, and development. Learn from hands-on troubleshooting, investigations, and production experiences."
socialIcons:
- name: github
url: "https://github.com/yourusername"
- name: rss
url: "/index.xml"
menu:
main:
- identifier: posts
name: Posts
url: /posts/
weight: 10
- identifier: archives
name: Archives
url: /archives/
weight: 20
- identifier: search
name: Search
url: /search/
weight: 30
outputs:
home:
- HTML
- RSS
- JSON # Required for search
module:
imports:
- path: github.com/adityatelange/hugo-PaperMod
Key settings explained:
buildFuture: false- Don’t publish posts dated in the future (enables scheduled posts)ShowCodeCopyButtons: true- Adds copy button to code blocks (critical for technical blog)outputs.home: JSON- Enables PaperMod’s built-in search functionalitytocopen: false- Table of contents starts collapsed (cleaner on mobile)
CI/CD: Automated Builds with GitHub Actions
I wanted automated builds on every push to main. GitHub Actions made this straightforward.
.github/workflows/deploy_hugo_sites.yml:
name: Deploy Hugo Sites
on:
push:
branches:
- master
paths:
- 'ShellNetBlog/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.140.2'
extended: true
- name: Install Hugo modules
run: |
cd ShellNetBlog
hugo mod tidy
- name: Build site
run: |
cd ShellNetBlog
rm -rf public/*
hugo -e production
- name: Update tracking file
run: |
cd ShellNetBlog
date > tracking
- name: Commit and push
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git add .
git commit -m "Auto-build: $(date)" || echo "No changes"
git push
Important details:
paths: 'ShellNetBlog/**'- Only trigger on changes to blog directory (I have multiple sites in one repo)hugo-version: '0.140.2'- Pin exact version to avoid surprisesextended: true- Required for Sass/SCSS supporthugo mod tidy- Ensures modules are up to date before buildingrm -rf public/*- Clean previous build to avoid stale content
The workflow commits the built site back to the repo (unusual for static sites, but works for my hosting setup).
Deployment: Where to Host
Hugo generates pure HTML/CSS/JS in the public/ directory. You can host literally anywhere:
Options I considered:
- GitHub Pages - Free, integrated, custom domains supported
- Netlify - Free tier, CDN included, preview deployments
- Vercel - Similar to Netlify, good DX
- AWS S3 + CloudFront - Full control, cheap, requires setup
- Self-hosted nginx - I already have servers
I use a hybrid approach: GitHub Actions builds the site, commits to repo, and my self-hosted nginx serves from the repo (pulled via cron job). Not the simplest solution, but gives me full control.
For most people, I’d recommend GitHub Pages or Netlify; both have excellent Hugo support out of the box.
Performance: The Payoff
WordPress site (before):
- First Contentful Paint: ~1.8s
- Time to Interactive: ~3.2s
- Lighthouse score: 72/100
Hugo site (after):
- First Contentful Paint: ~0.4s
- Time to Interactive: ~0.6s
- Lighthouse score: 98/100
The difference is dramatic. Static HTML served from CDN vs PHP + MySQL queries.
Build time performance:
- 134 posts build in ~180ms locally
- GitHub Actions builds complete in ~30 seconds (including checkout, Hugo install, module download)
Unexpected Challenges
1. URL Structure Changes
WordPress used /2024/01/15/post-title/ by default. Hugo’s default is /posts/post-title/.
Problem: Broke all existing backlinks.
Solution: Configure Hugo to match WordPress structure:
permalinks:
posts: /:year/:month/:day/:title/
Or use Hugo’s aliases frontmatter to redirect old URLs:
aliases:
- /2024/01/15/old-post-title/
2. Syntax Highlighting Edge Cases
Hugo uses Chroma for syntax highlighting. It’s excellent but handles some languages differently than WordPress plugins.
Problem: Bash command prompts ($) were highlighted as variables.
Solution: Use console language tag instead of bash:
```console
$ hugo server
Start building sites ...
```
3. Search Implementation
WordPress has built-in search. Hugo doesn’t; it’s a static site generator.
Solution: PaperMod includes JavaScript-based search using Fuse.js. It indexes content at build time (via JSON output format) and searches client-side. Works well for <1000 posts.
For larger sites, consider Algolia or Lunr.js.
4. RSS Feed Compatibility
Hugo’s default RSS template differs from WordPress. Some feed readers had issues.
Solution: Customize RSS template to match WordPress format:
Create layouts/_default/rss.xml based on Hugo’s default, adjusting date formats and field ordering to match WordPress output.
Migration Checklist for Others
If you’re considering a WordPress to Hugo migration:
Pre-Migration:
- Export WordPress content (Posts & Pages > Export)
- Download media library
- Document current URL structure (for permalinks config)
- Test Hugo locally with sample posts
- Choose and test theme thoroughly
During Migration:
- Convert top 10-20 posts manually (quality check)
- Use export plugin for bulk content
- Review every imported post (frontmatter, images, formatting)
- Set up proper permalink structure (match old URLs if possible)
- Configure RSS feed for reader compatibility
- Implement search (if needed)
Post-Migration:
- Set up redirects from old URLs (if structure changed)
- Update Google Search Console (submit new sitemap)
- Test all internal links
- Verify images load correctly
- Check RSS feed in multiple readers
- Monitor analytics for 404 errors (fix broken links)
SEO Considerations:
- Keep same domain (don’t change during migration)
- Maintain URL structure or use proper 301 redirects
- Submit new sitemap to Google Search Console
- Monitor Search Console for crawl errors
- Update any hardcoded URLs in old posts
Lessons Learned
What Went Well:
- Hugo’s speed is real - Build times are negligible even with hundreds of posts
- PaperMod theme is excellent - Well-documented, actively maintained, feature-rich
- Hugo modules > git submodules - Cleaner dependency management
- Static site security - No more plugin update anxiety
What I’d Do Differently:
- Budget more time for content review - Migration tools get you 80% there, manual review is essential
- Plan URL structure upfront - Changing it mid-migration is painful
- Test theme thoroughly before committing - Some features only surface after deep customization
- Document custom configurations - Hugo has many knobs; document why you set each one
Is Migration Worth It?
For me: Absolutely.
Benefits I’m seeing:
- Faster page loads (98 Lighthouse score vs 72)
- Simpler infrastructure (no database, no PHP)
- Lower security risk (static HTML can’t be exploited like PHP)
- Version-controlled content (everything in Git)
- Offline writing workflow (Markdown in any editor)
Migration makes sense if:
- Your site is mostly read-only content (blog, documentation, portfolio)
- You’re comfortable with Markdown and Git
- You want fast page loads without complex caching
- You value infrastructure simplicity over admin GUI
Stick with WordPress if:
- You need complex user interactions (comments, forums, e-commerce without external services)
- Multiple non-technical editors need a GUI
- You rely heavily on WordPress-specific plugins
- You’re not comfortable with command-line tools
What’s Next
Now that the migration is complete, my focus shifts to:
- Content quality improvements - Review and update old posts
- SEO optimization - Proper meta descriptions, internal linking
- Analytics integration - Google Analytics + Search Console monitoring
- Content schedule - Weekly publication rhythm
The migration was a one-time investment that paid off in ongoing simplicity. No more WordPress security updates, no more plugin conflicts, no more database backups. Just write Markdown, commit to Git, and let CI/CD handle the rest.
Resources
Official Documentation:
- Hugo: https://gohugo.io/documentation/
- PaperMod Theme: https://github.com/adityatelange/hugo-PaperMod/wiki
- Hugo Modules: https://gohugo.io/hugo-modules/
Migration Tools:
- Pandoc (HTML to Markdown): https://pandoc.org/
- WordPress to Hugo Exporter: https://github.com/SchumacherFM/wordpress-to-hugo-exporter
Deployment Options:
- GitHub Pages: https://pages.github.com/
- Netlify: https://www.netlify.com/
- Vercel: https://vercel.com/
Community:
- Hugo Discourse: https://discourse.gohugo.io/
- r/hugo: https://reddit.com/r/hugo
Final Thoughts:
Static site generation isn’t new; it’s how the web started. But modern tools like Hugo bring the simplicity of static HTML with the developer experience of modern frameworks. For content-focused sites, it’s a powerful combination.
If you’re running a technical blog on WordPress and hitting similar pain points, I recommend giving Hugo a serious look. The migration isn’t trivial, but the long-term benefits (performance, security, simplicity) make it worthwhile.