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:

  1. Copy post content from WordPress
  2. Convert HTML to Markdown (using Pandoc)
  3. Download images from WordPress media library
  4. Update image paths
  5. Clean up formatting issues
  6. 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:

![Alt text](image1.png)

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 functionality
  • tocopen: 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:

  1. paths: 'ShellNetBlog/**' - Only trigger on changes to blog directory (I have multiple sites in one repo)
  2. hugo-version: '0.140.2' - Pin exact version to avoid surprises
  3. extended: true - Required for Sass/SCSS support
  4. hugo mod tidy - Ensures modules are up to date before building
  5. rm -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:

  1. GitHub Pages - Free, integrated, custom domains supported
  2. Netlify - Free tier, CDN included, preview deployments
  3. Vercel - Similar to Netlify, good DX
  4. AWS S3 + CloudFront - Full control, cheap, requires setup
  5. 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:

  1. Hugo’s speed is real - Build times are negligible even with hundreds of posts
  2. PaperMod theme is excellent - Well-documented, actively maintained, feature-rich
  3. Hugo modules > git submodules - Cleaner dependency management
  4. Static site security - No more plugin update anxiety

What I’d Do Differently:

  1. Budget more time for content review - Migration tools get you 80% there, manual review is essential
  2. Plan URL structure upfront - Changing it mid-migration is painful
  3. Test theme thoroughly before committing - Some features only surface after deep customization
  4. 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:

  1. Content quality improvements - Review and update old posts
  2. SEO optimization - Proper meta descriptions, internal linking
  3. Analytics integration - Google Analytics + Search Console monitoring
  4. 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:

Migration Tools:

Deployment Options:

Community:


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.