ProvenPin Install

React Email template rendered to HTML and plain text: working versions (tested 2026-10-15)

Build a transactional email with @react-email/components (preview, rows/columns, button, link) in TSX and render both the HTML document and the plain-text version with @react-email/render, checking escaping offline.

passed5 passed · 0 failed2026-10-15 20:09 UTC
Status
passed
Last passed
2026-10-15 20:09 UTC
Tests
5 / 5
Language
TypeScript
Runtime
node v22.23.3
pinned versions6
@react-email/components@1.0.12@react-email/render@2.1.0react@19.3.1react-dom@19.3.1@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 38799498972

The code

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

package.json
{
  "name": "react-email-render-html-text",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "@react-email/components": "1.0.12",
    "@react-email/render": "2.1.0",
    "react": "19.3.1",
    "react-dom": "19.3.1"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/Welcome.tsx
import { Html, Head, Preview, Body, Container, Heading, Text, Button, Hr, Link, Section, Row, Column } from "@react-email/components";
import { render } from "@react-email/render";

export type WelcomeProps = { name: string; course: string; startDate: string; url: string; items: { label: string; hours: number }[] };

export function WelcomeEmail({ name, course, startDate, url, items }: WelcomeProps) {
  return (
    <Html lang="en">
      <Head />
      <Preview>{`You're enrolled in ${course}`}</Preview>
      <Body style={{ fontFamily: "Arial, sans-serif", backgroundColor: "#f4f4f4" }}>
        <Container style={{ padding: "24px", backgroundColor: "#ffffff" }}>
          <Heading as="h1">Welcome, {name}!</Heading>
          <Text>
            Your class <strong>{course}</strong> starts {startDate}.
          </Text>
          <Section>
            {items.map((i) => (
              <Row key={i.label}>
                <Column>{i.label}</Column>
                <Column align="right">{i.hours} h</Column>
              </Row>
            ))}
          </Section>
          <Button href={url} style={{ backgroundColor: "#0b3d91", color: "#ffffff", padding: "12px 20px" }}>
            View schedule
          </Button>
          <Hr />
          <Text style={{ fontSize: "12px" }}>
            Questions? <Link href="mailto:[email protected]">[email protected]</Link>
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

export async function renderWelcome(props: WelcomeProps) {
  const el = <WelcomeEmail {...props} />;
  const [html, text] = await Promise.all([render(el), render(el, { plainText: true })]);
  return { html, text };
}
test/welcome.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { renderWelcome } from "../src/Welcome.tsx";

const props = {
  name: "Sam <script>",
  course: "CITS Orientation",
  startDate: "Nov 1st",
  url: "https://example.com/schedule?c=cits&x=1",
  items: [{ label: "Classroom", hours: 6 }, { label: "Field", hours: 2 }],
};

test("renders a full HTML email document", async () => {
  const { html } = await renderWelcome(props);
  assert.match(html, /^<!DOCTYPE html PUBLIC "-\/\/W3C\/\/DTD XHTML 1\.0 Transitional\/\/EN"/);
  assert.match(html, /<html dir="ltr" lang="en">/);
  // React SSR puts <!-- --> markers between adjacent text nodes
  assert.match(html, /Your class <strong>CITS Orientation<\/strong> starts <!-- -->Nov 1st<!-- -->\./);
  assert.match(html, /href="https:\/\/example\.com\/schedule\?c=cits&amp;x=1"/);
  assert.match(html, /background-color:#0b3d91/);
});

test("user input is escaped", async () => {
  const { html } = await renderWelcome(props);
  assert.doesNotMatch(html, /<script>/);
  assert.match(html, /Sam &lt;script&gt;/);
});

test("preview text is a hidden preheader", async () => {
  const { html } = await renderWelcome(props);
  assert.match(html, /display:none[^>]*>You(&#x27;|')re enrolled in CITS Orientation/);
});

test("table layout for rows and columns", async () => {
  const { html } = await renderWelcome(props);
  assert.ok((html.match(/<table/g) ?? []).length >= 3);
  assert.match(html, /align="right"[^>]*>6<!-- --> h/);
});

test("plain text version", async () => {
  const { text } = await renderWelcome(props);
  assert.match(text, /WELCOME, SAM <SCRIPT>!/);
  assert.match(text, /Your class CITS Orientation starts Nov 1st\./);
  assert.match(text, /View schedule https:\/\/example\.com\/schedule\?c=cits&x=1/);
  assert.match(text, /^-{20,}$/m); // Hr becomes a dashed rule
  assert.doesNotMatch(text, /<table|<td/);
});
tsconfig.json
{ "compilerOptions": { "jsx": "react-jsx", "module": "esnext", "moduleResolution": "bundler", "target": "es2022", "strict": true } }

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.