Struggling to update post using Admin API & Lexical

I had a JavaScript script for updating an existing post using the Admin API that no longer works. It took a markdown file and updated the post, which is made up of one long Markdown card. It stopped working after Ghost changed the storage from MobileDoc to Lexical. The script runs without producing an error but the content of the post doesn’t update. Has anyone else run into this problem? Can someone share a script that can work?

Can you show an example payload? Were you passing in mobiledoc? I think you’re going to need to switch to using lexical, but it should still be possible to post lexical containing a markdown card.

Markdown in a Lexical card looks like this:

lexical: {"root":{"children":[{"type":"markdown","version":1,"markdown":"#Markthis down\n## Hello world!\nHere's some text\n"},{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}

-- except you need to escape it, so it looks like this:--

lexical: "{\"root\":{\"children\":[{\"type\":\"markdown\",\"version\":1,\"markdown\":\"#Markthis down\\n## Hello world!\\nHere's some text\\n\"},{\"children\":[],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":null,\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}"

You’ll probably also need to specify that the format=lexical in your POST request.

The API still supports receiving the mobiledoc field in a PUT so I’d be interested to know exactly what’s not working. Are you getting an error message?

You may run into issues if you’re working with a post both in the editor and programmatically because the editor will auto-convert the mobiledoc field to lexical when opening a post. If that’s the case you’ll need to switch over to modifying the lexical field content rather than the mobiledoc field content.

That’s not needed, ?formats= only changes the data that’s returned from an API request. By default the Admin API posts endpoint will return both lexical and mobiledoc fields (one of which will be null depending on which format is currently saved).

Thank you for your replies. This is the code that used to work. It reads a Markdown file, sanitizes it (by stripping HTML comments and leaving only Markdown bullet points that start with +), and saves this as a new file. Then, it takes the new file and updates an existing blog post using the Admin API. This script used to work well. It stopped working a few months ago, around the time Ghost changed from MobileDoc to Lexical. Now, the script runs without any errors. I see the post history that the post was updated when the script runs, but the content doesn’t change. Also, the updated date and time that I show on my blog post (next to the publish date) doesn’t show that the post was updated.

I removed private data below for the URL, API Key, and Post ID, but I verified that the API Key is current and the Post ID is correct.

I don’t know why this script used to work but suddenly stopped working. I am grateful for any help.

// Import the 'fs' and 'path' modules
const fs = require('fs');
const path = require('path');

// Define the path of the input and output files
const inputFilePath = path.join(__dirname, '..', 'Law', 'law44', '_law.md');
const outputFileName = 'output-js.md';
const outputFilePath = path.join(__dirname, outputFileName);

// Read the contents of the input file
const fileContent = fs.readFileSync(inputFilePath, 'utf-8');

// Strip only HTML comments
const strippedContent = fileContent.replace(/<!--[\s\S]*?-->/g, '');

// Split the content into an array of lines
const lines = strippedContent.split('\n');

// Initialize a variable to hold the output content
let outputContent = '';

// Iterate over each line
lines.forEach(line => {
  // Check if the line starts with a '+' symbol
  if (line.trim().startsWith('+')) {
    // If it does, add the line to the output content
    outputContent += line + '\n';
  }
});

// Write the output content to the output file
fs.writeFileSync(outputFilePath, outputContent);

// Print a message to the console indicating that the code has finished running and the name of the output file that was generated
console.log(`Finished running the code! The output file is: ${outputFileName}`);



const GhostAdminAPI = require('@tryghost/admin-api');

// Configure the Ghost API
const api = new GhostAdminAPI({
    url: '............',
    key: '............',
    version: 'v5.0',
});

// Read the markdown file
const markdown = fs.readFileSync('output-js.md', 'utf8');

// Define the post ID
const postId = '............';



// Get the current updated_at value for the post from the server
api.posts.read({id: postId})
.then((post) => {
    // Store the updated_at value in a variable
    const serverUpdatedAt = post.updated_at;

    // Update the post
    api.posts.edit({
        id: postId,
        updated_at: serverUpdatedAt,
        mobiledoc: JSON.stringify({
            // version: '0.3.1',
			version: '0.3.2',
            markups: [],
            atoms: [],
            cards: [['markdown', {cardName: 'markdown', markdown}]],
            sections: [[10, 0]]
        })
    })
    .then((updatedPost) => {
        console.log(`Post updated: ${updatedPost.url}`);
    })
    .catch((err) => {
        console.error(err);
    });
})
.catch((err) => {
    console.error(err);
});

This suggests you’ve opened the post in the editor which will have converted it to Lexical format. In which case if you’re sending mobiledoc without also sending lexical: null (in order to switch back to mobiledoc) the API should return a 422 error stating that you can’t have both mobiledoc and lexical :thinking:

I’ll run a test to confirm. If it’s not doing that then we have a bug somewhere :grimacing:

Yes, I opened the post in the editor because there is custom JavaScript injected that allows the reader to fold the outline. I recently updated this folding script. I opened the outline post that I’ve been updating using the API but then decided against using the new folding script.

How would I send lexical: null to switch back to mobiledoc?

api.posts.edit({
        id: postId,
        updated_at: serverUpdatedAt,
        lexical: null,
        mobiledoc: JSON.stringify({
            // version: '0.3.1',
			version: '0.3.2',
            markups: [],
            atoms: [],
            cards: [['markdown', {cardName: 'markdown', markdown}]],
            sections: [[10, 0]]
        })
    })

But I think long-term you’d be better off switching over to lexical:

    api.posts.edit({
        id: postId,
        updated_at: serverUpdatedAt,
        mobiledoc: null, // ensure we don't hit errors when updating if this was previously mobiledoc
        lexical: JSON.stringify({
            root: {
                children: [{type: 'markdown', version: 1, markdown}],
                direction: null,
                format: '',
                indent: 0,
                type: 'root',
                version: 1
            }
        })
    })

Thanks for the correction! :slight_smile:

Thank you! I can confirm that adding lexical: null, solves the problem with my script. I agree that switching to Lexical is better for the long-term. I was going to ask about this but you anticipated my question. I will try the Lexical script now.

The Lexical script didn’t work. It removed all of the post’s content. I confirmed that the post is now empty by logging into Ghost. I would love to get the Lexical script working, if possible.

I think there’s an error here.
Shouldn’t it be this?

             children: [{type: 'markdown', version: 1, markdown: markdown}],

@Cathy_Sarisky Thank you so much! That fixes the problem with the Lexical script. I can confirm that it worked for me.

I have a follow-up question about the Lexical API:

I have a file that I write using HTML because I add various class attributes to the paragraph tags since I have JavaScript code that allows readers to filter the paragraphs based on these attributes.

I read in the Admin API documentation that I can wrap the HTML in a single Lexical card, but I’m not sure how to do this:

<!--kg-card-begin: html-->
<p>HTML goes here</p>
<!--kg-card-end: html-->

Do I add the kg-card comment to my content file so the opening tag is at the top and the closing tag is at the end?

How can I modify the Lexcial API script so I can update the post using the Amin API?

1 Like

That’s valid JS so I’m not sure why it would be failing from that :confused: There’s already a markdown variable defined so I used the shorthand for object initialisation.

You can add a HTML card directly if you’re writing a lexical document:

const html = 'Your html file content';

api.posts.edit({
    id: postId,
    updated_at: serverUpdatedAt,
    mobiledoc: null, // ensure we don't hit errors when updating if this was previously mobiledoc
    lexical: JSON.stringify({
        root: {
            children: [{ type: 'html', version: 1, html: html }],
            direction: null,
            format: '',
            indent: 0,
            type: 'root',
            version: 1
        }
    })
});

The comment wrapper syntax is for when you’re passing a html field rather than mobiledoc or lexical and you use ?source=html in the API request to have Ghost convert the HTML to rich-text (docs here). That allows you to add HTML cards interspersed within your otherwise editor-friendly content.

From your link: JSON only permits property definition using the "property": value syntax. The property name must be double-quoted, and the definition cannot be a shorthand. Computed property names are not allowed either.

I wasn’t aware that JS (not JSON) has a shorthand option (although I probably should have been), so thanks for that! I learned something today. :slight_smile:

There’s no JSON in my code sample, it’s a JS object that is getting stringified to JSON after initialisation :slight_smile:

Right. Point taken. I wonder why it was failing for the OP then? :) shrugs

@Kevin and @Cathy_Sarisky Thank you so much! I can confirm that children: [{type: 'html', version: 1, html: html}], works. As you mentioned, @Kevin I did not need to use the comment wrapper syntax. The API script produces one HTML card. I can also confirm that the card can contain both JavaScript and CSS. This is the post that I’m now updating with the Lexical HTML script: Headlines

1 Like

I’m still trying to understand ?source=html. The challenge I’m facing is that one of my posts that I would like to update using the Admin API has a public preview. The source file uses Markdown.

I see from an earlier thread that you wrote, " ?source=html will pick up the <!--members-only--> comment and convert to a paywall card." Setting public previews via Admin API - #6 by Kevin

Is there a way to create a public preview with the Lexical script that is working for me? If not, what steps do I need to take to be able to create the public preview? Do I have to convert the Markdown to HTML in order to use the members-only comment?

I have excellent news. A public preview can easily be created with the Lexical script that is working for me. I just tested this in a Markdown card both manually and through the Lexcial Admin API for Markdown. I was running into problems because I was writing <!-- members-only --> instead of <!--members-only-->. The extra spaces were preventing the public preview from being created. Thank you again @Kevin and @Cathy_Sarisky for your help.

1 Like