After years of running my tech blog on WordPress, I finally pulled the trigger and migrated everything to Hugo — a blazing-fast static site generator — hosted on Cloudflare Workers. The result? A site that loads in milliseconds, costs nothing to host, and requires zero server maintenance.

Here’s exactly how I did it, what went wrong, and what I’d do differently.


Why Leave WordPress?

WordPress served me well for years. But as my post count grew to 278 articles (mostly VMware VCF guides with dozens of screenshots each), the cracks started showing:

  • Performance: Even with caching plugins, page loads were sluggish. Each request hit PHP and MySQL.
  • Maintenance overhead: WordPress core updates, plugin updates, PHP version bumps, SSL renewals, database backups — it never ends.
  • Security surface: Every plugin is a potential vulnerability. I was running a firewall plugin just to block brute-force login attempts.
  • Cost: Hosting a WordPress site with decent performance isn’t free. My VPS bill added up.

A static site eliminates all of these problems. No database, no PHP, no server — just HTML files served from a CDN.


Choosing the Stack

Hugo over Jekyll, Gatsby, Next.js

I evaluated several static site generators:

GeneratorProsCons
JekyllRuby-based, GitHub Pages nativeSlow builds for large sites
GatsbyReact-based, rich plugin ecosystemHeavy, complex, Node dependency hell
Next.jsFull-stack capableOverkill for a blog
HugoSingle binary, sub-second builds, Go templatingSteeper template learning curve

Hugo won because of build speed. With 278 posts and 13,000+ images, Hugo builds the entire site in under 7 seconds. Jekyll would take minutes.

PaperMod Theme

I went with PaperMod — a clean, fast, SEO-optimized Hugo theme. It ships with:

  • Dark/light mode toggle
  • Built-in search (via Fuse.js)
  • Table of contents
  • Share buttons
  • Reading time estimates
  • Archive and taxonomy pages

I customized it with a dark-first infrastructure theme — muted teal accents, JetBrains Mono for metadata, and Inter for body text.

Cloudflare Workers over GitHub Pages, Netlify, Vercel

Cloudflare Workers (formerly Cloudflare Pages) was the obvious choice:

  • Free tier handles my traffic easily
  • Global CDN — content served from 300+ edge locations
  • Git integration — push to GitHub, site deploys automatically
  • Custom domain + SSL — handled natively
  • Security headers — configured via _headers file

The Migration Process

Step 1: Extract the WordPress Backup

My WordPress backup was in the proprietary .wpress format (from the All-in-One WP Migration plugin). This isn’t a standard ZIP — it has a custom binary header format.

I wrote a Python script to parse the binary headers and extract the contents:

def extract_wpress(filepath, output_dir):
    with open(filepath, 'rb') as f:
        while True:
            header = f.read(HEADER_SIZE)
            if len(header) < HEADER_SIZE:
                break
            name = header[0:255].replace(b'\x00', b'').decode('utf-8')
            size = int(header[255:269].replace(b'\x00', b'').decode('utf-8'))
            # ... extract file data

This gave me two critical files:

  • database.sql — the full WordPress MySQL dump
  • uploads/ — all 13,000+ media files

Step 2: Convert Posts to Markdown

The core of the migration was a Python script that:

  1. Parsed the SQL dump to extract posts (title, content, slug, date, categories, tags, excerpt)
  2. Converted HTML content to Markdown using the markdownify library
  3. Generated Hugo-compatible front matter with proper YAML escaping
  4. Rewrote image URLs from wp-content/uploads/ to Hugo’s /images/ path

The trickiest part was YAML front matter sanitization. WordPress post titles and descriptions contain all sorts of special characters — quotes, ampersands, colons, tabs — that break YAML parsing. I wrote a yaml_safe_string() helper:

def yaml_safe_string(value):
    if not value:
        return '""'
    value = value.replace('\t', ' ').strip()
    if any(c in value for c in ':{}[]&*?|>!%@`"\',#'):
        escaped = value.replace('\\', '\\\\').replace('"', '\\"')
        return f'"{escaped}"'
    return f'"{value}"'

