ProvenPin Install

Handlebars email template helpers: working versions (tested 2026-10-15)

Compile Handlebars email layouts with partials and helpers, covering conditionals, loops and HTML escaping offline.

passed2 passed · 0 failed2026-10-15 16:00 UTC
Status
passed
Last passed
2026-10-15 16:00 UTC
Tests
2 / 2
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
handlebars@4.7.10@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38764699834

The code

Exactly the files that passed. Your agent can pull them with get_recipe("handlebars-email-templates") (install), or see them on GitHub.

package.json
{
  "name": "handlebars-email-templates",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {"handlebars":"4.7.10"},
  "devDependencies": {"@types/node":"26.6.5","tsx":"4.23.15"}
}
src/templates.ts
import Handlebars from "handlebars";

export type InvoiceItem = { name: string; qty: number; price: number };

Handlebars.registerHelper("money", (n: number) => `$${(Number(n) || 0).toFixed(2)}`);
Handlebars.registerHelper("eq", (a: unknown, b: unknown) => a === b);

const layout = Handlebars.compile(`<!doctype html>
<html><body>
{{> header}}
<main>{{{body}}}</main>
</body></html>`);

Handlebars.registerPartial("header", "<header><h1>{{brand}}</h1></header>");

const invoiceBody = Handlebars.compile(`
<p>Hello {{name}},</p>
{{#if premium}}<p>Thanks for being premium.</p>{{/if}}
<ul>
{{#each items}}
  <li>{{name}} x{{qty}} - {{money price}}</li>
{{/each}}
</ul>
<p>Total: {{money total}}</p>
`);

export function renderInvoice(data: {
  brand: string;
  name: string;
  premium: boolean;
  items: InvoiceItem[];
}) {
  const total = data.items.reduce((s, i) => s + i.qty * i.price, 0);
  const body = invoiceBody({ ...data, total });
  return layout({ brand: data.brand, body });
}
test/templates.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { renderInvoice } from "../src/templates.ts";

test("renders invoice with partials and helpers", () => {
  const html = renderInvoice({
    brand: "ProvenPin",
    name: "Ada",
    premium: true,
    items: [
      { name: "Widget", qty: 2, price: 9.5 },
      { name: "Gadget", qty: 1, price: 4 },
    ],
  });
  assert.match(html, /<h1>ProvenPin<\/h1>/);
  assert.match(html, /Hello Ada/);
  assert.match(html, /premium/);
  assert.match(html, /Widget x2/);
  assert.match(html, /\$9\.50/);
  assert.match(html, /Total: \$23\.00/);
});

test("escapes HTML in names", () => {
  const html = renderInvoice({
    brand: "X",
    name: "<script>x</script>",
    premium: false,
    items: [],
  });
  assert.doesNotMatch(html, /<script>x<\/script>/);
  assert.match(html, /&lt;script&gt;/);
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.