Why Migrate from WordPress?

WordPress is powerful, but for a technical blog that mostly serves static content, it comes with unnecessary overhead — hosting costs, plugin updates, security patches, and slower page loads. Static site generators like Hugo offer a simpler, faster, and cheaper alternative.

Here’s what we migrated to:

  • Hugo — blazing fast static site generator
  • PaperMod — clean, minimal theme perfect for tech blogs
  • Decap CMS — web-based content management with GitHub backend
  • Cloudflare Pages — free hosting with global CDN
  • Google AdSense — preserved auto ads from the WordPress site

The result? A site that builds in under 1 second, costs $0/month to host, and is served from Cloudflare’s global edge network.

Step 1: Export WordPress Content

We used the WordPress to Hugo Exporter plugin to export all posts and pages as Markdown files with YAML front matter. The export gave us:

  • 72 blog posts as .md files
  • Static pages (About, Contact, Privacy Policy)
  • A config.yaml with site metadata

Step 2: Set Up Hugo with PaperMod

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

The key configuration in hugo.toml:

baseURL = "https://learncodecamp.net"
title = "Learn Code Camp"
theme = "PaperMod"

[params]
  ShowReadingTime = true
  ShowCodeCopyButtons = true
  ShowToc = true

[markup.goldmark.renderer]
  unsafe = true  # Required for HTML content from WordPress

[permalinks]
  posts = "/:slug/"  # Match old WordPress URL structure

The permalinks setting is critical — it ensures all existing URLs continue to work, preserving SEO rankings.

Step 3: Clean Up WordPress Content

The exported Markdown files had a lot of WordPress-specific artifacts:

  • rank_math_* and zakra_* metadata in front matter
  • WordPress CSS classes like {.wp-block-heading}
  • <nav class="wp-block-table-of-contents"> blocks
  • HTML entities like &#8217; instead of '

We wrote a Python cleanup script that:

  1. Stripped front matter — kept only title, author, date, slug, draft, categories, and tags
  2. Derived slugs from the old url field to maintain URL compatibility
  3. Removed WordPress classes and table-of-contents blocks (PaperMod has its own TOC)
  4. Decoded HTML entities back to readable characters
  5. Fixed invalid dates on draft posts (6 posts had -001-11-30 as their date)

Step 4: Google AdSense Integration

Since the site uses AdSense auto ads (placement controlled from the AdSense console), the integration was simple — just one script tag in the <head>:

<!-- layouts/partials/google-ads-head.html -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-XXXXXXXXX"
     crossorigin="anonymous"></script>

PaperMod provides an extend_head.html hook that made this easy:

<!-- layouts/partials/extend_head.html -->
{{ partial "google-ads-head.html" . }}

We also added ads.txt and app-ads.txt files in the static/ directory for AdSense verification.

Step 5: Set Up Decap CMS

Decap CMS provides a web-based admin panel at /admin/ that commits directly to your GitHub repository.

Two files in static/admin/:

index.html — loads the CMS:

<!doctype html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Content Manager</title>
</head>
<body>
  <div id="nc-root"></div>
  <script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
</body>
</html>

Important: The script must be at the end of <body>, not in <head>. Decap CMS 3.x tries to mount into the DOM immediately, and placing the script in <head> causes a Cannot read properties of null (reading 'appendChild') error.

config.yml — defines the content model:

backend:
  name: github
  repo: nkalra0123/learncodecamp
  branch: main
  base_url: https://github-oauth-proxy.nkalra0123.workers.dev

collections:
  - name: "posts"
    label: "Posts"
    folder: "content/posts"
    create: true
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Date", name: "date", widget: "datetime" }
      - { label: "Author", name: "author", widget: "string", default: "Nitin" }
      - { label: "Categories", name: "categories", widget: "list" }
      - { label: "Tags", name: "tags", widget: "list" }
      - { label: "Body", name: "body", widget: "markdown" }

Step 6: OAuth Proxy for Decap CMS

Decap CMS needs OAuth to authenticate with GitHub. On Cloudflare Pages (unlike Netlify), there’s no built-in OAuth provider, so we deployed a Cloudflare Worker as an OAuth proxy.

The flow:

  1. User clicks “Login with GitHub” in the CMS
  2. CMS opens a popup to the worker’s /auth endpoint
  3. Worker redirects to GitHub OAuth
  4. GitHub redirects back to the worker’s /callback with an auth code
  5. Worker exchanges the code for an access token
  6. Worker sends the token back to the CMS via postMessage

Key gotcha: Decap CMS uses a handshake protocol — the callback page must first signal authorizing:github to the opener, then wait for a message before sending the token. Without this handshake, the CMS won’t receive the token.

Step 7: Deploy on Cloudflare Pages

  1. Connected the GitHub repo to Cloudflare Pages
  2. Set build command to hugo --minify and output directory to public
  3. Added HUGO_VERSION = 0.155.1 as an environment variable
  4. Added learncodecamp.net as a custom domain
  5. Purged Cloudflare cache to clear old WordPress responses

URL Verification

One of the most important aspects of the migration was ensuring all 66 published WordPress URLs matched exactly in Hugo. We verified every single URL — zero mismatches. This means:

  • No broken links from search engines or external sites
  • No drop in SEO rankings
  • No need for redirect rules

The Result

MetricWordPressHugo
Build timeN/A< 1 second
Hosting cost~$5-10/monthFree
Page load2-4 seconds< 1 second
DeploymentManual/FTPAuto on git push
Content editingWordPress adminDecap CMS or Git
Security patchesFrequentNone needed

New Post Workflow

Adding a new post is now:

  1. Go to https://learncodecamp.net/admin/
  2. Login with GitHub
  3. Write the post in the Markdown editor
  4. Set title, categories, tags
  5. Click Publish
  6. Decap CMS commits to GitHub → Cloudflare Pages auto-builds → live in ~1 minute

Or just push a new .md file to the content/posts/ directory in the repo — whatever you prefer.

Tools Used