_emailmd_

Partials

Reusable markdown components spliced into emails with the include directive.

Partials are named blocks of markdown you define once and reuse across emails: a legal footer, a branded header, a recurring promo card. The document references them with the include directive, and edits to a partial propagate to every email that includes it.

# Your order has shipped

Track it from your account page.

::: include legal-footer

Partials are supplied to render() as a map of name → markdown string:

import { render } from 'emailmd';

const { html } = await render(markdown, {
  partials: {
    'legal-footer': `::: footer
**Acme Inc.** · 123 Main St, Portland OR
[Unsubscribe]({{unsubscribe_url}}) · [Preferences]({{prefs_url}})
:::`,
  },
});

The same map works everywhere RenderOptions is accepted; pass it to the builder's renderOptions prop and includes expand live in the preview.

There is no filesystem convention: emailmd also runs in the browser, so where partials live is up to your app. Server-side, loading them from a directory is a few lines:

import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';

const dir = './emails/partials';
const partials = Object.fromEntries(
  await Promise.all(
    (await readdir(dir)).map(async (f) => [
      path.basename(f, '.md'),
      await readFile(path.join(dir, f), 'utf8'),
    ]),
  ),
);

const { html } = await render(markdown, { partials });

The CLI has this built in: emailmd input.md --partials ./partials loads every .md file in the directory, with subdirectories as name prefixes (blocks/legal.md::: include blocks/legal). The flag also accepts individual .md files (named by basename) and is repeatable, with later paths winning on name collisions.

Parameters

Pass parameters on the include line to fill {{key}} placeholders inside the partial. This is what turns an include into a reusable component. Values with spaces need quotes:

::: include promo-card title="Summer sale" url="https://acme.com/sale"
partials: {
  'promo-card': `::: callout
**{{title}}**

[Shop now]({{url}}){button}
:::`,
}

Only keys explicitly passed on the include line are substituted. Every other {{token}} passes through untouched for your app's template layer (Handlebars, Liquid, …) to resolve after render, following the same pass-through contract as the rest of the pipeline. In the footer example above, {{unsubscribe_url}} survives into the output HTML exactly as written.

What a Partial Can Contain

Expansion happens textually before the markdown is parsed, so a partial can contain anything a document can: directives, buttons, images, tables, even other includes. Nested includes expand up to 10 levels deep, and cycles are detected and skipped.

The one exception is frontmatter: a partial that starts with a --- block gets it stripped with a warning. Frontmatter belongs on the document.

Partial names may contain letters, digits, _, -, ., and /, so a naming scheme like blocks/legal works if you want namespacing.

Errors Never Break the Render

Consistent with the rest of emailmd, problems degrade with warnings instead of throwing:

  • An unknown partial name drops the include line and warns.
  • A cycle (a partial including itself, directly or transitively) is skipped with a warning.
  • Nesting deeper than 10 levels stops with a warning.

Include lines inside code fences and indented code blocks are left alone, so you can document the syntax itself in an email.

Partials vs. Snippets vs. Wrappers

  • Snippets (in the builder) insert a copy at the cursor; later edits to the snippet don't affect emails already written.
  • Partials are live references: edit the partial and every email that includes it changes on the next render.
  • Wrappers replace the whole document chrome in code, the right tool for layout-level control rather than content reuse.

Custom Pipelines

If you bypass render() and drive the pipeline yourself, the expansion pass is exported as expandPartials(content, partials, warnings); call it on the frontmatter-stripped content before parseMarkdown.

On this page