DEV Community

Cover image for WordPress to Hugo: Lightning Fast Sites in 2025
Pascal CESCATO
Pascal CESCATO

Posted on

WordPress to Hugo: Lightning Fast Sites in 2025

I shared how I transformed my old laptop into a home server. That experimental setup became the perfect testing ground for something I'd been curious about — migrating from WordPress to Hugo. What started as a weekend project has turned into this comprehensive guide.

Why Leave WordPress?

As someone obsessed with page load speeds, WordPress has always been a mixed bag. Sure, it's powerful, but between PHP generating pages on-the-fly, database queries, and plugin overhead, achieving true speed is a constant battle.

Every tenth of a second matters for SEO and UX. Studies show 50% of visitors abandon slow pages. While I'd optimized my WordPress site with OpenLiteSpeed and premium hosting, I knew there was untapped potential.

Hugo's advantages:
⚡ Static files serve faster than dynamic PHP
🔒 No database or plugins to patch constantly
✍️ Write in Markdown, deploy anywhere
🚀 Minimal maintenance overhead

The biggest concern? Migration horror stories — lost images, broken links, frustrated bloggers giving up halfway. But with the right approach, these pitfalls are avoidable.

Understanding Hugo's Core Concepts

Hugo is a static site generator that transforms Markdown into HTML. Unlike WordPress (dynamic generation on each visit), Hugo pre-builds everything once.

Key components:

Content directory: Articles and pages in Markdown
Layouts/Themes: Templates for appearance
Static directory: Images, CSS, JS served as-is

Each Markdown file starts with "front matter" — metadata similar to WordPress custom fields but cleaner:

---
title: "My Article"
date: 2025-01-20
tags: ["webdev", "performance"]
---
Enter fullscreen mode Exit fullscreen mode

Installation and Setup

Install Hugo Extended

The "Extended" version is essential for modern themes with Sass support:

cd /usr/local/bin
wget https://github.com/gohugoio/hugo/releases/latest/download/hugo_extended_0.157.0_Linux-64bit.tar.gz
tar -xzf hugo_extended_0.157.0_Linux-64bit.tar.gz
rm hugo_extended_0.157.0_Linux-64bit.tar.gz
hugo version
Enter fullscreen mode Exit fullscreen mode

Create Your Site

cd ~/projects
hugo new site my-hugo-site
cd my-hugo-site
Enter fullscreen mode Exit fullscreen mode

Add Hextra Theme

Modern, fast, and clean:

git init
git submodule add https://github.com/imfing/hextra themes/hextra
cp themes/hextra/hugo.toml ./hugo.toml
Enter fullscreen mode Exit fullscreen mode

Configure hugo.toml:

baseURL = "https://your-domain.com/"
title = "Your Site Title"
theme = "hextra"
Enter fullscreen mode Exit fullscreen mode

Test Locally

hugo server -D --bind=0.0.0.0
Enter fullscreen mode Exit fullscreen mode

Visit http://localhost:1313

Migration Process

Export from WordPress

Dashboard → Tools → Export → All Content

This generates a WXR file with all your content. Transfer it to your Hugo server.

Convert to Markdown

Rather than unreliable automated tools, here's a custom Python script using Pandoc.
First create convert_wp_to_hugo.py:

#!/usr/bin/env python3
import os, subprocess, re
from lxml import etree
from datetime import datetime

# Install dependencies first:
# apt install -y python3 python3-venv pandoc
# python3 -m venv venv && source venv/bin/activate
# pip install lxml pyyaml

INPUT_XML = "/path/to/wordpress-export.xml"
OUTPUT_DIR = "content/posts"
os.makedirs(OUTPUT_DIR, exist_ok=True)

def to_slug(s):
    return "".join(c.lower() if c.isalnum() or c in "-" else "-" for c in s).strip("-")

def yaml_safe(s: str) -> str:
    if re.search(r"[:#\-\?\[\]\{\},&\*!\|>'\"%@`]|^\s|\s$", s) or " " in s:
        return f'"{s.replace(\'"\', \'\\"\'}"'
    return s

root = etree.parse(INPUT_XML, parser=etree.XMLParser(encoding="utf-8"))

