I decided to migrate this site, which had been running on WordPress, to Hugo.
WordPress is convenient. You can write posts from the admin screen, and there are many themes and plugins. But after running it for a long time, the number of things to maintain keeps growing little by little: PHP, MySQL, plugins, themes, security updates, backups, and so on.
This blog is mostly a place where I leave notes about things I looked up. I do not need much dynamic behavior. For that kind of use, managing posts as Markdown files and publishing static HTML with Hugo feels like a better fit.
I also used Codex during this migration. I worked with it to write scripts for importing the WordPress export XML, check the converted posts with Hugo, and gradually fix places that were likely to break visually.
Migration Policy
For this migration I used the following policy:
- Use the WordPress export XML as the source data
- Convert posts and pages into Hugo’s
contentdirectory - Put uploaded files such as images under
static/wp-content/uploads - Preserve existing URLs as much as possible
- Keep the converted files easy to fix by hand
- Use Codex to help create and verify conversion scripts
Rather than trying to make a perfect automatic converter from the beginning, I first aimed to get the whole site into a state where Hugo could build it. Then I fixed the rough edges one by one.
Exporting from WordPress
Create an export file from the WordPress admin screen.
Open the following menu:
Tools -> Export
Select “All content” and download the WXR XML file.
This XML contains posts, pages, categories, tags, body content, and the original WordPress URLs. It does not include the image files themselves, so images need to be downloaded separately later.
Importing Posts into the Hugo Project
In this repository, I prepared scripts/import-wordpress-wxr.ps1 to read the WordPress WXR XML.
For example, to import wordpress.WordPress.xml, run:
.\scripts\import-wordpress-wxr.ps1 -InputPath .\wordpress.WordPress.xml
Published posts and pages are written to:
content/posts
content/pages
If you also want to include drafts or private posts, add options as needed:
.\scripts\import-wordpress-wxr.ps1 -InputPath .\wordpress.WordPress.xml -IncludeDrafts -IncludePrivate
The converted Markdown files keep some WordPress metadata in the front matter:
wordpress_id = "123"
wordpress_url = "https://nosubject.io/example/"
wordpress_status = "publish"
This makes it much easier to compare the converted Hugo post with the original WordPress post later.
Preserving URLs
To avoid breaking search results and bookmarks, I added url to the front matter of each post.
url = "/wordpress-cocoon-edit-header-tag/"
In Hugo, even if the file name or section structure changes, the public URL can be fixed with url.
For a WordPress migration, it is better to decide this early. If URLs change later, internal links and search engine handling become more troublesome.
Migrating Image Files
WordPress content often contains image URLs such as:
https://nosubject.io/wp-content/uploads/2020/02/image.png
With Hugo, files placed under static are served as-is. So I kept the same public paths by placing files like this:
static/wp-content/uploads/2020/02/image.png
I used scripts/download-wordpress-media.ps1 to download images and rewrite URLs.
First, run it in dry-run mode:
.\scripts\download-wordpress-media.ps1 -DryRun
If the output looks right, run it for real:
.\scripts\download-wordpress-media.ps1
For images that were successfully downloaded, the post content is rewritten to use local URLs such as /wp-content/uploads/....
Cleaning Up HTML from WordPress Blocks
Posts written with the WordPress block editor contain wp-block-* HTML and block comments.
In Hugo, I set markup.goldmark.renderer.unsafe = true so that the imported HTML can be rendered as-is. But if I leave everything untouched, there are many unnecessary classes and embedded fragments.
So after importing the posts, I run scripts/convert-wordpress-content-for-hugo.ps1 to clean up the content a little for Hugo.
.\scripts\convert-wordpress-content-for-hugo.ps1 -DryRun
After checking the dry-run output:
.\scripts\convert-wordpress-content-for-hugo.ps1
This script does things such as:
- Convert embedded WordPress URLs into a
blogcardshortcode - Convert GitHub Gist
<script>tags into Hugo’sgistshortcode - Move internal links toward relative paths
- Set
featured_imageandimagesfrom the first image - Clean up some heading and button HTML
Converting Amazon Links to Shortcodes
Some old posts contain Amazon Associate iframes.
They can be left as raw HTML, but long iframe HTML makes posts hard to edit. So I created layouts/shortcodes/amazon-iframe.html and converted the body to shortcode form.
{{< amazon-iframe asin="B07GBD3JLR" tag="nosubjectio0d-22" >}}
I used scripts/convert-amazon-links.ps1 for this conversion.
.\scripts\convert-amazon-links.ps1 -DryRun
If the result looks good:
.\scripts\convert-amazon-links.ps1
General PowerShell Script Examples
In my environment, I put purpose-specific scripts under the repository’s scripts directory.
I created these scripts while consulting Codex. The workflow was: convert a small part first, build with Hugo, fix errors or visual problems, and then expand the script.
The main scripts are:
import-wordpress-wxr.ps1download-wordpress-media.ps1convert-wordpress-content-for-hugo.ps1convert-amazon-links.ps1
The import script reads WordPress WXR XML and writes post items to content/posts and page items to content/pages.
The media script scans Markdown under content, finds WordPress image URLs, downloads them into static/wp-content/uploads, and rewrites successful URLs to local /wp-content/uploads/... paths.
The cleanup script removes some WordPress block comments and normalizes content just enough for Hugo. I intentionally did not try to convert everything into pure Markdown. Keeping some HTML is easier and safer for old posts.
The Amazon conversion script detects Amazon iframes and turns them into a Hugo shortcode. This makes the post body much easier to read and edit.
For each script, I added a -DryRun option. This is important. With a migration like this, it is much safer to inspect the planned changes before rewriting many files at once.
Checking with Hugo
After migrating posts and images, check the site with Hugo’s local server.
hugo server -D --disableFastRender
This environment uses Hugo Extended.
hugo version
At the time of writing, I checked with:
hugo v0.164.0+extended windows/amd64
After starting the local server, open:
http://localhost:1313/
Things to Watch Out For
After actually doing the migration, I found that converting the content to HTML was not the end of the work.
The main things I had to check were:
- Whether images exist
- Whether internal links still point to the old domain
- Whether HTML from WordPress blocks breaks the layout
- Whether categories and tags appear as intended
- Whether
featured_imagelooks natural on list pages - Whether switching between Japanese and English posts works correctly
- Whether existing URLs are preserved
When there are many posts, trying to make a perfect conversion script takes a long time.
It is better to first get the whole site building with Hugo, then fix the most visible problems one by one.
Summary
I migrated from WordPress to Hugo in this order:
- Export WXR XML from WordPress
- Import posts and pages with
import-wordpress-wxr.ps1 - Save images under
static/wp-content/uploadswithdownload-wordpress-media.ps1 - Clean up the body content with
convert-wordpress-content-for-hugo.ps1 - Convert Amazon links and similar embeds to shortcodes where needed
- Check the site with
hugo server -D - Manually fix posts that still look wrong
Instead of being tied to the WordPress admin screen, Hugo lets me manage posts as files.
For a site like mine, where the main purpose is to leave notes about things I looked up, a static site seems easier to handle in many situations.
There are still small adjustments left, but at least the posts, images, and URLs are now manageable on the Hugo side.
Prompt for Using Codex in a WordPress-to-Hugo Migration
Here is a prompt that may be useful when migrating another WordPress site to Hugo.
It is easier to split the work into stages: first inspect the current project, then create the import script, then convert the body content, and finally verify the build.
Please help me migrate from WordPress to Hugo.
Goals:
- Migrate a WordPress WXR XML export into Hugo's content directory
- Preserve existing post URLs as much as possible
- Save images under Hugo's static/wp-content/uploads directory
- Keep the converted files in a Markdown / HTML mixed format that is easy to edit by hand
- Prioritize getting the site to build with Hugo first
Assumptions:
- The working directory is the currently opened Hugo project
- The WordPress export XML is in the project root or at a path I provide
- Hugo Extended is installed
- Please create migration scripts that can be run with PowerShell
What I want you to do:
1. First inspect the repository structure, hugo.toml, content, layouts, static, and existing scripts
2. Check git status and do not revert existing uncommitted changes without asking
3. Create a ps1 script that reads WordPress WXR XML and writes post items to content/posts and page items to content/pages
4. Add title, date, lastmod, draft, slug, url, author, categories, and tags to the front matter
5. Also keep wordpress_id, wordpress_url, and wordpress_status for comparison with the original posts
6. Preserve existing URLs by setting url = "/slug/" from the WordPress slug
7. Clean up WordPress block comments and unnecessary wp-block HTML, but do not over-normalize the content
8. Create a ps1 script that detects /wp-content/uploads/ image URLs and downloads them under static/wp-content/uploads
9. Replace successfully downloaded image URLs with local /wp-content/uploads/... URLs
10. Convert Gists, embedded links, Amazon iframes, and similar content to Hugo shortcodes where appropriate
11. Add a -DryRun option to conversion scripts so large changes are not applied immediately
12. Run hugo -D after conversion to verify the build
13. If errors occur, check the Hugo version, configuration, and converted content before changing templates
14. At the end, summarize changed files, commands run, build results, and remaining checks
Notes:
- Do not destructively overwrite existing files
- Do not change existing URLs
- Do not try too hard to convert all WordPress HTML into pure Markdown
- For image and internal-link replacement, run DryRun before applying changes
- Keep theme and layout changes minimal
For a smaller first request, I would start with:
Please inspect this Hugo project and create a migration plan from WordPress.
Do not modify files yet. Check hugo.toml, content, layouts, static, existing scripts, and whether Hugo can build, then propose the migration steps.
When actually creating the import script, I would ask:
Create a PowerShell script that converts WordPress WXR XML into Hugo content/posts and content/pages.
Target only published posts first, and set url front matter to preserve existing URLs.
After creating it, verify it with DryRun or a small number of posts.
For image migration:
Create a PowerShell script that extracts WordPress image URLs from Markdown under content and saves them into static/wp-content/uploads.
Make it support -DryRun so I can confirm target URLs and output paths.
Only replace body URLs for images that were successfully saved.
For content cleanup:
Create a PowerShell script to clean up WordPress-imported content for Hugo.
Include removing WordPress block comments, making internal links relative, and setting featured_image/images from the first image.
Do not break HTML too aggressively, and make it support -DryRun so I can check the number of changes.