Linting
Catch deliverability, accessibility, and readability problems before you send.
Rendering tells you the email works. Linting tells you whether it will land well. lint() checks an email for problems that render fine but hurt it in practice: missing alt text, http:// links, Gmail's 102KB clip limit, generic link text, spam-trigger phrases, and more.
import { lint } from 'emailmd';
const findings = await lint(markdown);
// [{ rule: 'image-alt', severity: 'warning',
// message: 'Image is missing alt text — …', line: 12 }]Each finding carries a machine-readable rule id, a severity, a human-readable message, and the 1-based line in the source markdown. See the API reference for the full signature.
Severities
- warning: likely a real problem. Fix these before sending.
- suggestion: worth a look, but often intentional (a transactional email may deliberately skip the unsubscribe link, for example).
Rules
| Rule | Severity | Checks |
|---|---|---|
image-alt | warning | Images without alt text |
insecure-link | warning | Links or image sources using http:// |
gmail-clip | warning | Minified HTML over 102KB (Gmail clips the message) |
render | warning | Render warnings, folded in so one call surfaces everything |
image-format | warning / suggestion | SVG and data: images, stripped by most clients (warning); WebP/AVIF, broken in Outlook desktop (suggestion) |
link-text | suggestion | Generic link text ("click here", "here", "link") |
preheader-missing | suggestion | No preheader in frontmatter |
preheader-length | suggestion | Preheader longer than ~100 characters (inbox previews truncate) |
unsubscribe | suggestion | No unsubscribe link anywhere in the rendered output |
spam-words | suggestion | Common spam-filter trigger phrases ("act now", "100% free", …) |
placeholder-image | suggestion | Images or hero backgrounds on placeholder hosts (picsum.photos, placehold.co, wsrv.nl, …) |
image-heavy | warning / suggestion | Email that is mostly images: under 100 characters of visible text (warning), or under 400 per full-width image (suggestion). See Image-Heavy Email |
Template tokens are respected throughout: a {{tracking_url}} link is never flagged insecure, and [Unsubscribe]({{unsubscribe_url}}) satisfies the unsubscribe check.
Image-Heavy Email
An email that is mostly images is a classic spam shape, and it says nothing when the reader's client blocks images. Spam filters check for it, but their image-only rules measure the raw HTML's length, which email's table-heavy markup always fills, so they rarely catch it. What they do catch is the side effect: the plain-text part, made up mostly of alt text, reads nothing like the HTML (SpamAssassin's MPART_ALT_DIFF_COUNT).
image-heavy measures what a reader actually sees in the rendered email:
- Text is the visible text, not counting alt text, the hidden preheader, or anything hidden with
display: none, such as an alternate phone layout. - Images are counted by width. An image 480px or wider counts as one full-width image, and narrower ones count in proportion, so three images side by side in columns count about the same as one across the email. Icons, social buttons and tracking pixels (48px or smaller) aren't counted, and images that add up to less than half a full-width image, such as a logo, are never flagged.
The rule asks for about 400 characters of text per full-width image: a short paragraph or two for a banner. Under 100 characters, the email is essentially images alone, and the finding is a warning.
In the CLI
emailmd lint runs the same checks from the terminal and prints findings with line numbers:
emailmd lint input.mdThe exit code is 1 when warnings are found, so it slots into CI; suggestions alone exit 0 unless you pass --strict. See CLI → Linting.
In the Builder
Pass the lint prop to <EmailmdBuilder /> (or the lint option to useEmailmd) and findings surface live alongside render warnings as you type.