ProvenPin Install

mjml to html: working versions (tested 2026-10-16)

Render MJML markup to responsive HTML email with mjml2html, asserting expected text and inlined styles, running validation in soft mode and inspecting the errors array for intentionally broken markup.

passed3 passed · 0 failed2026-10-16 02:18 UTC
Status
passed
Last passed
2026-10-16 02:18 UTC
Tests
3 / 3
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
mjml@5.4.1@types/mjml@5.0.0@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 38839686104

The code

Exactly the files that passed. Your agent can pull them with get_recipe("mjml-to-html") (install), or see them on GitHub.

package.json
{
  "name": "mjml-to-html",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "mjml": "5.4.1"
  },
  "devDependencies": {
    "@types/mjml": "5.0.0",
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/convert.ts
import mjml2html from "mjml";

export type RenderError = {
  message: string;
  line?: number;
};

export type RenderResult = {
  html: string;
  errors: RenderError[];
};

export async function render(source: string): Promise<RenderResult> {
  const { html, errors } = await mjml2html(source, { validationLevel: "soft" });
  return {
    html,
    errors: (errors ?? []).map((error: { message: string; line?: number }) => ({
      message: error.message,
      line: error.line,
    })),
  };
}
test/convert.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { render } from "../src/convert.ts";

const TEMPLATE = `
<mjml>
  <mj-body>
    <mj-section background-color="#f0f0f0">
      <mj-column>
        <mj-text font-size="20px" color="#333333">Hello MJML</mj-text>
        <mj-button href="https://example.com" background-color="#ff5500">Click me</mj-button>
      </mj-column>
    </mj-section>
  </mj-body>
</mjml>`;

test("renders mjml to html with expected text", async () => {
  const { html, errors } = await render(TEMPLATE);
  assert.deepEqual(errors, []);
  assert.match(html, /Hello MJML/);
  assert.match(html, /Click me/);
  assert.match(html, /https:\/\/example\.com/);
});

test("inlines styles derived from mjml attributes", async () => {
  const { html } = await render(TEMPLATE);
  assert.match(html, /20px/);
  assert.match(html, /#333333|#333/i);
  assert.match(html, /#ff5500|#f50/i);
  assert.match(html, /background(-color)?/i);
});

test("reports validation errors for broken markup in soft mode", async () => {
  const broken = `<mjml><mj-body><mj-text>no column</mj-text></mj-body></mjml>`;
  const { html, errors } = await render(broken);
  assert.ok(errors.length > 0, "expected validation errors");
  assert.ok(errors.some((error) => error.message.length > 0));
  assert.equal(typeof html, "string");
});

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.