capsule-capsulate

June 17, 2026 · View on GitHub

HTML document converter and processor for email templates. Transforms raw HTML through a composable plugin pipeline — inlining CSS, replacing query strings, fixing email client quirks, rendering templates, and more.

Installation

npm install capsule-capsulate
# or
pnpm add capsule-capsulate
# or
yarn add capsule-capsulate

Peer dependencies:

npm install capsule-lint vue

Quick Start

import { capsulate } from 'capsule-capsulate';

const html = await capsulate('<div id="contents">Hello world</div>', {
  template: {
    src: '<html><body>{{ contents }}</body></html>'
  }
});

API

capsulate(src, opts?)

Main function. Runs the full default plugin pipeline against an HTML string.

capsulate(src: string, opts?: Partial<CapsulateOptions> | (() => Partial<CapsulateOptions>)): Promise<string>

Options can be a plain object or a factory function (useful when options depend on runtime state).

only(options)

Create an options object that runs only the specified plugins — all others are disabled.

import { capsulate, only } from 'capsule-capsulate';

// Run only the HTML minifier
const result = await capsulate(html, only({ htmlMinifier: {} }));

defaultOptions(options?)

Merge partial options with sensible defaults. Useful for building option objects programmatically.

run(src, plugins)

Execute a custom plugin sequence directly.

import { run, ManipulateDom, DecodeHrefAmpersands } from 'capsule-capsulate';
import { ReplaceQueryStrings } from 'capsule-capsulate';

const result = await run(html, [
  new ManipulateDom([
    new ReplaceQueryStrings({ sourceCodes: [{ key: 'utm', to: 'newsletter' }] })
  ]),
  new DecodeHrefAmpersands()
]);

manipulate(src, plugins)

Convenience wrapper — run only DOM manipulation plugins.


Configuration

All options are optional. Set a key to false to disable that plugin entirely.

type CapsulateOptions = {
  extractTarget?: ExtractTargetOptions | false;
  htmlMinifier?: HtmlMinifierOptions | false;
  preserveHeadTag?: false;
  template?: TemplateOptions | false;
  dom?: DomOptions | false;
  preserveBodyAttributes?: false;
  previewText?: PreviewTextOptions | false;
  decodeHrefAmpersands?: false;
  courier?: CourierOptions | false;
  inlineCss?: InlineCssOptions | false;   // runs last
};

Template

Wrap the document in a Nunjucks template. The processed content is available as {{ contents }}.

template: {
  src: '<html><body>{{ contents }}</body></html>',
  data: { title: 'My Email' },          // variables for the template
  nunjucks: { autoescape: false }        // nunjucks configure options
}

HTML Minifier

Minifies with html-crush.

htmlMinifier: {
  lineLengthLimit: Infinity,
  removeIndentations: true,
  removeLineBreaks: true,
  removeHTMLComments: false,
  removeCSSComments: false,
  breakToTheLeftOf: [],
  mindTheInlineTags: []
}

Inline CSS

Inlines <style> rules into element style attributes using juice. This plugin always runs last.

inlineCss: {
  // juice options — see https://github.com/Automattic/juice#options
  removeStyleTags: true
}

Preview Text

Manage email preview text snippets.

previewText: {
  html: '<span style="display:none">Preview text here</span>'
  // or a function: ($ : CheerioAPI) => PreviewTextHtml
}

Extract Target

Extract a specific DOM element and discard the rest.

extractTarget: {
  selector: '#contents'
}

Courier

Attach a request ID attribute to trackable elements.

courier: {
  requestId: 'abc-123'
}

DOM Plugins

Fine-grained control over individual DOM manipulation plugins. Set any to false to disable.

dom: {
  decodeEntitiesInStyleAttributes: {},
  fixHrefQueryStrings: {},
  fixBackgroundColor: {},
  fixFontColor: {},
  fixMsoWrapper: {},
  fixResponsiveImages: { /* FixResponsiveImagesOptions */ },
  fixTableAlignment: {},
  removeDisplayNone: {},
  removeScriptTags: {},
  replaceQueryStrings: { sourceCodes: [...] },
  replaceNonAsciiChars: {}
}

