{"record":{"id":"27b024dd12ec5774","repo":"TryGhost/Ghost","slug":"failed-to-convert-html-to-lexical","errorCode":null,"errorMessage":"Failed to convert HTML to Lexical","messagePattern":"Failed to convert HTML to Lexical","errorType":"validation","errorClass":"ValidationError","httpStatus":422,"severity":"error","filePath":"ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js","lineNumber":172,"sourceCode":"            defaultRelations(frame);\n        }\n    },\n\n    add(apiConfig, frame, options = {add: true}) {\n        debug('add');\n\n        if (_.get(frame,'options.source')) {\n            const html = frame.data.pages[0].html;\n\n            if (frame.options.source === 'html' && !_.isEmpty(html)) {\n                if (process.env.CI) {\n                    console.time('htmlToLexicalConverter (page)'); // eslint-disable-line no-console\n                }\n\n                try {\n                    frame.data.pages[0].lexical = JSON.stringify(lexical.htmlToLexicalConverter(html));\n                } catch (err) {\n                    throw new ValidationError({\n                        message: tpl(messages.failedHtmlToLexical),\n                        err\n                    });\n                }\n\n                if (process.env.CI) {\n                    console.timeEnd('htmlToLexicalConverter (page)'); // eslint-disable-line no-console\n                }\n            }\n        }\n\n        frame.data.pages[0] = url.forPost(Object.assign({}, frame.data.pages[0]), frame.options);\n\n        // @NOTE: force storing page\n        if (options.add) {\n            frame.data.pages[0].type = 'page';\n        }\n","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/ghost/core/core/server/api/endpoints/utils/serializers/input/pages.js#L154-L190","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Sanitize/normalize the HTML before sending (use a tolerant HTML parser like `jsdom` or tidy).","Remove unsupported tags/attributes from the payload and retry.","Inspect the wrapped `err` in the response to identify the exact conversion failure point.","If migrating between Ghost versions, re-validate the HTML against the current converter's supported subset."],"exampleFix":"// before\nawait api.pages.add({pages: [{html: rawImportedHtml}], options: {source: 'html'}});\n\n// after\nconst cleanHtml = sanitizeHtml(rawImportedHtml, {allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img'])});\nawait api.pages.add({pages: [{html: cleanHtml}], options: {source: 'html'}})","handlingStrategy":"validation","validationCode":"function isValidHtmlForConversion(html: string): boolean {\n    try {\n        new DOMParser().parseFromString(html, 'text/html');\n        return true;\n    } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try {\n    await api.pages.add({pages: [{html}], options: {source: 'html'}});\n} catch (err) {\n    if (/Failed to convert HTML to Lexical/i.test(JSON.stringify((err as any).response?.body))) {\n        html = sanitizeHtml(html);\n        await api.pages.add({pages: [{html}], options: {source: 'html'}});\n    } else { throw err; }\n}","preventionTips":["Sanitize imported HTML before sending it to the conversion endpoint.","Inspect the wrapped `err` to find the exact conversion failure.","Send Lexical JSON directly when you control the editor format."],"tags":["api","lexical","html","pages","validation","import"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}