Migrate WordPress to Khalid: keep your slugs, your SEO titles and every old address.
Export your WordPress blog, try the import with a dry run, then bring the posts in. Khalid keeps each slug, reads the SEO plugin's titles and descriptions, and stores a redirect for every address that moves.
Free and open source. Every command here was run with Khalid 1.3.2.
khalid init blog --address https://www.example.com --mount /blog/ -y
khalid --site blog import wordpress export.xml --dry-run
khalid --site blog import wordpress export.xml
khalid --site blog lint
Export. Take your blog out of WordPress.
WordPress writes one XML file with your posts, pages, authors and categories. Khalid reads that file. It never needs your database or a plugin.
Open Tools, Export
In your WordPress admin, go to Tools, then Export.
Choose All content
Pick All content and click Download Export File. WordPress saves an XML file. Name it export.xml.
Using the Redirection plugin? Export it too
Its rules are not in the WordPress export. In the plugin, export the rules as JSON and name the file redirection.json.
Put both files beside your new blog
Keep them in the folder where you run the khalid commands.
WordPress's own Tools Export screen guide, checked 4 Nov 2026. Images are not in this file. See Images below.
Dry run. See what would come in. Nothing is written.
First make the new blog with khalid init. Use your own address, and the folder the blog will live in, such as /blog/. Put your export file beside the new blog folder.
The dry run lists what would come in, what would stay behind and why, and how many old addresses it maps. Read it before you import.
Add --redirection redirection.json to bring in the Redirection plugin's rules too. On the sample, that took the map from 5 old addresses to 13, and the report named the 5 rules it left behind, such as one that is switched off.
khalid init blog --address https://www.example.com --mount /blog/ -y
Made a Khalid site in blog for https://www.example.com/. It holds site.toml, routes.toml, theme/tokens.toml, theme/templates/, content/en/posts/ and drafts/en/.
khalid --site blog import wordpress export.xml --dry-run
This is a dry run: nothing was written.
Coming in: 1 post, 1 draft, 1 page, 2 authors and 2 categories.
Slugs kept: 2. Slugs made for posts that had none: 1.
Old addresses mapped: 5, of which 5 move and 0 stay the same.
Items with fields their files cannot hold yet: keep-a-starter-alive (tags, feature image).
Items that could not come in: 0.
Items left behind: an uploaded file whose page leads to its post (1: 12); a nav_menu_item, which Khalid has no place for (1: 14).
Redirect rules left behind: 0.
Import. Run it again without --dry-run.
The import writes each post as a Markdown file with its title, slug, date, author, category and description. If you used Yoast, Rank Math, All in One SEO or SEOPress, it reads their titles and descriptions too.
Posts keep their dates. Each one goes live at the next build once it passes the linter. Drafts go to the drafts folder.
A post the linter blocks stays out of the blog, and so do its old addresses. Those outside the blog's folder wait in blog/import/held-redirects.csv. Once the post passes, store them with khalid --site blog redirects import blog/import/held-redirects.csv. Give the path from where you run the command, as here.
The import will not write over files that are already there. Run it once, into a new blog.
khalid --site blog import wordpress export.xml
Wrote 7 files and stored 3 redirects.
1 imported post is blocked by the linter, so readers do not see it yet: content/en/posts/2024-03-12-keep-a-starter-alive.md.
Old addresses held in import/held-redirects.csv, because they lie outside the blog's folder: 2. Store them with `khalid redirects import import/held-redirects.csv` once their posts are live.
Coming in: 1 post, 1 draft, 1 page, 2 authors and 2 categories.
Slugs kept: 2. Slugs made for posts that had none: 1.
Old addresses mapped: 5, of which 5 move and 0 stay the same.
Items with fields their files cannot hold yet: keep-a-starter-alive (tags, feature image).
Items that could not come in: 0.
Items left behind: an uploaded file whose page leads to its post (1: 12); a nav_menu_item, which Khalid has no place for (1: 14).
Redirect rules left behind: 0.
Run `khalid lint` to see how each post scores.
Addresses. Every old address gets an entry in the map.
Each post keeps its slug, the last part of its address. When the rest of the address changes, the import stores a 301 redirect from the old address to the new one. That covers posts, pages, each image's own page and category archives.
The full list goes into blog/import/wordpress-redirects.csv, one row per old address. Khalid answers the old addresses inside the blog's folder. Old addresses outside it, such as /2024/03/my-post/ when the blog now lives at /blog/, reach your main site first. Add those rows to your main site's redirect rules. If Khalid runs the whole site at /, it answers them all.
redirects explain shows where any old address now leads.
khalid --site blog redirects explain /about/
/about/ matches rule 0, the exact path /about/.
1. /about/ moves for good (301) to https://www.example.com/blog/about/, by rule 0.
A visitor gets one step: moves for good (301) to https://www.example.com/blog/about/.
Images.
The importer does not download images. Each image in a post keeps the address it had, usually under /wp-content/uploads/. So they keep working as long as that address answers.
If you shut WordPress down, first copy its wp-content/uploads folder to your main site's server at the same path, so every image address still answers.
A post's feature image is not written into the post yet. The report names each post that had one. Add it back with image: in the post's header.
The honest limits.
Tags, shortcodes and focus keywords do not come in yet. The report names every post that had them, so nothing is lost silently.
Comments, menus and plugin data stay in WordPress. Private and binned posts stay behind too.
The linter blocks an imported post that links to none of your other posts. Run it before you switch over, and plan time to add those links.
Check. Lint, build, audit, then Search Console.
The linter scores each post out of 100 and names anything that blocks it. A blocked post stays out of the blog until you fix it, and its old addresses wait with it. So no old link lands on a page readers cannot see.
The build lists every post it left out and why. The audit crawls the built blog the way SEO crawlers do and scores its health. --offline skips the links to other sites.
Search Console. Once the blog is live, add its sitemap in Google Search Console. It is the blog's address plus sitemap.xml, such as https://www.example.com/blog/sitemap.xml. Khalid can also read your Search Console numbers with your own Google account. Then search-console index asks Google whether each post is in its index. The deploy guide has the steps.
khalid --site blog lint
khalid --site blog build
khalid --site blog audit --offline
khalid --site blog search-console check
1 block and 8 warnings.
Left out 1 post, because it is not ready to publish. The blocks are listed above.
Fix each block and build again, or move a post to drafts/<language>/ to keep it a draft.
Audited 8 pages: the ones readers and search engines reach through links and the sitemap.
Health score: 100 out of 100, the share of pages with no error. Warnings and notices do not lower it.
This site is not connected to Google Search Console, Google's report of how a site does in search.
Khalid asks Google nothing until the owner opts in under [search_console] in site.toml.
Questions about moving from WordPress.
Can't find the answer? Email us.
Do I need a plugin to export from WordPress?
No. Use Tools, Export, All content in the WordPress admin. Khalid reads that XML file. Only the Redirection plugin's rules need their own JSON export.
Does Khalid keep my Yoast or Rank Math titles?
Yes. The import reads the SEO titles and descriptions set in Yoast, Rank Math, All in One SEO and SEOPress. Focus keywords do not come in yet, and the report names each post that had one.
What happens to my WordPress images?
They stay where they are. The importer does not download them, so each image keeps its old address under wp-content/uploads. Before you shut WordPress down, copy that folder to your main site's server at the same path.
What does not come over from WordPress?
Comments, menus, plugin data, private posts and posts in the bin. Tags, shortcodes and feature images do not come in yet either. The report names every item that had them.
Why is an imported post not on my new blog?
The linter blocks it, most often because it links to none of your other posts. Run khalid lint to see why. Once you fix the post, it goes live at the next build, and its old addresses inside the blog's folder with it.
Bring the posts. Keep the addresses.
Move your blog to Khalid.
Free and open source. Start with the dry run: it writes nothing.