for item in root.xpath("//item"):
    status = item.findtext("{http://wordpress.org/export/1.2/}status") or ""
    post_type = item.findtext("{http://wordpress.org/export/1.2/}post_type") or "post"

    if status != "publish" or post_type not in {"post", "page"}:
        continue

    title = (item.findtext("title") or "Untitled").strip()
    slug = item.findtext("{http://wordpress.org/export/1.2/}post_name") or to_slug(title)
    date = item.findtext("{http://wordpress.org/export/1.2/}post_date") or datetime.utcnow().isoformat()
    html = item.findtext("{http://purl.org/rss/1.0/modules/content/}encoded") or ""

    # Convert HTML to Markdown via Pandoc
    p = subprocess.run(
        ["pandoc", "-f", "html", "-t", "gfm-smart", "--wrap=none"],
        input=html.encode("utf-8"), capture_output=True
    )
    md = p.stdout.decode("utf-8").strip()

    # Extract categories and tags
    categories = [cat.text.strip() for cat in item.findall("category") 
                 if cat.get("domain") == "category" and cat.text]
    tags = [cat.text.strip() for cat in item.findall("category") 
           if cat.get("domain") == "post_tag" and cat.text]

    # Build front matter
    fm = ["---", f'title: "{title.replace("\\", "\\\\").replace('"', '\\"')}"',
          f"slug: {slug}", f"date: {date}", "draft: false"]

    if categories:
        fm.append("categories:")
        for c in categories:
            fm.append(f"  - {yaml_safe(c)}")

    if tags:
        fm.append("tags:")
        for t in tags:
            fm.append(f"  - {yaml_safe(t)}")

    fm.extend(["---", ""])

    out_name = f"{date[:10]}-{slug}.md" if post_type == "post" else f"{slug}.md"

    with open(os.path.join(OUTPUT_DIR, out_name), "w", encoding="utf-8") as f:
        f.write("\n".join(fm) + "\n" + md + "\n")

print(f"✓ Conversion complete → {OUTPUT_DIR}")
Enter fullscreen mode Exit fullscreen mode

Run it:

python3 convert_wp_to_hugo.py
Enter fullscreen mode Exit fullscreen mode

Image Optimization

Serving modern image formats (AVIF/WebP) dramatically improves performance.

Image Conversion Script

Here is the code:

#!/usr/bin/env python3
import os, re, requests
from pathlib import Path
from PIL import Image
from io import BytesIO
import pillow_avif

# pip install pillow pillow-avif-plugin requests

MD_DIR = Path("content")
OUT_DIR = Path("static/images")
OUT_DIR.mkdir(parents=True, exist_ok=True)

def save_formats(img_bytes, base_path):
    """Save image in multiple formats"""
    try:
        img = Image.open(BytesIO(img_bytes))
        img.save(f"{base_path}.avif", "AVIF", quality=80)
        img.save(f"{base_path}.webp", "WEBP", quality=80)
        img.save(f"{base_path}.jpg", "JPEG", quality=85)
        print(f"{base_path.name}")
    except Exception as e:
        print(f"✗ Error: {e}")

# Find all image URLs in Markdown
img_pattern = re.compile(r'!\[([^\]]*)\]\((https?://[^)]+)\)')

for md_file in MD_DIR.rglob("*.md"):
    content = md_file.read_text(encoding="utf-8")
    urls = {url for _, url in img_pattern.findall(content) if url.startswith("http")}

    for url in urls:
        try:
            filename = url.split("?")[0].split("/")[-1]
            base_path = OUT_DIR / Path(filename).stem

            r = requests.get(url, timeout=30)
            r.raise_for_status()
            save_formats(r.content, str(base_path))
        except Exception as e:
            print(f"Failed {url}: {e}")

print(f"\n✓ All images processed → {OUT_DIR}")
Enter fullscreen mode Exit fullscreen mode

Hugo Image Shortcode

Create layouts/shortcodes/img.html:

{{ $src := .Get "src" }}
{{ $alt := .Get "alt" | default "" }}
<picture>
  <source type="image/avif" srcset="{{ $src }}.avif" />
  <source type="image/webp" srcset="{{ $src }}.webp" />
  <img src="{{ $src }}.jpg" alt="{{ $alt }}" loading="lazy" />
