_emailmd_

API Reference

Complete API documentation for emailmd.

render(markdown, options?)

Renders markdown to email-safe HTML. Async since v0.3.0; returns a Promise.

Returns Promise<{ html, text, meta, warnings? }>

  • html: complete HTML email document
  • text: plain text version for the text/plain MIME part
  • meta: extracted frontmatter metadata
  • warnings: non-fatal issues encountered while rendering (e.g. invalid frontmatter YAML). Omitted when empty. See RenderWarning.
import { render } from "emailmd";

const { html, text, meta } = await render("# Hello");

RenderOptions

{
  theme?: Partial<Theme>;
  wrapper?: 'default' | WrapperFn;
  minify?: boolean;
  fonts?: Record<string, string>;
  validationLevel?: 'skip' | 'soft' | 'strict';
  templateSyntax?: Array<{ prefix: string; suffix: string }>;
  sanitizeStyles?: boolean;
  beautify?: boolean;
  strings?: RenderStrings;
  darkTheme?: true | Partial<Theme>;
  breaks?: boolean;
  allowHtml?: boolean;
  css?: string;
  partials?: Record<string, string>;
}
OptionTypeDescription
themePartial<Theme>Override default theme values
wrapper'default' | WrapperFnCustom email wrapper function
minifybooleanMinify output HTML. Default false.
fontsRecord<string, string>Custom web fonts: { [family]: url } rendered as <mj-font>.
validationLevel'skip' | 'soft' | 'strict'MJML validation level. Default 'soft'.
templateSyntaxArray<{ prefix: string; suffix: string }>Template delimiters preserved during compilation. Default [{{...}}, [[...]]].
sanitizeStylesbooleanSanitize template variables inside CSS before minification. Requires minify: true.
beautifybooleanPretty-print output HTML. Ignored when minify: true. Default false.
stringsRenderStringsOverride output strings for localization. See RenderStrings.
darkThemetrue | Partial<Theme>Opt into automatic dark mode. true = built-in dark palette; a partial theme merges over it. Overridden by a pinned frontmatter theme: light/dark.
breaksbooleanRender single newlines as <br>. Frontmatter breaks: wins per document. Default false.
allowHtmlbooleanAllow raw HTML tags in the Markdown source. Default true (raw tags pass through). Set false to escape raw tags (<script> → text) while keeping every Markdown feature. See Raw HTML.
cssstringCustom CSS emitted as one extra <mj-style> in the head, after the built-in styles so it can override them. Reaches renderer classes like .emd-btn (see CSS Hooks). Inline styles still win, so use !important, and as an embedded <style> not every client honours it. Trusted developer input only, not end-user-controlled strings.
partialsRecord<string, string>Named markdown partials spliced in at ::: include <name> lines.

RenderResult

{
  html: string;
  text: string;
  meta: { preheader?: string; [key: string]: unknown };
  warnings?: RenderWarning[];
}
PropertyTypeDescription
htmlstringComplete email-safe HTML document
textstringPlain text version
metaobjectExtracted frontmatter metadata
warningsRenderWarning[]?Non-fatal issues (e.g. invalid frontmatter YAML). Omitted when empty.

RenderWarning

{
  stage: 'frontmatter' | 'theme' | 'content' | 'mjml';
  message: string;
  cause?: Error;
}
PropertyTypeDescription
stagestringWhich render stage produced the warning (see below)
messagestringHuman-readable message
causeError?Original thrown error, when one is available

Warnings never stop the render: html/text output always contains a working email, so UIs can keep the preview alive and surface warnings alongside it. The stages:

StageMeaning
frontmatterThe YAML block could not be parsed. The body renders with empty meta.
themeA theme or font value was invalid (e.g. contained markup) and the default was used instead.
contentA directive or button value was invalid (an unknown color, alignment, length, or unsafe URL) and was dropped or replaced with the theme default.
mjmlThe MJML compiler reported a validation error (at the default soft level).

lint(markdown, options?)

Check an email for deliverability, accessibility, and readability problems. Unlike render warnings, which report things emailmd had to repair, lint findings flag content that renders fine but is likely to hurt the email in practice. Returns a Promise<LintFinding[]>, sorted by source line:

import { lint } from 'emailmd';

const findings = await lint(markdown, { partials });
// [{ rule: 'image-alt', severity: 'warning',
//    message: 'Image is missing alt text — …', line: 12 }]
interface LintFinding {
  rule: string;                          // machine-readable rule id
  severity: 'warning' | 'suggestion';    // warning = likely real problem
  message: string;
  line?: number;                         // 1-based line in the source markdown
}

