Importing posts, and only posts, into Ghost.

If you've ever wanted to import a JSON file straight into Ghost, rather than using the Admin API, this post is for you.

A happy ghost carrying a moving box.
Importing a whole box full of posts today!
It's 100% possible to import posts and their tags by calling the Admin API, and most of the time that probably makes the most sense. Still, I'm documenting my experimenting with the import file format here in case it helps anyone.

If you've installed a paid theme, you've probably noticed that many of them have rather lengthy directions. "Create a page. Make sure to assign it to the membership template, and give it exactly this slug: "membership". Then create another page. Give it the slug "tags" and the "tags" template. Then..." It's no wonder rookie users find installing a Ghost theme sort of intimidating! I've been looking at import files as a way to more rapidly get new Ghost installs up and running. While importing content isn't automatic (that'd be nice, Ghost team...), I can imagine that it might be easier to have a user import a single JSON file, instead of creating half a dozen pages.

I sometimes want to import just a site's posts, without importing the various settings, staff users, and so on. Ghost's export writes out entire sites (minus members and images) as JSON. But can we import just one part, using this format and the Labs importer? Yes! It's possible! Here's a minimalist example that can be uploaded under /ghost > settings (gear icon) > labs > import :

{
    "db": [
        {
            "meta": {
                "exported_on": 1704312169707,
                "version": "5.75.1"
            },
            "data": {
                "posts": [
                    {
                        "title": "Your title",
                        "html": "<p>Valid HTML goes here.</p>",
                        "feature_image": null,
                        "featured": 0,
                        "type": "post",
                        "status": "published",
                        "locale": null,
                        "visibility": "public",
                        "show_title_and_feature_image": 1
                    }
]}}]}

Super simple. It's not necessary to convert your post to Lexical format. You can load in html instead, and let Ghost handle the conversion, as long at the HTML structure is valid and not especially complicated.

This example above doesn't tackle tags. Tags get created in one part of the import, and attached to a post in another, like so:

(Also within the "data" object above: )

                "posts_tags": [
                    {
                        "id": "65542734ade2a412315d49e3",
                        "post_id": "65542734ede2a412315d49de",
                        "tag_id": "65542734ede2a400015d49e0",
                        "sort_order": 0
                    },
                    ]

  .... and ....
                  "tags": [
                    {
                        "id": "65542734ede2a412315d49e0",
                        "name": "Tag name",
                        "slug": "tag-name",
                        "description": null,
                        "feature_image": null,
                        "parent_id": null,
                        "visibility": "public",
                        "og_image": null,
                        "og_title": null,
                        "og_description": null,
                        "twitter_image": null,
                        "twitter_title": null,
                        "twitter_description": null,
                        "meta_title": null,
                        "meta_description": null,
                        "codeinjection_head": null,
                        "codeinjection_foot": null,
                        "canonical_url": null,
                        "accent_color": "#11053b",
                        "created_at": "2023-11-15T02:04:36.000Z",
                        "updated_at": "2023-11-27T01:17:48.000Z"
                    },

So, it should be possible to create tags with an import file. (That "id" is going to be optional, and everything else can be made up on the spot.) But... I can't see how to attach the tags to the posts. I tried replacing post_id with post_slug and tag_id with tag_slug, but although the import created the post and created the tag and didn't email me an error message, it didn't actually attach the tag to the post either. Boo.

I was feeling pretty enthusiastic about uploading a .json as a way to get untagged posts into Ghost, but what if I we want the tags too, and for some reason I don't want to use the Admin API?

This is when it occurred to me that maybe I could just make up the IDs. Here's a fully working post upload that creates the post in Ghost and applies a tag to it. Note that the ID numbers used weren't assigned by Ghost ahead of time. Nope. I just made them up on the fly. And Ghost took them. Here's my rather minimal final version:

{
    "db": [
        {
            "meta": {
                "exported_on": 1704312169707,
                "version": "5.75.1"
            },
            "data": {
                "posts": [

                    {   "id": "6554273bede2a400015d49f6",
                        "slug": "imported-post-test-b",
                        "title": "An imported post that came in as html formatB",
                        "html": "<p>This is my overly simplistic HTML</p><ul><li>I can import lists, too</li><li>Cool, huh?</li></ul>",
                        "feature_image": null,
                        "featured": 0,
                        "type": "post",
                        "status": "published",
                        "locale": null,
                        "visibility": "public",
                        "show_title_and_feature_image": 1
                    }
                ],
                "tags": [
                    {   "id": "65542740ede2a400015d4a03",
                        "name": "Imported This Tag B",
                        "slug": "imported-tag"
                    }],
                "posts_tags": [
                    {
                        "post_id": "6554273bede2a400015d49f6",
                        "tag_id": "65542740ede2a400015d4a03",
                        "sort_order": 0
                    }
                ]

                }}]}

And it works! I got a published post, complete with the assigned tag.

Theme creators, check it out. There's an opportunity here to ONLY need new users to install the theme and load one import file. Everything else could be included. You could even create a couple sample posts, that specifically linked the user to the right spot in the admin panel to edit them.

Is my college professor identity showing through a little bit?

Yeah, maybe a little bit...