Updating post content via Admin API: HTML/Lexical/MobileDoc changes not pushed through

I’m attempting to automatically add alt-texts to the images on my Ghost blog posts using the Admin API, but somehow this all fails when I try a PUT request to update the post – the PUT request works in general (e.g. for updating featured_image_alt), but fails completely on html or mobiledoc.

In short, I can GET the post data just fine, in html or lexical, but PUTting it fails completely. If I send the updated HTML or mobiledoc (converted from HTML), none of it gets through.

Here’s a Python code snippet:

url = f"{ghost_url}ghost/api/admin/posts/{post_id}/"
post = get_post(post_id)

# this works just fine
html = add_alt_texts_to_html(post_id)

data = {}
data["html"] = html
data["mobiledoc"] = html_to_mobiledoc(html)
data["updated_at"] = post["updated_at"]

ghost_token = get_ghost_token(ghost_api_key)
ghost_headers = {
    "Authorization": f"Ghost {ghost_token}",
    "Content-Type": "application/json",

response = requests.put(url, headers=ghost_headers, json=data)

# here it includes the post's original lexical data, without added alt tags. Viewing page in editor also shows no alt tags

I’m not too fussed about which format I read/write, whether it’s Lexical/Mobiledoc/HTML. I’d prefer HTML, but I’ll go with whatever works at this point

Please check Integration Flarum -> Ghost API (Python script) - #3 by AnimaVillis

There is how to get and post on flarum, but I think if you’re trying to make somethink like that it’ll be helpfull ;)

Not quite like that, but thanks! I’m looking to

  • pull post content to my local machine (works OK for HTML)
  • extract image tags via beautiful soup (working)
  • automatically generate alt text for each image using computer vision (working)
  • embed that into the HTML (working)
  • replace the post’s original HTML with the updated HTML (working)
  • push the updated post back to Ghost (this is where it fails)

The html field isn’t writeable as it’s generated from the lexical/mobiledoc data which is always the source of truth.

If you want to send HTML and have Ghost convert it to lexical/mobiledoc you need to use PUT /ghost/api/admin/posts/:id/?source=html, just be aware that it’s a conversion so can be lossy if you’re sending unsupported html.

What is the preferred format for writing to Ghost? I’d rather stay as lossless as possible.

I already tried writing Lexical/Mobiledoc before, but no joy on either front

lexical is the native format for the editor. mobiledoc is deprecated and will be auto-converted to lexical when the post is opened in the editor.

I already tried writing Lexical/Mobiledoc before, but no joy on either front

How exactly were you making the request where you tried to update lexical and what didn’t work?

Are you setting updated_at in your put? It needs to match the current value or the request will be rejected.

Maybe that’s not it, but it’s definitely tripped me up before! :)

Looks like that part’s fine :ok_hand:

1 Like

Oops! That’s what I get for reading in my phone. :)

I’ve got it working at last!

def update_post(post_id, post_data):
    url = f"{ghost_url}ghost/api/admin/posts/{post_id}/"

    # keeps whining about jwt token expired, so let's recreate before we send the data
    ghost_headers = {
        "Authorization": f"Ghost {ghost_token}",
        "Content-Type": "application/json",

    data = {"posts": [post_data]}

    console.print("\t- Sending updated post to [cyan]Ghost[/cyan]")
    response = requests.put(url, headers=ghost_headers, json=data)

    return response.json()
1 Like

And here’s the function that actually rewrites the post content:

def add_alts(post_id):
    post = get_post(post_id)
    print(f"- Processing [blue]{post['title']}[/blue]")

    # Process post featured image
    if not post["feature_image_alt"]:
        print("- Adding featured image alt text")
        post["feature_image_alt"] = create_alt_text(post["feature_image"])[
        ]  # Ghost is strict on this so I'm putting in hard limit

    # Process post body

    # convert string to dict
    post["lexical"] = json.loads(post["lexical"])

    for row in post["lexical"]["root"]["children"]:
        if row["type"] == "image":
            if not row["alt"]:
                alt_text = create_alt_text(row["src"])
                row["alt"] = alt_text

    # convert dict back to string
    post["lexical"] = json.dumps(post["lexical"])

    return post
1 Like