Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
e4a42c6
feat(examples): send Elements emails through 14 providers, and add an…
ivoIturrieta Oct 16, 2026
6e2d6b2
docs(react): add guides for email providers, i18n and monorepos
ivoIturrieta Oct 16, 2026
4551a96
docs(migrate): add guides for migrating from MJML and from Beefree
ivoIturrieta Oct 16, 2026
7bd0f0c
docs(react): add the upgrade guide for 0.3 and 0.2
ivoIturrieta Oct 16, 2026
386a366
docs(examples): say Azure's SDK declares Node 22 or newer
ivoIturrieta Oct 16, 2026
dfe82d7
docs(react): say in the 0.3 upgrade guide what its renderer changes
ivoIturrieta Oct 17, 2026
1ac0eec
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 18, 2026
ca80561
docs(react): say in the guides what 0.3 changed since they were written
ivoIturrieta Oct 18, 2026
5d5c13c
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 20, 2026
47cdf1b
docs(react): say when an MJML email stays left to right, and that fai…
ivoIturrieta Oct 20, 2026
56d20ab
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 20, 2026
f1997b0
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
24fdf28
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
4d07fd1
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
899d7a9
docs(react): say which Reacts the tests run on, and that an async tem…
ivoIturrieta Oct 22, 2026
9a80382
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
e950605
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
ea14ed8
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 22, 2026
cd2a803
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
f736575
docs(react): list the renderToString context change in the upgrade guide
ivoIturrieta Oct 23, 2026
b24c739
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
fe2e487
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
904937d
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
8b01845
docs(react): list javascript: URLs and null links in the upgrade guide
ivoIturrieta Oct 23, 2026
fecb448
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
3ce9d78
docs(react): say every javascript: link runs nothing, and null entrie…
ivoIturrieta Oct 23, 2026
fd8ab24
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
9775e72
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
7981f69
docs(react): say a block's javascript: link is caught however it's wr…
ivoIturrieta Oct 23, 2026
4b5d62e
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
c1766e9
docs(react): say in the upgrade guide a Table's cells and style props…
ivoIturrieta Oct 23, 2026
0b58c05
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 23, 2026
26c2141
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
0473360
docs(react): say in the upgrade guide a null prop is one not given, a…
ivoIturrieta Oct 24, 2026
c1a7127
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
d4963bb
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
8a4d45e
docs(react): say a null content prop renders nothing and a Video's id…
ivoIturrieta Oct 24, 2026
f488b69
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
33953ab
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
20319c1
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
e8e8aab
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
f91dea8
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
9089fc1
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
b4ea875
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
82ced63
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 24, 2026
b052691
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 25, 2026
10839de
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 25, 2026
acbc34b
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 27, 2026
c6fbc6c
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 27, 2026
31755f2
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 27, 2026
4e225f5
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 28, 2026
5a5eedb
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 28, 2026
1adad39
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 28, 2026
af72fa5
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
828aa94
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
17c2e9e
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
eff85a4
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
7675c6b
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
15043e0
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
6bc6644
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
81d8bbb
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
a34c0b9
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
24fbed6
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
dc2830d
Merge branch 'feat/migrate-mjml-beefree' into feat/guides
ivoIturrieta Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ jobs:
pnpm --filter @unlayer/content-workflows-example typecheck
pnpm --filter @unlayer/content-workflows-example test

- name: i18n example (react-intl, react-i18next, and next-intl in English and Arabic)
run: |
pnpm --filter @unlayer/i18n-example typecheck
pnpm --filter @unlayer/i18n-example test

# ── Browser import/runtime safety gate ──────────────────────
# Bundles the shipped package entry as a browser consumer would, then
# imports and renders inside Chromium without a Node `process` global.
Expand Down
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,14 @@ import { Row, Column, ColumnLayouts } from '@unlayer/react-elements';

For the full API reference, component props, design patterns, and common mistakes, see the **[React package documentation](./packages/react/README.md)**.