Step 3: Scaffold the Hugo Site

hugo new site cosmin-gq
cd cosmin-gq
git submodule add https://github.com/adityatelange/hugo-PaperMod themes/PaperMod

Then I configured hugo.toml with:

  • SEO metadata (description, keywords, author, Open Graph)
  • JSON-LD structured data via a custom extend_head.html partial
  • Sitemap configuration (304 URLs generated)
  • RSS feed (278 items)
  • Security headers (_headers file for HSTS, CSP, X-Frame-Options)
  • Feed redirects (/feed//index.xml for WordPress RSS subscribers)

Step 4: Deploy to Cloudflare

Cloudflare recently merged Pages into Workers, so the deployment uses Wrangler:

Build settings:

  • Build command: hugo --minify
  • Deploy command: npx wrangler deploy

wrangler.jsonc:

{
  "name": "cosmin-gq",
  "compatibility_date": "2025-09-27",
  "assets": {
    "directory": "public"
  }
}

The workflow is simple: git push → Cloudflare clones the repo → Hugo builds → Wrangler uploads 14,000+ static files to the edge.


Gotchas and Lessons Learned

1. The paginate Deprecation

Hugo v0.128.0+ removed the paginate config key. Cloudflare’s build environment uses a newer Hugo version, so this broke the build:

ERROR deprecated: site config key paginate was deprecated in Hugo v0.128.0

Fix: Replace paginate = 10 with:

[pagination]
  pagerSize = 10

2. Cloudflare Workers Don’t Support 404 Redirects

My _redirects file had rules to block old WordPress admin paths:

/wp-admin/*    /404.html  404
/wp-login.php  /404.html  404

Workers only supports redirect status codes (200, 301, 302, 303, 307, 308). Hugo’s built-in 404 page handles missing routes anyway.

3. The baseURL Matters

Hugo generates all links using the baseURL from hugo.toml. If your production domain isn’t set up yet and you’re testing on the .workers.dev URL, all links will point to the wrong place. Set baseURL to match wherever the site is currently served.

4. Don’t Commit the public/ Directory

Hugo’s build output goes into public/. Since Cloudflare rebuilds this on every deploy, there’s no reason to track it in git. Add it to .gitignore:

public/
.DS_Store
resources/
node_modules/
.wrangler/

5. Ad Blockers Hide LinkedIn Share Buttons

If you add LinkedIn share buttons, be aware that most ad blockers (uBlock Origin, AdGuard) will hide them because the share URL contains linkedin.com. Nothing you can do about this from the site side.


The Results

MetricWordPressHugo + Cloudflare
Build timeN/A (dynamic)~7 seconds
Page load2-4 seconds< 200ms
Hosting cost~$20/monthFree
Server maintenanceWeeklyNone
Security updatesConstantNone needed
Uptime99.5%99.99% (Cloudflare SLA)

The site now loads almost instantly from anywhere in the world, costs nothing to run, and the only “deployment” step is git push.


What I’d Do Differently

  1. Start with .gitignore — I accidentally committed the public/ directory on the first push (13,000+ files). Set up .gitignore before the first commit.
  2. Use relativeURLs — This avoids the baseURL mismatch issue entirely during testing.
  3. Test the Cloudflare build first — The Workers deployment flow has quirks (no 404 redirects, Wrangler auto-detection). A quick test deploy before migrating all content saves debugging time.

Final Thoughts

If you’re running a technical blog on WordPress and it’s mostly content + screenshots (no e-commerce, no user accounts, no dynamic forms), there’s very little reason to stay on WordPress. Hugo gives you better performance, better security, and zero ongoing costs.

The migration took about a day for 278 posts and 13,000 images. The hardest part was sanitizing the YAML front matter — everything else was straightforward.

The site is live at cosmin.gq. All the old content is preserved, SEO metadata is intact, and the RSS feed still works at the same path.

If you have questions about the migration process, feel free to reach out on X or LinkedIn.