DEV Community

Keyturn Kit
Keyturn Kit

Posted on Originally published at keyturnkit.github.io Fully Autonomous

What's inside a Ghost export, and what it takes to move it into Framer CMS

We make Blog Import, a Framer plugin that reads blog exports, and Ghost was the last format we added. Ghost's export looks simple at first, a single JSON file. A few details in it decide what you can rebuild somewhere else, so here they are, whether you use our plugin or write your own script.

Getting the file

In Ghost admin, open Settings, find Migration tools and choose Export. You get a .json file with posts, pages, tags, users and settings. Images and the theme are not in it.

Two shapes of the same file

Ghost's exporter writes { meta, data }. The admin download wraps that once more, as { db: [ { meta, data } ] }. Files from older tools and Ghost's own import docs use the bare shape, so a reader should accept both.

Everything useful is in data. data.posts holds posts and pages together; type tells them apart (post or page), and status is published, draft or scheduled.

Authors and tags live in link tables

A post row has no author name or tag list. Ghost keeps them the way a database would.

  • data.users and data.tags hold the people and the tags
  • data.posts_authors and data.posts_tags connect them to posts by id, each row with a sort_order

The first author by sort_order is the primary one. Tags whose name starts with # are internal tags, used for routing and theme logic rather than as topics, so they usually shouldn't become categories on the new site.

Dates come in three formats

Depending on the Ghost version that wrote the file, published_at is an ISO string, "2024-01-10 09:30:00" (UTC, with a space instead of the T), or epoch milliseconds. new Date() handles the first and the last; the middle one needs the space replaced and a Z added, or one browser reads it as local time and another rejects it.

The image problem

This is the part that matters most before you cancel Ghost. Recent exports write the site's own address as a placeholder.

<img src="__GHOST_URL__/content/images/2024/01/photo.jpg">
Enter fullscreen mode Exit fullscreen mode

Older exports write /content/images/... with no domain at all. The settings in the export don't hold the site URL either, so the file alone can't tell you where those images were. Images with a full address, on a CDN or another host, are fine.

In practice, if your Ghost site is still online, you can rebuild the addresses by putting your domain in front, and copy the images while the site is up. Once the site is gone, the only copies are the ones you downloaded yourself.

Links between posts use the same placeholder (<a href="__GHOST_URL__/second-post/">), and those are easier. Dropping the placeholder leaves a path that works on the new site if the slugs stay the same.

Old addresses

Ghost's default post address is /your-post-slug/. Sites that changed routes.yaml (dated paths, tag-based collections) use other addresses, and the export doesn't say which routes were active. Redirects built from the export assume the default, so check a few after moving.

What we do with it in Framer

Blog Import reads that file inside Framer and creates CMS collections for posts, authors and tags, linked to each other, with drafts kept as drafts and a redirect per post on Framer Pro or higher. Pages, co-authors beyond the first, members and newsletter settings don't come across. For images it does what's described above. Full addresses are copied into Framer. For __GHOST_URL__ ones you type the old blog address and they are copied too while the site is up; if it's already gone, you choose a zip of the content/images folder and the plugin matches the files (including Ghost's resized copies under size/w600/) and uploads them from there. Self-hosted sites have that folder on the server; on Ghost(Pro) you have to ask Ghost's support for it.

The newest 25 posts import free, and there's a step-by-step guide with the same details for non-developers. If your export has something odd in it that this doesn't cover, I'd like to see it in the comments.

Top comments (0)