Khalid / Migrate / Substack

Move from Substack to Khalid: your posts on your own blog, paid posts kept for members.

Export your Substack, try the import with a dry run, then bring the posts in. Khalid keeps each slug, keeps paid posts for signed-in members, and writes down where every old address now leads.

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 substack substack-export --old-site https://rye.substack.com --dry-run
khalid --site blog import substack substack-export --old-site https://rye.substack.com
khalid --site blog lint
Four commands, run from the folder that holds your export. The dry run writes nothing.
01Substack

Export. Take your blog out of Substack.

Substack gives you an archive with a list of your posts, posts.csv, and one HTML file per post in a posts folder. Khalid reads both.

  1. Export from Substack

    In your Substack settings, find Exports and create a new export. Substack gives you a zip file.

  2. Unpack the zip

    You get posts.csv and a posts folder. Keep them together in one folder named substack-export.

  3. Note your Substack address

    Such as https://rye.substack.com. You give it to the import with --old-site.

The menu names are the ones Khalid's importer describes. Substack's help pages did not load for us on 4 Nov 2026, so check them in your own account.

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 Substack lived. The import uses it to build the map of old addresses.

The dry run lists what would come in, what would stay behind and why. Discussion threads stay behind, because they live in Substack's comments. So does a post with no text, such as a podcast episode with no notes.

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 substack substack-export --old-site https://rye.substack.com --dry-run
What it printed
This is a dry run: nothing was written.
Coming in: 1 post, 0 pages and 0 drafts.
For signed-in members only, as on Substack: 1.
Slugs kept: 1.
Old addresses mapped: 1, of which 1 move and 0 stay the same.
Old addresses on substack.com: 1. Substack cannot redirect them, so link each post there to its new home.
Content with no place in Khalid, left out: 0.
Posts left behind: 0.
Posts that could not come in: 0.
Rows of posts.csv that could not be read: 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.

Published posts keep their dates and go live at the next build once they pass the linter. A post that was for subscribers only on Substack is marked for signed-in members in Khalid.

Images, videos, buttons and embedded posts become Khalid blocks. The subscribe boxes Substack puts in posts are left out, and the report says so.

Import
khalid --site blog import substack substack-export --old-site https://rye.substack.com
What it printed
Wrote 2 files and stored 0 redirects.
1 imported post is blocked by the linter, so readers do not see it yet: content/en/posts/2024-03-12-why-rye.md.
Coming in: 1 post, 0 pages and 0 drafts.
For signed-in members only, as on Substack: 1.
Slugs kept: 1.
Old addresses mapped: 1, of which 1 move and 0 stay the same.
Old addresses on substack.com: 1. Substack cannot redirect them, so link each post there to its new home.
Content with no place in Khalid, left out: 0.
Posts left behind: 0.
Posts that could not come in: 0.
Rows of posts.csv that could not be read: 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.

Substack does not send readers on to a new home. So when your Substack lived on substack.com, Khalid cannot store redirects for it, and the sample stored none.

It still writes every old address and its new home to blog/import/substack-redirects.csv. Use the list to link each Substack post to its new home.

If your Substack used your own domain and Khalid now runs on that domain, give that domain as --old-site. Then the old /p/ addresses are yours. Old addresses outside the blog's folder reach your main site first, so add those rows to its redirect rules.

The redirect map
from,to,status,match
https://rye.substack.com/p/why-rye,https://www.example.com/blog/why-rye/,301,exact
The file Khalid 1.3.2 wrote for the sample export, 4 Nov 2026.

Images.

The importer does not download images. Each image keeps the address it had on Substack's image servers, so it loads from Substack for as long as Substack keeps it there.

To host an image yourself, save it into the blog and change its address in the post.

The honest limits.

The import brings posts, not people. Your subscriber list and payments stay in Substack, and Khalid does not import them.

Old substack.com addresses cannot redirect. Search results that point there stay on Substack until you link each post.

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 Substack.

Can't find the answer? Email us.

Can I redirect my old Substack posts?

Not from substack.com, because Substack does not send readers on to a new home. If your Substack used your own domain and Khalid now runs on it, the old addresses are yours and can redirect.

What happens to my paid posts?

A post that was for subscribers only on Substack is marked for signed-in members in Khalid, so it does not become public.

Does Khalid import my Substack subscribers?

No. The import brings posts only. Your subscriber list and payments stay in Substack.

What stays behind?

Discussion threads, which live in Substack's comments, and posts with no text, such as a podcast episode with no notes. Subscribe boxes in posts are left out too. The report lists each one.

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

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