TryGhost/Ghost · error · ValidationError

Failed to convert HTML to Lexical

Error message

Failed to convert HTML to Lexical

What it means

Thrown by the pages input serializer when `frame.options.source === 'html'` and a non-empty `html` payload fails `lexical.htmlToLexicalConverter`. The conversion error is wrapped in a ValidationError with the `failedHtmlToLexical` message so the API returns a 422 instead of crashing. Used when importing pages from HTML into the Lexical editor format.

Source

Thrown at ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js:172

            defaultRelations(frame);
        }
    },

    add(apiConfig, frame, options = {add: true}) {
        debug('add');

        if (_.get(frame,'options.source')) {
            const html = frame.data.pages[0].html;

            if (frame.options.source === 'html' && !_.isEmpty(html)) {
                if (process.env.CI) {
                    console.time('htmlToLexicalConverter (page)'); // eslint-disable-line no-console
                }

                try {
                    frame.data.pages[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));
                } catch (err) {
                    throw new ValidationError({
                        message: tpl(messages.failedHtmlToLexical),
                        err
                    });
                }

                if (process.env.CI) {
                    console.timeEnd('htmlToLexicalConverter (page)'); // eslint-disable-line no-console
                }
            }
        }

        frame.data.pages[0] = url.forPost(Object.assign({}, frame.data.pages[0]), frame.options);

        // @NOTE: force storing page
        if (options.add) {
            frame.data.pages[0].type = 'page';
        }

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Sanitize/normalize the HTML before sending (use a tolerant HTML parser like `jsdom` or tidy).
  2. Remove unsupported tags/attributes from the payload and retry.
  3. Inspect the wrapped `err` in the response to identify the exact conversion failure point.
  4. If migrating between Ghost versions, re-validate the HTML against the current converter's supported subset.

Example fix

// before
await api.pages.add({pages: [{html: rawImportedHtml}], options: {source: 'html'}});

// after
const cleanHtml = sanitizeHtml(rawImportedHtml, {allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img'])});
await api.pages.add({pages: [{html: cleanHtml}], options: {source: 'html'}})
Defensive patterns

Strategy: validation

Validate before calling

function isValidHtmlForConversion(html: string): boolean {
    try {
        new DOMParser().parseFromString(html, 'text/html');
        return true;
    } catch { return false; }
}

Try / catch

try {
    await api.pages.add({pages: [{html}], options: {source: 'html'}});
} catch (err) {
    if (/Failed to convert HTML to Lexical/i.test(JSON.stringify((err as any).response?.body))) {
        html = sanitizeHtml(html);
        await api.pages.add({pages: [{html}], options: {source: 'html'}});
    } else { throw err; }
}

Prevention

When it happens

Trigger: A page add/edit request is sent with `source=html` and an `html` field that the converter cannot parse — malformed HTML, unsupported tags, encoding issues, or a converter library bug.

Common situations: Importing content from another platform with malformed/legacy HTML; HTML containing elements the converter does not support; character encoding problems (BOM, wrong charset); a version bump in the html-to-Lexical converter that rejects previously-valid input.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/27b024dd12ec5774. Report an issue: GitHub.