_emailmd_

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

RuleSeverityChecks
image-altwarningImages without alt text
insecure-linkwarningLinks or image sources using http://
gmail-clipwarningMinified HTML over 102KB (Gmail clips the message)
renderwarningRender warnings, folded in so one call surfaces everything
image-formatwarning / suggestionSVG and data: images, stripped by most clients (warning); WebP/AVIF, broken in Outlook desktop (suggestion)
link-textsuggestionGeneric link text ("click here", "here", "link")
preheader-missingsuggestionNo preheader in frontmatter
preheader-lengthsuggestionPreheader longer than ~100 characters (inbox previews truncate)
unsubscribesuggestionNo unsubscribe link anywhere in the rendered output
spam-wordssuggestionCommon spam-filter trigger phrases ("act now", "100% free", …)
placeholder-imagesuggestionImages or hero backgrounds on placeholder hosts (picsum.photos, placehold.co, wsrv.nl, …)
image-heavywarning / suggestionEmail 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.md

The 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.

On this page