Skip to main content

Migrating from Ghost

This guide moves a Ghost site to Kiln. It covers Ghost 4, 5 and 6, self-hosted or on Ghost(Pro). It brings over posts, pages, tags, feature and body images, SEO titles and descriptions, authors and publication dates. Members-only posts stay members-only, and every old URL becomes a redirect.

mix kiln.import.ghost shares its import engine with the WordPress importer, so the dry run, the re-run behaviour and the report work the same way. See content portability for that engine's reference.

Before you start

  • A running Kiln site, with an admin account (getting started).

  • A way to run the importer. The commands below use mix, from a checkout with the same configuration as the site (the same DATABASE_URL, secrets and media storage settings). On a Docker or Coolify deploy there is no Mix, so run the same import inside the running container instead. Copy the export in, then call the release function through rpc:

    docker cp ghost-export.json <container>:/tmp/ghost-export.json
    docker exec -it <container> /app/bin/kiln_cms rpc 'KilnCMS.Release.import_ghost("/tmp/ghost-export.json", site_url: "https://blog.example.com", dry_run: true)'

    Each --flag below becomes a keyword option: --dry-run is dry_run: true, --limit 20 is limit: 20, --author-map [email protected] is author_map: %{"jo" => "[email protected]"}. Use rpc, not eval: the import needs the running application. The full table is in content portability.

  • The Ghost site still online. The export has image addresses, not image files, so the importer downloads each one from your Ghost site. Keep it up until you have checked the result.

1. Export from Ghost

In Ghost Admin, go to Settings → Advanced → Import/Export and choose Export under content. You get one .json file. It holds your settings, staff users, posts, pages and tags (Ghost's export docs).

You also need your site's address, such as https://blog.example.com. Ghost writes its own URLs into the export as a __GHOST_URL__ placeholder, so the importer needs the address to find your images.

2. Dry run

mix kiln.import.ghost ghost-export.json --site-url https://blog.example.com --dry-run

Nothing is written and nothing is downloaded. Read the report:

  • Records: how many posts and pages would be created or skipped.

  • Not as Ghost had it: posts the importer changed on purpose (see decisions).

  • Unreadable: posts whose body could not be converted. This only happens to very old posts that Ghost stored without rendered HTML. Open each one in Ghost, save it once, and export again.

  • Authors: each Ghost staff author, matched to a Kiln user by email. Unmatched authors' posts are credited to the user running the import. Map them with --author-map, using the author's Ghost slug:

    mix kiln.import.ghost ghost-export.json --site-url https://blog.example.com \
      --dry-run --author-map [email protected]

3. Import a slice, then the rest

mix kiln.import.ghost ghost-export.json --site-url https://blog.example.com --limit 20
mix kiln.import.ghost ghost-export.json --site-url https://blog.example.com

Check the slice in the editor and on the site before running the rest. Re-running skips everything that already exists. Add --drain-media if no Kiln node is processing background jobs where you run it.

4. Check redirects, then switch over

Ghost serves both posts and pages at /<slug>/. In Kiln, posts are at /blog/<slug> and pages stay at /<slug>. The importer adds a redirect from each old address, which you can see under Redirects at /editor/redirects.

If you changed Ghost's URL structure with a custom routes.yaml (for example /blog/{slug}/ or dated URLs), the importer cannot know that. Add those redirects yourself, and use the 404s tab under Redirects to catch any you missed after switching your domain over.

Decisions the importer makes

GhostKiln
Post / pagePost / page
Rendered HTMLTyped blocks: text, images, embeds
Gallery cardOne image block per image
Bookmark cardA paragraph linking its title to the bookmarked page
Video, audio and file cardsNot imported. The post is listed so you can move the files by hand
Callout, toggle and other cardsTheir text, as prose
Links to your own postsMade root-relative (/other-post/), so they reach the redirects
Public tagsTags, in Ghost's order
Internal #tagsNot imported. They are theme plumbing, never shown to readers
Primary tagJust the first tag. Kiln's category is left for you to set
Feature image and its alt textFeatured image
Meta title and descriptionSEO title and description
Custom excerptExcerpt
Canonical URLNot carried. It would point search engines back at Ghost
publishedPublished, with Ghost's publication date
scheduledDraft, with its date. Schedule it again in Kiln, so nothing goes live early
Email-only (sent)Draft. It was never on your site, so it is not published now
draftDraft
Visibility membersPublished to the member audience, so it stays gated
Visibility paid or a tierPublished to a paid audience if you configure one; otherwise to member, with a note

On gated posts: Kiln has one gated audience, member, unless you configure more (memberships). If paid and free members must see different things, add a paid audience (config :kiln_cms, :audiences, [:public, :member, :paid]) before importing. Paid and tier posts then go into it. Without it they go into member, which free members can read, and the import lists each one. On a site configured with no gated audience at all, gated posts import as drafts rather than being made public.

The importer needs --site-url for exports from older Ghost versions too: they store images as /content/images/… paths, which only resolve against the old site.

What does not come over

GhostWhat to do in Kiln
Members and subscribersNot imported. Export them from Ghost's Members screen as CSV. Kiln has no subscriber import yet, and a migration is a good moment to ask people to confirm their subscription again (newsletter).
Paid subscriptionsThey live in your Stripe account, not in Ghost's export. Set up tiers in Kiln (memberships) before moving paying members.
Newsletters already sentNot imported; email-only posts arrive as drafts.
CommentsNot imported.
Tiers, offers, benefitsRecreate them as membership tiers.
Code injectionNot imported. Kiln has site-wide code injection.
Navigation, themeRebuild with navigation menus and public theming.
Staff usersNot imported. Invite your team at /editor/team first, so authors match by email.
Post historyNot imported. History starts at the import.