Khalid / Migrate / Ghost

Migrate Ghost to Khalid: your posts, pages and authors, with a redirect for every old address.

Export your Ghost blog, try the import with a dry run, then bring the posts in. Khalid keeps each slug, keeps members-only posts out of public view, and stores a redirect for every address that moves.

Free and open source. Every command here was run with Khalid 1.3.2.

The whole move
khalid init blog --address https://www.example.com --mount /blog/ -y
khalid --site blog import ghost export.json --old-site https://www.example.com --dry-run
khalid --site blog import ghost export.json --old-site https://www.example.com
khalid --site blog lint
Four commands, run from the folder that holds your export. The dry run writes nothing.
01Ghost

Export. Take your blog out of Ghost.

Ghost exports your content and settings as one zip file. Khalid reads the JSON file inside it.

  1. Open Settings, Advanced, Import/Export

    In Ghost Admin, open the Export tab and click All data. Ghost saves a zip file.

  2. Unpack the zip

    Inside is export.json, your content. Khalid reads that file, not the zip.

  3. Note your Ghost address

    A Ghost export does not say where the blog lived. You give that address to the import with --old-site.

  4. Put export.json beside your new blog

    Keep it in the folder where you run the khalid commands.

Ghost's own migration guide, checked 4 Nov 2026. It says images and other media are not part of the export. See Images below.

02Terminal

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.

--old-site is where your Ghost blog lived. The import uses it to fill in Ghost's own addresses in your posts and to build the redirect map. Leave it out and Khalid uses the address of the new blog.

The dry run lists what would come in, what would stay behind and why. Read it before you import.

Make the blog
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/.

Dry run
khalid --site blog import ghost export.json --old-site https://www.example.com --dry-run
What it printed
This is a dry run: nothing was written.
Coming in: 1 post, 3 drafts, 1 page and 2 authors.
Slugs kept: 5. Slugs made for posts that had none: 0.
Old addresses mapped: 4, of which 4 move and 0 stay the same.
Tag archives not mapped until posts can hold tags: 2.
Posts with fields their files cannot hold yet: keep-a-starter-alive (search title, more authors, tags, feature image, Open Graph card, X card); about (canonical address); spring-flour (scheduled time); members-loaf (members only).
Posts for members only, kept as drafts so nothing paid is published: 1.
Posts with code injection, which is never carried: 6650a1f2c3d4e5f6a7b8c901.
Cards with no place in Khalid, left out: 0.
Posts that could not come in: 0.
Posts left behind: 0.
What Khalid 1.3.2 printed for the sample export in its source code, run on 4 Nov 2026.
03Terminal

Import. Run it again without --dry-run.

The import reads both of Ghost's editors, the newer one and the older one. Images, galleries, code, embeds, bookmarks, audio, video and file cards become Khalid blocks. A card Khalid has no place for is named in the report.

Published posts keep their dates and go live at the next build once they pass the linter. Drafts, scheduled posts and members-only posts go to the drafts folder, so nothing paid becomes public.

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.

Code injection is never carried over. The report names each post that had some.

Import
khalid --site blog import ghost export.json --old-site https://www.example.com
What it printed
Wrote 8 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: 1. Store them with `khalid redirects import import/held-redirects.csv` once their posts are live.
Coming in: 1 post, 3 drafts, 1 page and 2 authors.
Slugs kept: 5. Slugs made for posts that had none: 0.
Old addresses mapped: 4, of which 4 move and 0 stay the same.
Tag archives not mapped until posts can hold tags: 2.
Posts with fields their files cannot hold yet: keep-a-starter-alive (search title, more authors, tags, feature image, Open Graph card, X card); about (canonical address); spring-flour (scheduled time); members-loaf (members only).
Posts for members only, kept as drafts so nothing paid is published: 1.
Posts with code injection, which is never carried: 6650a1f2c3d4e5f6a7b8c901.
Cards with no place in Khalid, left out: 0.
Posts that could not come in: 0.
Posts left behind: 0.
Run `khalid lint` to see how each post scores.
What Khalid 1.3.2 printed for the sample export in its source code, run on 4 Nov 2026.
04Redirects

Addresses. Every old address gets an entry in the map.

Each post keeps its slug. The map covers every live post and page and each author's archive at Ghost's default addresses, such as /author/mara/. Each address that moves gets a 301 redirect to its new home.

The full list goes into blog/import/ghost-redirects.csv. Khalid answers the old addresses inside the blog's folder. Old addresses outside it, such as /my-post/ when the blog now lives at /blog/, reach your main site first. Add those rows to your main site's redirect rules.

Ghost's own redirects.yaml and custom routes are not read. If you set any up in Ghost, copy them across by hand.

Check one old address
khalid --site blog redirects explain /author/mara/
What it printed
/author/mara/ matches rule 1, the exact path /author/mara/.
1. /author/mara/ moves for good (301) to https://www.example.com/blog/authors/mara/, by rule 1.
A visitor gets one step: moves for good (301) to https://www.example.com/blog/authors/mara/.
What Khalid 1.3.2 printed for the sample export in its source code, run on 4 Nov 2026.

Images.

The importer does not download images. Ghost writes its own address into image links as a placeholder, and the import fills in your --old-site address. So an image ends up as, for example, https://www.example.com/content/images/2024/03/jar.jpg.

Those images keep working as long as that address answers. If you shut Ghost down, first copy its content/images folder to your main site's server at the same path. On a self-hosted Ghost the folder sits in Ghost's content directory.

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, extra authors, search titles and share cards do not come in yet. The report names every post that had them, so nothing is lost silently.

The import brings posts, not people. Members, subscribers and payments stay in Ghost.

Tag archive addresses are not mapped until posts can hold tags. The report counts them.

05After the move

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.

Checks after the move
khalid --site blog lint
khalid --site blog build
khalid --site blog audit --offline
khalid --site blog search-console check
What it printed
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.
Key lines from each answer, for the WordPress sample, run on 4 Nov 2026. Lint blocked the one post because it links to no other post.

Questions about moving from Ghost.

Can't find the answer? Email us.

Which Ghost file does Khalid read?

The export.json file inside the zip that Ghost Admin gives you under Settings, Advanced, Import/Export. Unpack the zip first.

Why does the Ghost import need --old-site?

A Ghost export does not say where the blog lived. Khalid uses that address to fill in Ghost's own links in your posts and to build the map of old addresses. Without it, Khalid uses the new blog's address.

What happens to members-only posts?

They come in as drafts, so nothing paid becomes public. The import brings posts, not people: members, subscribers and payments stay in Ghost.

What happens to my Ghost images?

They are not in Ghost's export, and the importer does not download them. Each image keeps its address under your old site's content/images folder. Copy that folder to your main site's server at the same path before you shut Ghost down.

Does Khalid read my Ghost redirects.yaml?

No. The import maps Ghost's default addresses for posts, pages and authors. Custom routes and redirects set up in Ghost need to be copied across by hand.

Bring the posts. Keep the addresses.
Move your blog to Khalid.

Free and open source. Start with the dry run: it writes nothing.