Skip to content

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GhostToGrav

This script will convert a ghost blog export to grav blog item.md files.

Ghost version: updated in August 2026 and confirmed against an export from a current Ghost 6 site. It also handles the Ghost 5 exports that share the same lexical schema, and the older mobiledoc and pre-mobiledoc ones it was originally written for, so an export from any of them should convert. Ghost has changed its export schema roughly every couple of years, and each time it has broken this script quietly rather than obviously, so if you are here from a future version that does not work, the fixtures and tests in tests/ are the quickest way to see what moved.

Usage:

  • -g ghostexport.json Required: Specify the json file that contains the exported ghost blog data.
  • -o path Optional Specify the path to export to. Defaults to ./01.blog
  • -l lang Optional Specify the two letter language code, eg en. This will add the language code suffix to the blog and item.md files as blog.en.md and item.en.md. If you omit this argument then no language code suffix will be used.
  • -f quoted string Optional Add additional frontmatter tags to item.md. Specify this argument as many times as desired. e.g., -f "show_sidebar: true" -f "hero_classes: text-light" These will be written one after the other so spaces should be kept, otherwise you can try to specify everything in a line using \n as a separator.
  • -s Optional If this argument is present, the path for item.md will be generated from the ghost post slug. Otherwise, the path is generated from the ghost post title.
  • -u url Optional The address of the ghost site, eg https://example.com. Since Ghost 4, links to the site's own content are stored as a __GHOST_URL__ placeholder, and this is what the placeholder is replaced with. Omit it and the placeholder is replaced with nothing, which leaves those links relative to the site root.
  • -i path Optional Path to the content folder of an unzipped ghost export, ie the folder that holds images. Any images, media or files a post uses are copied in to that post's folder and the links are rewritten to match, which is how Grav expects page media to be laid out.
  • -d Optional Download anything that could not be found locally from the site given by -u. Needs -u.
  • -p path Optional Folder to write ghost pages to, eg ./pages. Pages are written as default.md rather than item.md and do not get the blog category. Omit this and pages are converted as blog items, as before.
  • --html Optional Posts that only have html content are converted to markdown by default. If this argument is present, their html is written to item.md as-is instead.

I largely wrote this to convert my own data, so I've made some assumptions. I thought other people may find this useful, so I added some features like the lang and frontmatter options.

Unless you pass -p, this script does not distinguish between pages and posts in the ghost export.

Ghost slugs are unique but post titles are not, so two posts can end up wanting the same folder. The script gives the second one a numbered suffix and tells you which ones it renamed, rather than letting them overwrite each other.

The script extracts authors and applies them to their respective posts. If there is only one user, they are set as the post author. A ghost post can have several authors but author: has room for one, so the primary author is used and the rest are counted at the end. The script automatically adds the 'blog' category taxonomy to each item.md. If there are any tags applied to a post, the script will extract them and apply them to the respective item.md, in the order ghost had them so that the primary tag stays first. Ghost's internal #hashtag tags are left out, since ghost keeps those off the site as well.

Ghost posts that were never on the site, meaning drafts, scheduled posts and email only ones, all get published: false. Ghost also has members only and paid posts, which Grav has no equivalent for. Those are converted like anything else and would be public, so the script lists them at the end for you to deal with.

Post text from the plaintext key is written as-is, it is not converted to markdown. Newer Ghost exports (Ghost 5 and up, which use the lexical editor) often leave plaintext empty, so the script falls back to the html key, then to lexical, then to mobiledoc, and tells you at the end which posts it had to fall back on. The html fallback is converted to markdown, unless you pass --html to keep it as-is, which Grav will render quite happily too. The lexical and mobiledoc fallbacks are best effort and only try to recover the words, not the formatting.

Images, media and downloads are handled if you point -i at the content folder of your unzipped ghost export, or use -d to pull them off a site that is still up. They are copied in to each post's folder and referenced by filename, which is how Grav does page media. The feature image is copied across too and written to the frontmatter as hero_image, which your theme may or may not use. Ghost generates the resized /content/images/size/w600/... variants on the fly, so the original is always the one that gets copied. Anything that cannot be found is reported and left pointing at the ghost site.

This script should work with Python 3.10 and up.

I highly recommend using virtualenv first so that your system packages don't get clobbered: python -m venv venv then ./venv/bin/activate or venv\Scripts\activate.bat for Windows. Then do pip install -r requirements.txt to install the python-slugify and markdownify dependencies.

Tests

Ghost changes its export schema every few years, and the breakage only turns up when somebody runs the script on a fresh export. There are fixtures in tests/fixtures standing in for the exports this has been caught out by so far, from the old schema through to the lexical editor. Run them with python -m unittest discover tests once the requirements are installed.

LLM Notes

I wrote the original script 4 years ago and I no longer use Ghost myself. When someone opened an issue against it, I used Claude Opus 5 to bring it up to date with Ghost's current export schema, fix the bugs that were in the original, and fill in some of the gaps in the conversion along the way. See CHANGELOG.md for everything that changed, and TODO.md for what was left.

About

Python script to convert ghost blog export json to grav item.md files.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages