TryGhost/Ghost · error · ValidationError
Email content is required
Error message
Email content is required
What it means
A ValidationError thrown by the automation email preview validator when `frame.data.lexical` is missing, not a string, or whitespace-only. The validator treats `lexical` (the Lexical editor JSON serialized as a string) as mandatory for both the `preview` and `sendTestEmail` operations. It fires before the Lexical structural validation, so it only catches empty/absent content.
Source
Thrown at ghost/core/core/server/api/endpoints/utils/validators/input/automation_email_previews.js:28
invalidEmailReceived: 'The server did not receive a valid email',
invalidLexical: 'Lexical must be a well-formed Lexical document',
subjectRequired: 'Subject is required',
lexicalRequired: 'Email content is required'
};
const validatePreviewData = async (frame) => {
const subject = frame.data.subject;
const lexical = frame.data.lexical;
if (typeof subject !== 'string' || !subject.trim()) {
throw new ValidationError({
message: tpl(messages.subjectRequired),
property: 'subject'
});
}
if (typeof lexical !== 'string' || !lexical.trim()) {
throw new ValidationError({
message: tpl(messages.lexicalRequired),
property: 'lexical'
});
}
if (!await lexicalLib.validate(lexical)) {
throw new ValidationError({
message: tpl(messages.invalidLexical),
property: 'lexical'
});
}
};
module.exports = {
async preview(apiConfig, frame) {
await validatePreviewData(frame);
},
View on GitHub (pinned to 47d8b0e2ad)
Solutions
- Stringify the Lexical editor state and pass it as `data.lexical`: `JSON.stringify(editorState.toJSON())`.
- Verify the post actually has content before triggering the preview — guard with a non-empty check upstream.
- Use the correct field name `lexical` (not `mobiledoc` or `html`) for this endpoint.
- If the source content is HTML/Mobiledoc, convert it to a Lexical document first via the Ghost lexical converter.
Example fix
// before
await api.sendTestEmail({data: {email, subject, lexical: editorState.toJSON()}});
// after
const lexical = JSON.stringify(editorState.toJSON());
if (!lexical.trim()) throw new Error('Editor has no content');
await api.sendTestEmail({data: {email, subject, lexical}}); Defensive patterns
Strategy: validation
Validate before calling
function ensureLexicalString(lexical) {
if (typeof lexical !== 'string' || !lexical.trim()) {
throw new Error('Email content is required');
}
return lexical;
} Type guard
const isLexicalString = (v) => typeof v === 'string' && v.trim().length > 0;
Try / catch
try {
await api.preview({data: {subject, lexical}});
} catch (err) {
if (err.type === 'ValidationError' && err.property === 'lexical' && /required/i.test(err.message)) promptForContent();
else throw err;
} Prevention
- Always stringify the editor state with JSON.stringify before sending.
- Disable the submit button until the editor has non-empty serialized content.
- Use the field name `lexical`, not `mobiledoc`/`html`.
When it happens
Trigger: Calling the automation preview/send-test-email endpoint with no `lexical` field, with `lexical: null`, or with `lexical: ""`. Also when the caller passes a Lexical object instead of its JSON-serialized string form and the field ends up undefined after frame parsing.
Common situations: A post has no Lexical document (e.g. a brand-new/draft post with empty content); the editor's `serialize()` output was not stringified before being sent; the wrong key name is used (e.g. `mobiledoc` or `html` instead of `lexical`); a migration left posts with null content.
Related errors
- Subject is required
- Lexical must be a well-formed Lexical document
- The server did not receive a valid email
- Failed to convert HTML to Lexical
- Failed to convert HTML to Lexical
AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13).
Data as JSON: /api/errors/ae71962cf6dc862d.
Report an issue: GitHub.