Using a coding agent? Install the official **[`unlayer-elements` skill](https://github.com/unlayer/unlayer-skills)** (`npx skills add unlayer/unlayer-skills`): the structure, layouts, renderers, preview commands, and common mistakes.

## Packages

| Package | Description | Published |
|---------|-------------|-----------|
| [`@unlayer/react-elements`](./packages/react) | React components and renderers | [![npm](https://img.shields.io/npm/v/@unlayer/react-elements.svg)](https://www.npmjs.com/package/@unlayer/react-elements) |
| [`@unlayer/migrate`](./packages/migrate) | Migrate React Email templates to Elements, checked against the originals | Not yet |
| [`@unlayer/migrate`](./packages/migrate) | Migrate React Email, MJML and Beefree templates to Elements, checked against the originals | [![npm](https://img.shields.io/npm/v/@unlayer/migrate.svg)](https://www.npmjs.com/package/@unlayer/migrate) |
| [`@unlayer/from-react-email`](./packages/from-react-email) | The React Email → Elements converter, as a library | Private (bundled into `@unlayer/migrate`, API at `@unlayer/migrate/react-email`) |
| [`@unlayer/convert-core`](./packages/convert-core) | Shared model for converters (Elements tree, report, TSX printer, layout check) | Internal |
| [`@unlayer-internal/shared-elements`](./packages/shared) | Framework-agnostic shared logic | Internal |
Expand All @@ -278,7 +280,8 @@ Runnable projects you can clone and experiment with:

| Example | Description |
|---------|-------------|
| [`examples/content-workflows`](./examples/content-workflows) | One React invoice → email + web + real PDF, five email providers, and editable design JSON |
| [`examples/content-workflows`](./examples/content-workflows) | One React invoice → email + web + real PDF, fourteen email providers, and editable design JSON |
| [`examples/i18n`](./examples/i18n) | One email in English and Arabic with react-intl, react-i18next, and next-intl, tested for translated text, `lang`, and `dir` |
| [`examples/nextjs-app-router`](./examples/nextjs-app-router) | Next.js 15 App Router — renders an email with `renderToHtml()` in a Server Component, previews it at `/email-preview`, and serves the raw HTML from a Route Handler |

## Development
Expand Down Expand Up @@ -342,8 +345,10 @@ Yes. `registerElementsTool()` turns a custom tool definition into a first-class
Start with [when to use Elements](./packages/react/docs/README.md), or go directly to a runnable workflow:

- [Build transactional emails in React](./packages/react/docs/transactional-email.md) — HTML and plain-text receipts, order confirmations, and password reset patterns.
- [Send with Resend, AWS SES, Postmark, SendGrid, or Nodemailer](./packages/react/docs/email-providers.md) — provider setup and complete server-side integrations.
- [Send with Resend, AWS SES, Postmark, SendGrid, Nodemailer, Mailgun, Brevo, and more](./packages/react/docs/email-providers.md) — provider setup and complete server-side integrations for fourteen providers.
- [One React invoice to email, web, and PDF](./packages/react/docs/one-template-email-web-pdf.md) — share content across all three output modes.
- [Translate emails with react-intl, react-i18next, or next-intl](./packages/react/docs/i18n.md) — load messages before rendering, and set language, direction, and alignment for right-to-left locales.
- [Use Elements in a monorepo](./packages/react/docs/monorepo.md) — a shared emails package with npm, pnpm, Yarn, or Bun, and one copy of React.
- [Generate a real PDF with Playwright](./packages/react/docs/react-to-pdf.md) — print document HTML with Chromium.
- [Make code and AI-generated templates visually editable](./packages/react/docs/visual-editing.md) — `renderToJson()` → Unlayer `loadDesign()` → edited JSON and HTML.

Expand Down
6 changes: 3 additions & 3 deletions examples/content-workflows/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# React transactional email, web, PDF, and visual editing example

One invoice template demonstrates `@unlayer/react-elements` rendering and delivery. Use this example for receipts, order confirmations, invoices, or AI-generated templates that need visual editing. All commands below run from the repository root with Node.js 20+ and pnpm 9.
One invoice template demonstrates `@unlayer/react-elements` rendering and delivery. Use this example for receipts, order confirmations, invoices, or AI-generated templates that need visual editing. All commands below run from the repository root with Node.js 20+ and pnpm 9 (Azure's SDK declares Node.js 22 or newer: use 22+ to send with Azure).

## Install and run

Expand Down Expand Up @@ -41,12 +41,12 @@ The [provider guide](../../packages/react/docs/email-providers.md) lists install
- `src/message.ts`: HTML and plain-text renderer plus environment handling.
- `src/render-email.ts` / `src/render-all.ts`: output files.
- `src/pdf.ts`: Playwright PDF generation and browser cleanup.
- `src/providers/`: Resend, SES, Postmark, SendGrid, and SMTP delivery.
- `src/providers/`: Resend, SES, Postmark, SendGrid, SMTP, Mailgun, Azure Communication Services, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend delivery.
- `editor.html`: wait for editor readiness, load design, export edits.

```bash
pnpm --filter @unlayer/content-workflows-example typecheck
pnpm --filter @unlayer/content-workflows-example test
```

Tests check invoice content in Chromium across all three modes, plain text, editable JSON, escaped customer input, URL validation, and real PDF bytes. They do not send email or depend on the hosted editor. Review inbox compatibility and PDF pagination with your real content before deployment. See the [task guides](../../packages/react/docs/README.md) for constraints and adaptation.
Tests check invoice content in Chromium across all three modes, plain text, editable JSON, escaped customer input, URL validation, and real PDF bytes. They also run the Mailgun, Azure, MailerSend, Scaleway, Plunk, Lettermint, Brevo, Inbound, and eusend scripts with [nock](https://github.com/nock/nock) answering their HTTP requests, and check what each one sends. They do not send email or depend on the hosted editor. Review inbox compatibility and PDF pagination with your real content before deployment. See the [task guides](../../packages/react/docs/README.md) for constraints and adaptation.
25 changes: 22 additions & 3 deletions examples/content-workflows/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@
"send:ses": "tsx src/providers/ses.ts",
"send:postmark": "tsx src/providers/postmark.ts",
"send:sendgrid": "tsx src/providers/sendgrid.ts",
"send:nodemailer": "tsx src/providers/nodemailer.ts"
"send:nodemailer": "tsx src/providers/nodemailer.ts",
"send:mailgun": "tsx src/providers/mailgun.ts",
"send:azure": "tsx src/providers/azure.ts",
"send:mailersend": "tsx src/providers/mailersend.ts",
"send:scaleway": "tsx src/providers/scaleway.ts",
"send:plunk": "tsx src/providers/plunk.ts",
"send:lettermint": "tsx src/providers/lettermint.ts",
"send:brevo": "tsx src/providers/brevo.ts",
"send:inbound": "tsx src/providers/inbound.ts",
"send:eusend": "tsx src/providers/eusend.ts"
},
"dependencies": {
"@unlayer/react-elements": "workspace:*",
Expand All @@ -24,7 +33,16 @@
"@aws-sdk/client-sesv2": "^3.0.0",
"postmark": "^4.0.0",
"@sendgrid/mail": "^8.1.0",
"nodemailer": "^7.0.0"
"nodemailer": "^7.0.0",
"mailgun.js": "14.1.0",
"@azure/communication-email": "1.1.0",
"mailersend": "3.4.1",
"@scaleway/sdk-client": "2.14.0",
"@scaleway/sdk-tem": "2.16.1",
"lettermint": "3.1.0",
"@getbrevo/brevo": "6.1.0",
"inboundemail": "0.20.0",
"@eusend_dev/sdk": "0.19.0"
},
"devDependencies": {
"@types/node": "^24.9.1",
Expand All @@ -33,6 +51,7 @@
"@types/nodemailer": "^7.0.0",
"tsx": "^4.20.5",
"typescript": "^5.6.3",
"http-server": "14.1.1"
"http-server": "14.1.1",
"nock": "14.0.17"
}
}
16 changes: 16 additions & 0 deletions examples/content-workflows/src/providers/azure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { EmailClient } from "@azure/communication-email";
import { deliveryMessage, requireEnv } from "../message";

const client = new EmailClient(requireEnv("AZURE_COMMUNICATION_CONNECTION_STRING"));
const { from, to, subject, html, text } = deliveryMessage();
// beginSend queues the message and returns a poller for the send operation.
const poller = await client.beginSend({
senderAddress: from,
recipients: { to: [{ address: to }] },
content: { subject, html, plainText: text },
});
const result = await poller.pollUntilDone();
if (result.status !== "Succeeded") {
throw new Error(`Azure Communication Services: ${result.error?.message ?? result.status}`);
}
console.info("Azure accepted:", result.id);
13 changes: 13 additions & 0 deletions examples/content-workflows/src/providers/brevo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { BrevoClient } from "@getbrevo/brevo";
import { deliveryMessage, requireEnv } from "../message";

const client = new BrevoClient({ apiKey: requireEnv("BREVO_API_KEY") });
const { from, to, subject, html, text } = deliveryMessage();
const result = await client.transactionalEmails.sendTransacEmail({
sender: { email: from },
to: [{ email: to }],
subject,
htmlContent: html,
textContent: text,
});
console.info("Brevo accepted:", result.messageId);
7 changes: 7 additions & 0 deletions examples/content-workflows/src/providers/eusend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Eusend } from "@eusend_dev/sdk";
import { deliveryMessage, requireEnv } from "../message";

const client = new Eusend(requireEnv("EUSEND_API_KEY"));
const { data, error } = await client.emails.send(deliveryMessage());
if (error) throw new Error(`eusend: ${error.name} ${error.message}`);
console.info("eusend accepted:", data.id);
7 changes: 7 additions & 0 deletions examples/content-workflows/src/providers/inbound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import Inbound from "inboundemail";
import { deliveryMessage, requireEnv } from "../message";

const client = new Inbound({ apiKey: requireEnv("INBOUND_API_KEY") });
const { from, to, subject, html, text } = deliveryMessage();
const result = await client.emails.send({ from, to, subject, html, text });
console.info("Inbound accepted:", result.id);
7 changes: 7 additions & 0 deletions examples/content-workflows/src/providers/lettermint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Lettermint } from "lettermint";
import { deliveryMessage, requireEnv } from "../message";

const client = new Lettermint({ sendingToken: requireEnv("LETTERMINT_PROJECT_TOKEN") });
const { from, to, subject, html, text } = deliveryMessage();
const result = await client.emails.send({ from, to: [to], subject, html, text });
console.info("Lettermint accepted:", result.message_id);
13 changes: 13 additions & 0 deletions examples/content-workflows/src/providers/mailersend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EmailParams, MailerSend, Recipient, Sender } from "mailersend";
import { deliveryMessage, requireEnv } from "../message";

const client = new MailerSend({ apiKey: requireEnv("MAILERSEND_API_KEY") });
const { from, to, subject, html, text } = deliveryMessage();
const params = new EmailParams()
.setFrom(new Sender(from))
.setTo([new Recipient(to)])
.setSubject(subject)
.setHtml(html)
.setText(text);
const response = await client.email.send(params);
console.info("MailerSend accepted:", response.statusCode, response.headers["x-message-id"]);
19 changes: 19 additions & 0 deletions examples/content-workflows/src/providers/mailgun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Mailgun from "mailgun.js";
import { deliveryMessage, requireEnv } from "../message";

// mailgun.js builds a multipart request with the FormData that Node.js 18+ provides.
const client = new Mailgun(FormData).client({
username: "api",
key: requireEnv("MAILGUN_API_KEY"),
// Domains in Mailgun's EU region use https://api.eu.mailgun.net.
url: process.env.MAILGUN_URL ?? "https://api.mailgun.net",
});
const { from, to, subject, html, text } = deliveryMessage();
const result = await client.messages.create(requireEnv("MAILGUN_DOMAIN"), {
from,
to: [to],
subject,
html,
text,
});
console.info("Mailgun accepted:", result.id);
20 changes: 20 additions & 0 deletions examples/content-workflows/src/providers/plunk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { deliveryMessage, requireEnv } from "../message";

// The Plunk npm package is deprecated, so this calls Plunk's REST API directly.
// Plunk derives the plain-text part from `body`; the API has no text field.
type PlunkResponse =
| { success: true; data: { emails: { email: string }[] } }
| { success: false; error: { code: string; message: string } };

const { from, to, subject, html } = deliveryMessage();
const response = await fetch("https://next-api.useplunk.com/v1/send", {
method: "POST",
headers: {
Authorization: `Bearer ${requireEnv("PLUNK_SECRET_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ from, to, subject, body: html }),
});
const result = (await response.json()) as PlunkResponse;
if (!result.success) throw new Error(`Plunk ${response.status}: ${result.error.message}`);
console.info("Plunk accepted:", result.data.emails[0].email);
21 changes: 21 additions & 0 deletions examples/content-workflows/src/providers/scaleway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { createClient } from "@scaleway/sdk-client";
import { Temv1alpha1 } from "@scaleway/sdk-tem";
import { deliveryMessage, requireEnv } from "../message";

const client = createClient({
accessKey: requireEnv("SCW_ACCESS_KEY"),
secretKey: requireEnv("SCW_SECRET_KEY"),
defaultProjectId: requireEnv("SCW_DEFAULT_PROJECT_ID"),
// Transactional Email runs in the fr-par region.
defaultRegion: "fr-par",
});
const tem = new Temv1alpha1.API(client);
const { from, to, subject, html, text } = deliveryMessage();
const { emails } = await tem.createEmail({
from: { email: from },
to: [{ email: to }],
subject,
html,
text,
});
console.info("Scaleway accepted:", emails.map((email) => email.id).join(", "));
Loading