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
.mdfiles - Static pages (About, Contact, Privacy Policy)
- A
config.yamlwith 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_*andzakra_*metadata in front matter- WordPress CSS classes like
{.wp-block-heading} <nav class="wp-block-table-of-contents">blocks- HTML entities like
’instead of'
We wrote a Python cleanup script that:
- Stripped front matter — kept only
title,author,date,slug,draft,categories, andtags - Derived slugs from the old
urlfield to maintain URL compatibility - Removed WordPress classes and table-of-contents blocks (PaperMod has its own TOC)
- Decoded HTML entities back to readable characters
- Fixed invalid dates on draft posts (6 posts had
-001-11-30as 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 aCannot 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:
- User clicks “Login with GitHub” in the CMS
- CMS opens a popup to the worker’s
/authendpoint - Worker redirects to GitHub OAuth
- GitHub redirects back to the worker’s
/callbackwith an auth code - Worker exchanges the code for an access token
- 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
- Connected the GitHub repo to Cloudflare Pages
- Set build command to
hugo --minifyand output directory topublic - Added
HUGO_VERSION = 0.155.1as an environment variable - Added
learncodecamp.netas a custom domain - 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
| Metric | WordPress | Hugo |
|---|---|---|
| Build time | N/A | < 1 second |
| Hosting cost | ~$5-10/month | Free |
| Page load | 2-4 seconds | < 1 second |
| Deployment | Manual/FTP | Auto on git push |
| Content editing | WordPress admin | Decap CMS or Git |
| Security patches | Frequent | None needed |
New Post Workflow
Adding a new post is now:
- Go to
https://learncodecamp.net/admin/ - Login with GitHub
- Write the post in the Markdown editor
- Set title, categories, tags
- Click Publish
- 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
- Hugo — static site generator
- PaperMod — Hugo theme
- Decap CMS — Git-based content management
- Cloudflare Pages — free static site hosting
- Cloudflare Workers — OAuth proxy for Decap CMS