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
- 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.
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
- 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.
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
- Failed to convert HTML to Lexical
- Email content is required
- Lexical must be a valid JSON string
- Cannot disconnect Stripe whilst you have active subscription
- The NQL filter you passed was invalid.
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/27b024dd12ec5774.
Report an issue: GitHub.