The full rule list, severity semantics, and CLI and builder integration are documented on the Linting page. Template tokens are respected throughout: a {{tracking_url}} link is never flagged insecure, and [Unsubscribe]({{unsubscribe_url}}) satisfies the unsubscribe check.

Input validation

All user-supplied values that end up in the HTML are escaped and/or validated before interpolation:

  • The frontmatter preheader is HTML-escaped.
  • Directive params must be plausible values: colors must be hex, named, rgb()/rgba(), or hsl()/hsla(); alignment must be left, center, or right; lengths must be CSS length tokens like 8px or 50%.
  • Hero background URLs with javascript:, data:, vbscript:, or file: schemes are dropped.
  • Theme values containing <, >, or " are replaced with the base theme's value.
  • Font entries with unsafe family names or URLs are skipped.

Invalid values never throw; they fall back to safe defaults and surface as warnings. Template tags ({{ x }}, {% x %}, ${x}, %%x%%, [[x]], *|X|*) pass through validation untouched so merge fields keep working. Note that raw inline HTML in the markdown body is passed through by design: treat the markdown source as trusted, and resolve template variables with properly encoded values in your sending app.

RenderStrings

Overridable output strings, for localization:

{
  buttonFallback?: string;
}

buttonFallback is the sentence shown under a button rendered with the fallback attribute. It supports the placeholders {text} (the button label) and {url} (a clickable copy of the button link), and may contain HTML entities:

await render(md, {
  strings: {
    buttonFallback: 'Probleme mit &bdquo;{text}&ldquo;? Nutzen Sie diesen Link: {url}',
  },
});

A per-button fallback="custom text" attribute still takes precedence over the global string. See Buttons.

Theme

The full theme interface with all customizable properties:

interface Theme {
  brandColor: string;       // Links, highlights, accents
  headingColor: string;     // Heading text
  bodyColor: string;        // Body text
  backgroundColor: string;  // Outer background
  contentColor: string;     // Content area background
  cardColor: string;        // Callout / code block background
  buttonColor: string;      // Primary button background
  buttonTextColor: string;  // Button text
  secondaryColor: string;   // Secondary button border
  secondaryTextColor: string; // Secondary button text
  successColor: string;     // Success button background
  successTextColor: string; // Success button text
  dangerColor: string;      // Danger button background
  dangerTextColor: string;  // Danger button text
  warningColor: string;     // Warning button background
  warningTextColor: string; // Warning button text
  dividerColor: string;     // Horizontal rule color
  fontFamily: string;       // Font stack
  fontSize: string;         // Base font size (e.g. "16px")
  lineHeight: string;       // Base line height (e.g. "1.6")
  contentWidth: string;     // Email width (e.g. "600px")
  borderRadius: string;     // Border radius for buttons, callouts (e.g. "8px")
  mutedColor?: string;      // Secondary text; falls back to bodyColor
  chartColors?: string[];   // Data colors in series order; bars default to the first
  positiveColor?: string;   // Text of a good change; falls back to successColor
  negativeColor?: string;   // Text of a bad change; falls back to dangerColor
}

Pass a Partial<Theme> to render() to override any subset of values. See Theme for defaults.

darkTheme

A pre-built dark theme object. Import and pass directly or spread with overrides:

import { render, darkTheme } from "emailmd";

// Use as-is
await render(md, { theme: darkTheme });

// Override specific values
await render(md, { theme: { ...darkTheme, brandColor: "#e11d48" } });

buildDocument(body, theme, meta?)

function buildDocument(body: string, theme: Theme, meta?: WrapperMeta): string;

Wraps MJML body content in a complete document: the <mjml> root with the document's lang and dir, the head from buildHead with every option on meta (preheader, dark mode, direction, custom CSS), and the themed <mj-body>. The default wrapper is this function applied to segmentsToMjml's output. Used when building custom wrappers that only change the body.

buildHead(theme, preheader?, darkTheme?, dir?, css?)

function buildHead(
  theme: Theme,
  preheader?: string,
  darkTheme?: Theme,
  dir?: 'ltr' | 'rtl' | 'auto',
  css?: string,
): string;

Generates the MJML <mj-head> element containing global styles, font imports, the dark-mode overrides, custom CSS, and the hidden preheader text. Pass meta.preheader, meta.darkTheme, meta.dir and meta.css through from the wrapper's meta; any you leave out is dropped from the output. Prefer buildDocument, which passes them all, unless you need a custom <mjml> root or <mj-body>.

segmentsToMjml(segments, theme, ctx?)