</picture>
Enter fullscreen mode Exit fullscreen mode

Use in Markdown:

{{< img src="/images/my-screenshot" alt="Description" >}}
Enter fullscreen mode Exit fullscreen mode

Update Markdown Files

Replace standard image syntax with shortcode:

#!/usr/bin/env python3
import os, re

pattern = re.compile(r'!\[([^\]]*)\]\((https?://[^)]+)\)')

for root, _, files in os.walk("content"):
    for filename in files:
        if not filename.endswith('.md'):
            continue

        filepath = os.path.join(root, filename)
        with open(filepath, 'r', encoding='utf-8') as f:
            content = f.read()

        def replacement(match):
            alt_text, url = match.groups()
            image_name = os.path.splitext(os.path.basename(url))[0]
            return f'{{{{< img src="/images/{image_name}" alt="{alt_text}" >}}}}'

        new_content = pattern.sub(replacement, content)

        if new_content != content:
            with open(filepath, 'w', encoding='utf-8') as f:
                f.write(new_content)
            print(f"✓ Updated: {filepath}")
Enter fullscreen mode Exit fullscreen mode

Deployment

Build and deploy your static site:

hugo  # Generates site in public/
cp -r public/* /var/www/html/
Enter fullscreen mode Exit fullscreen mode

Your site is now pure HTML — no PHP, no database queries, just blazing-fast static content.

Pre-Migration Checklist

Backup everything: Full WordPress files + database export. Test restore separately.

Plan URL structure: Set up redirects from old WordPress URLs to Hugo structure for SEO.

Test thoroughly: Multiple browsers and devices, especially mobile performance.

Lost features: WordPress search, comments, contact forms need alternatives (Algolia, Disqus, Netlify Forms).

Security: Even static sites need proper SSL and security headers.

Performance Optimizations

For maximum speed:

  • Use Hugo's partialCached for expensive operations
  • Implement Hugo Pipes for asset bundling
  • Consider Page Bundles for better organization
  • Set up CI/CD for automated deployments

Common Issues

"try not defined" errors: Hugo version too old. Update to latest Extended version.

404 on production: Build with hugo and copy public/ folder to web server.

Can't access from network: Add --bind=0.0.0.0 and check firewall settings.

Results

After migrating to Hugo, my site loads in under 200ms — a dramatic improvement from WordPress. Page speeds that once required extensive optimization now come naturally.

The maintenance burden has practically disappeared. No more plugin updates, security patches, or database optimization. Just write in Markdown and deploy.

Conclusion

After completing this migration and running extensive performance tests (detailed in my previous article "WordPress vs Hugo: When Reality Challenges the Speed Myths"), I'll be honest: Hugo didn't convince me as much as I expected.

The performance gains? On paper, Hugo should dominate. In practice, with a properly optimized WordPress setup (OpenLiteSpeed + LS Cache), the difference was only 32ms for content-heavy pages. Hugo's advantage becomes clear at massive scale, but for most sites, a well-configured WordPress can match it.

What I learned:

  • Static isn't automatically faster - my Hugo pages had significant HTML parsing overhead
  • WordPress with good caching essentially becomes a static site generator
  • The real Hugo advantage is simplicity and predictability, not raw speed
  • You'll miss WordPress's admin interface more than you think

Was it worth it? As an experiment, absolutely. I learned valuable lessons about web performance, infrastructure, and the real trade-offs between platforms. The migration process itself was enriching.

Should you migrate? Only if you value simplicity over flexibility, are comfortable with Markdown workflows, and don't need dynamic features. A properly optimized WordPress might serve you just as well.

The future probably belongs to hybrid approaches anyway - static generation where it makes sense, dynamic features where needed. Both platforms are evolving toward this middle ground.


Have questions about the migration or want to discuss the performance results? Drop them in the comments below!


Have questions about the migration? Drop them in the comments below!

Resources


Follow me for more web development tutorials and infrastructure guides!


📬 Want essays like this in your inbox?

I just launched a newsletter about thinking clearly in tech — no spam, no fluff.

Subscribe here: https://buttondown.com/efficientlaziness

Efficient Laziness — Think once, well, and move forward.

Top comments (0)