Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -186,6 +186,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