Query String Helpers

replaceQueryString(href, replacements)

Replace, upsert, or delete query parameters in a single URL.

import { replaceQueryString } from 'capsule-capsulate';

Replace an existing value:

replaceQueryString('https://example.com/?src=OLD', [
  { key: 'src', from: 'OLD', to: 'NEW' }
]);
// → 'https://example.com/?src=NEW'

Replace a value across all keys (no key specified):

replaceQueryString('https://example.com/?a=foo&b=foo', [
  { from: 'foo', to: 'bar' }
]);
// → 'https://example.com/?a=bar&b=bar'

Upsert — set a key regardless of whether it exists (no from):

replaceQueryString('https://example.com/', [
  { key: 'utm_source', to: 'newsletter' }
]);
// → 'https://example.com/?utm_source=newsletter'

Delete — remove a key entirely:

replaceQueryString('https://example.com/?a=1&b=2', [
  { key: 'a', delete: true }
]);
// → 'https://example.com/?b=2'

mailto:, tel:, #anchor, and javascript: URLs are left untouched.

extractSourceCodes(html)

Extract all query parameter values and their occurrence counts from every URL in an HTML document.

import { extractSourceCodes } from 'capsule-capsulate';

const codes = extractSourceCodes(html);
// → { source_code: { 'ABC123': 3, 'XYZ789': 2 }, utm_campaign: { 'spring': 5 } }

extractUrls(html)

Extract every valid URL found in href and src attributes.

import { extractUrls } from 'capsule-capsulate';

const urls = extractUrls(html);
// → ['https://example.com/', ...]

useReplaceQueryStrings(src) (Vue 3 composable)

Reactive composable for building interactive query-string editors.

import { useReplaceQueryStrings } from 'capsule-capsulate';

const { sourceCodes, newSourceCodes, model, replace } = useReplaceQueryStrings(html);

// sourceCodes — reactive array of [key, replacements[]] pairs extracted from the HTML
// newSourceCodes — reactive array of upsert replacements to add to every link
// model — the current processed HTML (reactive)
// replace() — apply all replacements and return the updated HTML string

Freemarker Helpers

If your HTML contains Freemarker tags (<#if>, </#if>, ${...}, etc.), encode them before parsing to prevent conflicts with HTML parsers, then decode afterward.

import { encodeFreemarkerTags, decodeFreemarkerTags } from 'capsule-capsulate';

const safe = encodeFreemarkerTags('<#if user.active>Hello</#if>');
// safe to pass through HTML parsers

const restored = decodeFreemarkerTags(safe);
// → '<#if user.active>Hello</#if>'

Writing Custom Plugins

Extend BasePlugin for document-level transformations or BaseDomPlugin for per-element manipulation.

Document Plugin

import { BasePlugin } from 'capsule-capsulate';

class StripComments extends BasePlugin<{}> {
  async transform(src: string): Promise<string> {
    return src.replace(/<!--[\s\S]*?-->/g, '');
  }
}

Plugin lifecycle (each method is optional):

PhaseMethodDescription
1initialize()Set up state before processing
2preprocess(src)Transform the raw string before DOM parsing
3process($)Manipulate the Cheerio DOM
4postprocess(src)Transform the serialized HTML string
5transform(src)Final string transformation

DOM Plugin

import { BaseDomPlugin } from 'capsule-capsulate';

class AddDataAttr extends BaseDomPlugin<{}> {
  async process($: CheerioAPI) {
    $('a').each((_, el) => {
      $(el).attr('data-tracked', 'true');
    });
  }
}

Use ManipulateDom to wrap DOM plugins for use in run():

import { run, ManipulateDom } from 'capsule-capsulate';

await run(html, [new ManipulateDom([new AddDataAttr({})])]);

Development

# Install dependencies
pnpm install

# Run tests
pnpm test

# Type-check and build
pnpm run build

# Watch mode
pnpm run dev

Output formats:

FormatPath
ESMdist/capsule-capsulate.js
UMDdist/capsule-capsulate.umd.cjs
Typesdist/index.d.ts

Changelog

See CHANGELOG.md.