function segmentsToMjml(
  segments: Segment[],
  theme: Theme,
  ctx?: { strings?: RenderStrings; warnings?: RenderWarning[]; dir?: 'ltr' | 'rtl' | 'auto' },
): string;

Converts parsed content segments into MJML body elements (sections, columns, text, buttons, images, tables, etc.). Used when building custom wrappers.

The optional ctx argument forwards localized strings, a warnings array to collect content validation issues during segment rendering, and the text direction. A wrapper's meta has all three, so pass it straight through: segmentsToMjml(segments, theme, meta).

Segment

interface Segment {
  type: 'text' | 'callout' | 'centered' | 'highlight' | 'header' | 'footer'
      | 'button' | 'button-group' | 'image' | 'hr' | 'table' | 'hero'
      | 'columns' | 'spacer' | 'social' | 'accordion' | 'chart'
      | 'progress' | 'sparkline' | 'stats' | 'breakdown' | 'steps' | 'rating' | 'raw';
  content: string;
  attrs?: Record<string, string>;
  buttons?: Array<Record<string, string>>;
}

Represents a parsed block of content from the markdown. The segments array is what gets passed to segmentsToMjml and to custom wrapper functions.

WrapperFn

type WrapperFn = (
  segments: Segment[],
  theme: Theme,
  meta?: WrapperMeta,
) => string;

A function that receives parsed segments, the resolved theme, and render context, and returns a complete MJML string. See Wrappers for usage examples.

WrapperMeta

The context object passed to wrapper functions:

{
  preheader?: string;
  frontmatter?: Record<string, unknown>;
  strings?: RenderStrings;
  warnings?: RenderWarning[];
  darkTheme?: Theme;
  lang?: string;
  dir?: 'ltr' | 'rtl' | 'auto';
  css?: string;
}
PropertyTypeDescription
preheaderstring?Preheader text from frontmatter; forward to buildHead
frontmatterRecord<string, unknown>?The full frontmatter map, including custom keys
stringsRenderStrings?Localized strings from render options; forward to segmentsToMjml
warningsRenderWarning[]?The render's warnings collector; forward to segmentsToMjml
darkThemeTheme?Resolved dark palette when dark mode is on; forward to buildHead
langstring?Validated frontmatter lang, for the <mjml> root's lang attribute
dir'ltr' | 'rtl' | 'auto'?Validated frontmatter dir; forward to buildHead, segmentsToMjml and the <mjml> root
cssstring?The css render option; forward to buildHead

buildDocument forwards all of these for you.

Sanitize helpers

The validation primitives used internally are exported for reuse:

import { escapeHtml, escapeAttrValue, isCssColor, isCssLength, isSafeUrl } from "emailmd";
  • escapeHtml(value): escape a raw string for HTML text or attribute context
  • escapeAttrValue(value): escape " and < without double-escaping existing entities
  • isCssColor(value): hex, named, rgb()/hsl() functional color, or template token
  • isCssLength(value): 1–4 CSS length tokens (e.g. 8px 16px), or template token
  • isSafeUrl(value): no scheme, a safe scheme (http, https, mailto, tel), or template token

Raw HTML

By default (allowHtml: true) raw HTML in the Markdown source passes through verbatim: <span style="…">, <div>, and inline tags like <u> all render as written. This is convenient for templates you control. To embed a larger block of HTML at the email's full width, without markdown processing, use a ::: raw block.

Set allowHtml: false to escape raw tags to text instead:

const { html } = await render(markdown, { allowHtml: false });

Now <script>alert(1)</script> renders as the literal characters rather than an element, while every Markdown feature (headings, emphasis, links, lists, tables, images, emoji, directives, buttons) keeps working. javascript:/data: URLs in Markdown links are blocked either way.

allowHtml: false also closes the two other ways Markdown can emit markup:

  • Attribute syntax. The {attr=…} syntax attaches attributes to the generated element (# Hi {onclick=…} becomes <h1 onclick="…">). In this mode event handlers (on*), inline style, and href/src overrides with a javascript:/data: scheme are dropped. Harmless attributes (class, id, data-*, and emailmd's own button, caption, width, align) are kept, so buttons, captions, and classes still work.
  • Template tags. Raw HTML wrapped in a template tag ({{<script>…</script>}}) is escaped to text instead of spliced back verbatim. A legitimate template variable like {{ profile_url }} has no HTML-significant characters, so it stays unchanged.

This is not a general HTML sanitizer. It prevents injection through emailmd's own syntax, but if your threat model demands high assurance, run the output through a dedicated sanitizer (tag/attribute allowlist) as a second layer.

On this page