best-email-builder 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +133 -0
- package/README.md +214 -19
- package/dist/builder.js +4 -4
- package/dist/{chunk-ZSUHOCCE.js → chunk-AT73Y7JA.js} +159 -83
- package/dist/chunk-AT73Y7JA.js.map +1 -0
- package/dist/chunk-HHLOE6OZ.js +1682 -0
- package/dist/chunk-HHLOE6OZ.js.map +1 -0
- package/dist/chunk-JNHFZXDL.js +1776 -0
- package/dist/chunk-JNHFZXDL.js.map +1 -0
- package/dist/chunk-MY6WPDT4.js +6110 -0
- package/dist/chunk-MY6WPDT4.js.map +1 -0
- package/dist/{chunk-JOC752I3.js → chunk-NAEGU6PR.js} +13 -4
- package/dist/{chunk-JOC752I3.js.map → chunk-NAEGU6PR.js.map} +1 -1
- package/dist/{chunk-BVOVAEEJ.js → chunk-TGDZ4NFI.js} +44 -3
- package/dist/chunk-TGDZ4NFI.js.map +1 -0
- package/dist/index.js +45 -8
- package/dist/index.js.map +1 -1
- package/dist/library.js +3 -3
- package/dist/server.js +544 -204
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/blocks/shared.d.ts +20 -0
- package/dist/types/core/EmailEditor.d.ts +72 -19
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/mjmlDefaults.d.ts +13 -0
- package/dist/types/core/scrollbars.d.ts +1 -1
- package/dist/types/defaultEditor.d.ts +4 -1
- package/dist/types/engine/NativeAdapter.d.ts +28 -0
- package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
- package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
- package/dist/types/engine/canvas/icons.d.ts +14 -0
- package/dist/types/engine/canvas/morph.d.ts +21 -0
- package/dist/types/engine/canvasMjml.d.ts +30 -0
- package/dist/types/engine/mjml.d.ts +31 -0
- package/dist/types/engine/model.d.ts +130 -0
- package/dist/types/engine/placement.d.ts +69 -0
- package/dist/types/engine/richText.d.ts +15 -0
- package/dist/types/engine/unwrapModule.d.ts +29 -0
- package/dist/types/features/brand/BrandButton.d.ts +4 -0
- package/dist/types/features/brand/brand.d.ts +108 -0
- package/dist/types/features/brand/extract.d.ts +61 -0
- package/dist/types/features/brand/index.d.ts +2 -0
- package/dist/types/features/brand/messages.d.ts +13 -0
- package/dist/types/features/brand/useBrand.d.ts +22 -0
- package/dist/types/features/generate/GenerateButton.d.ts +3 -0
- package/dist/types/features/generate/RewriteText.d.ts +5 -0
- package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
- package/dist/types/features/generate/chip.d.ts +1 -0
- package/dist/types/features/generate/claude.d.ts +21 -0
- package/dist/types/features/generate/draft.d.ts +122 -0
- package/dist/types/features/generate/generator.d.ts +118 -0
- package/dist/types/features/generate/index.d.ts +2 -0
- package/dist/types/features/generate/prompt.d.ts +76 -0
- package/dist/types/features/generate/purposes.d.ts +48 -0
- package/dist/types/features/generate/rewrite.d.ts +83 -0
- package/dist/types/features/generate/subjects.d.ts +83 -0
- package/dist/types/features/preview/PreviewButton.d.ts +1 -1
- package/dist/types/features/preview/darkMode.d.ts +48 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/server/brand.d.ts +32 -0
- package/dist/types/server/brandImport.d.ts +93 -0
- package/dist/types/server/compileMjml.d.ts +2 -0
- package/dist/types/server/handler.d.ts +25 -0
- package/dist/types/server/index.d.ts +11 -0
- package/dist/types/storage/client.d.ts +75 -3
- package/dist/types/ui/fields.d.ts +2 -1
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/primitives/color-picker.d.ts +2 -1
- package/dist/types/ui/useLayoutWidth.d.ts +1 -1
- package/package.json +13 -7
- package/dist/chunk-3M36VVXS.js +0 -665
- package/dist/chunk-3M36VVXS.js.map +0 -1
- package/dist/chunk-57CTQNTK.js +0 -56
- package/dist/chunk-57CTQNTK.js.map +0 -1
- package/dist/chunk-BVOVAEEJ.js.map +0 -1
- package/dist/chunk-ILRAQTHS.js +0 -3677
- package/dist/chunk-ILRAQTHS.js.map +0 -1
- package/dist/chunk-ZSUHOCCE.js.map +0 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,139 @@ All notable changes are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.4.0] — 2026-10-15
|
|
10
|
+
|
|
11
|
+
### Changed — the builder's own engine (breaking)
|
|
12
|
+
- **GrapesJS is gone.** The interface, the canvas, the drag and drop and the
|
|
13
|
+
text editing are the builder's own (`src/engine/`); the email is a tree of
|
|
14
|
+
MJML elements, and MJML alone turns it into HTML. The canvas shows the email
|
|
15
|
+
compiled by `mjml-browser` 5.4.1 (the same MJML version as the server): what
|
|
16
|
+
is seen is what is sent. The package lost GrapesJS and grapesjs-mjml from its
|
|
17
|
+
dependencies, and its stylesheet their CSS.
|
|
18
|
+
- Stored documents are written by the engine `beb-mjml` (project: the MJML
|
|
19
|
+
tree). **Templates saved before** (engine `grapesjs-mjml`) open from their
|
|
20
|
+
stored MJML, versions included, and are saved in the new format next time;
|
|
21
|
+
a local draft of the old format is dropped.
|
|
22
|
+
- `editor.exportHtml()` returns a Promise (MJML 5 compiles asynchronously).
|
|
23
|
+
- `createGrapesJsAdapter` is replaced by `createNativeAdapter`; `parseMjml`,
|
|
24
|
+
`parseMjmlFragment` and `serializeMjml` are exported.
|
|
25
|
+
- `createEmailEditor({ …, t })` takes the interface's translator: the texts a
|
|
26
|
+
block brings and the names in the canvas follow the language. A block's
|
|
27
|
+
`mjml` / `sectionMjml` may be a function of `t`.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Generate an email with AI.** A *Generate* button opens a dialog with 68
|
|
31
|
+
purposes as badges in 8 groups (sales, holidays, events, news, account,
|
|
32
|
+
orders, relationship, organization), a search field and an optional line of
|
|
33
|
+
details. The email is written in the interface's language with the
|
|
34
|
+
builder's sender and variables, cleaned for the builder (longhand padding,
|
|
35
|
+
sections and columns only, no stock photos, unknown variables as visible
|
|
36
|
+
placeholders), validated like a saved template, and opens in the editor with
|
|
37
|
+
its subject and preview text. Server: `createEmailGenerator({ model })`,
|
|
38
|
+
`claudeEmailModel({ client })` (Claude Opus 5.5, structured output,
|
|
39
|
+
streamed), the `generator` option of the handler (`POST /generate`, 30 per
|
|
40
|
+
user per hour); client: `createHttpStorage({ generate: true })`. Any other
|
|
41
|
+
provider plugs in as a function. The live demo generates with the visitor's
|
|
42
|
+
own Anthropic key.
|
|
43
|
+
- **Rewrite with AI.** Selecting a text or a button shows, in its properties,
|
|
44
|
+
*Shorter*, *Longer*, *Fix mistakes*, *Friendlier*, *More formal*,
|
|
45
|
+
*Translate* (13 languages) and a field to say what to change. The text is
|
|
46
|
+
replaced in place, in one step of the history (Ctrl+Z). Links and variables
|
|
47
|
+
must survive and the HTML must pass the validator, or the model gets one more
|
|
48
|
+
turn and then nothing changes; a button label stays plain text. Server:
|
|
49
|
+
`generator.rewrite` (`createEmailGenerator` returns it), `POST /rewrite`
|
|
50
|
+
(200 per user per hour, `rateLimits.rewritePerHour`), `claudeEmailModel`'s
|
|
51
|
+
`rewriteEffort` (`'low'`); client: `storage.rewrite`, given by
|
|
52
|
+
`createHttpStorage({ generate: true })`. `editor.getElement(id).html` returns
|
|
53
|
+
the stored HTML of a text block.
|
|
54
|
+
- **Your brand.** A *Brand* button opens one dialog: the company's website
|
|
55
|
+
first (*Get my brand* reads its logo, main color, headings color, font and
|
|
56
|
+
name), then every field by hand, with a live preview. Saving keeps the
|
|
57
|
+
brand for the workspace and repaints the email being edited in one step
|
|
58
|
+
(Ctrl+Z); new emails, gallery templates, inserted blocks and generated
|
|
59
|
+
emails start in it. Emails are repainted by role (filled buttons and
|
|
60
|
+
colored banners, bold headings, colored links, fonts; gray footers and body
|
|
61
|
+
text kept), and the text on a colored background switches to what reads.
|
|
62
|
+
Server: `brandStore` (`createMemoryBrandRepository`), `brandImporter`
|
|
63
|
+
(`createBrandImporter`: public http(s) addresses on ports 80/443 only,
|
|
64
|
+
private, link-local, metadata and IPv4-in-IPv6 addresses refused at the
|
|
65
|
+
DNS lookup and on every redirect, size and time limits), routes
|
|
66
|
+
`GET/PUT /brand` and `POST /brand/import` (20 per user per hour,
|
|
67
|
+
`rateLimits.brandImportsPerHour`); client: `createHttpStorage({ brand: true })`,
|
|
68
|
+
`storage.loadBrand / saveBrand / importBrand`. `applyBrand`, `cleanBrand`,
|
|
69
|
+
`brandedSender`, `isPublicAddress`, `editor.setBrand` and `editor.setMjml`
|
|
70
|
+
(replace the email in one undoable step) are exported. The live demo keeps
|
|
71
|
+
the brand in `localStorage`.
|
|
72
|
+
- **Suggest subjects.** Under the subject, one click gives three other
|
|
73
|
+
subject lines drawn from the words of the email, each from its own angle.
|
|
74
|
+
They are a list to pick from, the user's own subject first to come back to;
|
|
75
|
+
*More ideas* asks for three new ones. Plain lines, distinct, in the email's
|
|
76
|
+
language, merge tags only from the catalog (one repair turn, fewer than
|
|
77
|
+
three still shown). Server: `generator.suggestSubjects`, `POST /subjects`
|
|
78
|
+
(100 per user per hour, `rateLimits.subjectsPerHour`); client:
|
|
79
|
+
`storage.suggestSubjects`, given by `createHttpStorage({ generate: true })`.
|
|
80
|
+
`emailWords` and `SUBJECTS_SYSTEM_PROMPT` are exported.
|
|
81
|
+
- **Dark mode in the preview.** A sun / moon switch next to Desktop / Mobile
|
|
82
|
+
shows the email as the apps that repaint it in dark mode do (Gmail and
|
|
83
|
+
Outlook apps, Outlook.com): light backgrounds and borders turn dark of the
|
|
84
|
+
same hue, dark text turns light, brand colors, light text and images stay —
|
|
85
|
+
a dark logo on a transparent background disappears there as it does in
|
|
86
|
+
those apps. Simulated on the compiled HTML, without script: the frame stays
|
|
87
|
+
sandboxed. In the editor's preview and in the template library.
|
|
88
|
+
`simulateDarkMode(html)` is exported.
|
|
89
|
+
- Drag and drop with pointer events: a line shows where the block lands
|
|
90
|
+
(between two elements of a column, or between two sections — a content block
|
|
91
|
+
then gets a section of its own), the canvas scrolls near its edges, Esc
|
|
92
|
+
cancels. Sections and elements move by the ✥ handle of their toolbar.
|
|
93
|
+
- On the selection: its name, and a toolbar — select the parent, move,
|
|
94
|
+
duplicate, delete. Keyboard: Delete, Ctrl+Z / Ctrl+Y, Ctrl+C / Ctrl+V,
|
|
95
|
+
Ctrl+D, Escape (parent), Enter (edit the text).
|
|
96
|
+
- Ctrl+A selects every block of the email (each section outlined, "All
|
|
97
|
+
blocks" in the panel); Delete — or the panel's delete button — removes them
|
|
98
|
+
in one step, Ctrl+Z brings them back. Inside a text being edited or a field,
|
|
99
|
+
Ctrl+A keeps selecting the text. `editor.selectAll()` does the same.
|
|
100
|
+
- The toolbar's first button names where it goes ("⤴ Column", "⤴ 2 columns"):
|
|
101
|
+
it selects the parent element; a bare up arrow read as "move up".
|
|
102
|
+
- Panel features for well-made emails: **column widths** (1/4 to 3/4; the
|
|
103
|
+
other columns of the section share the rest, "Equal" resets), a section
|
|
104
|
+
**background across the whole screen**, an image **full width on mobile**,
|
|
105
|
+
menu links **as typed or in capitals** (MJML capitalised them; new menus
|
|
106
|
+
are written as typed).
|
|
107
|
+
- The server's HTML gets `x-apple-disable-message-reformatting` and
|
|
108
|
+
`format-detection` (Apple Mail does not rescale the email, iOS does not turn
|
|
109
|
+
dates, addresses and phone numbers into blue links).
|
|
110
|
+
- Email-client checks on the compiled HTML of every template and of an email
|
|
111
|
+
holding every block (tests/unit/email-clients.test.mjs): Outlook's MSO
|
|
112
|
+
tables and VML backgrounds, Gmail's 102 KB clipping and dropped CSS, images
|
|
113
|
+
(alt, width, no border), mobile media queries and the fallback without
|
|
114
|
+
`<style>`, links, and the same body from the canvas compiler and the
|
|
115
|
+
server's. And rendered in a browser at 640 px, 375 px and without `<style>`
|
|
116
|
+
(e2e/rendering.stories.spec.js).
|
|
117
|
+
- The template library is called a library everywhere ("Bibliothèque de
|
|
118
|
+
modèles e-mail" in French, instead of "Banque").
|
|
119
|
+
- Text edited in place (double-click, or a click on a selected text) with a
|
|
120
|
+
formatting bar: bold, italic, underline, strike, link, unlink, clear. Pasted
|
|
121
|
+
content arrives as plain text, Enter is a line break, the HTML is reduced to
|
|
122
|
+
what an email accepts.
|
|
123
|
+
- The panel shows MJML's own default in an empty field (a section's padding:
|
|
124
|
+
20 px above and below) and keeps it for the sides not edited.
|
|
125
|
+
- New layouts start with empty columns showing "Drop a block here" (canvas
|
|
126
|
+
only); an image without file shows a frame sized to its column.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
- Dropping an image opened an empty "Select an image" dialog of the old engine.
|
|
130
|
+
- A 2-column layout was named "Section" in the canvas and the panel; it is now
|
|
131
|
+
"2 columns", and every element carries its block's name, in both languages.
|
|
132
|
+
- Blocks inserted English texts ("Your text.", "Column 1", "Learn more"…) in a
|
|
133
|
+
French interface; their texts now follow the interface's language.
|
|
134
|
+
- An empty padding showed 0 while MJML renders 20 px (sections) or 10 px
|
|
135
|
+
(texts, top and bottom); editing one side set the others to 0.
|
|
136
|
+
- The button's "Label" field was not translated.
|
|
137
|
+
- The server-side project check now also reads the new project's `tag` field
|
|
138
|
+
(an `mj-raw` or `script` element in a tampered project is refused).
|
|
139
|
+
|
|
7
140
|
## [0.3.0] — 2026-10-13
|
|
8
141
|
|
|
9
142
|
### Added
|
package/README.md
CHANGED
|
@@ -21,8 +21,9 @@ canvas you have to wire to MJML, storage, validation and sending yourself.
|
|
|
21
21
|
Best Email Builder is the second option done once, properly, and given away:
|
|
22
22
|
|
|
23
23
|
- **Free and MIT-licensed** — use it in commercial products, fork it, ship it.
|
|
24
|
-
- **No account, no
|
|
25
|
-
|
|
24
|
+
- **No account, no vendor.** It is a React component and a server handler
|
|
25
|
+
you run in your own app, against your own database. The AI generation is
|
|
26
|
+
optional and runs on your server, with your own key.
|
|
26
27
|
- **Readable from end to end.** The engine is hidden behind one adapter, every
|
|
27
28
|
block and feature is a registry entry, and the rules a template must obey
|
|
28
29
|
live in one validation folder — so it is easy to change what you need.
|
|
@@ -31,23 +32,52 @@ Best Email Builder is the second option done once, properly, and given away:
|
|
|
31
32
|
oddly are all welcome — see [Contributing](#contributing).
|
|
32
33
|
|
|
33
34
|
- **MJML under the hood** — every block compiles to battle-tested, table-based
|
|
34
|
-
email HTML. You never write a `<table>` by hand.
|
|
35
|
+
email HTML. You never write a `<table>` by hand. Every template is checked
|
|
36
|
+
against what Outlook (MSO tables, VML backgrounds), Gmail (102 KB clipping,
|
|
37
|
+
dropped CSS) and Apple Mail need, and rendered at desktop and phone widths
|
|
38
|
+
in the test suite.
|
|
35
39
|
- **The blocks emails are made of**: heading, text, image, button, divider,
|
|
36
40
|
spacer, social links, menu, 1 to 4 columns, image + text, and a hero banner
|
|
37
41
|
with a background image — sections take a background image too.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
Columns take a share of the row (1/4 to 3/4), a section's background can
|
|
43
|
+
span the whole screen, an image can take the full width on phones.
|
|
44
|
+
- **Its own editor, end to end.** The interface, the canvas and the drag and
|
|
45
|
+
drop are the builder's own; the email is a tree of MJML elements, and MJML
|
|
46
|
+
alone turns it into HTML. The canvas shows the real compiled email — what you
|
|
47
|
+
see is what Outlook, Gmail and Apple Mail receive. Templates are stored by
|
|
48
|
+
*your* backend, as a versioned JSON project (the source of truth) plus
|
|
49
|
+
server-compiled HTML.
|
|
50
|
+
- **Editing that behaves**: drag a block, a line shows exactly where it will
|
|
51
|
+
land; double-click a text to type in place, with bold, italic, underline,
|
|
52
|
+
links; every element has its name, its toolbar (parent, move, duplicate,
|
|
53
|
+
delete) and its keyboard shortcuts; an empty field of the panel shows the
|
|
54
|
+
value MJML will really use.
|
|
42
55
|
- **Server-side rendering only.** The browser never sends HTML: the server
|
|
43
56
|
validates the project and the MJML, compiles, and stores its own HTML.
|
|
57
|
+
- **An email written from scratch by AI** — the user picks what it is for
|
|
58
|
+
among 68 purposes (sale, Christmas, password reset, meeting reminder, order
|
|
59
|
+
shipped…), adds a line of details if they want, and a complete email opens in
|
|
60
|
+
the editor, subject and preview text included, ready to edit and save. The
|
|
61
|
+
model's MJML is cleaned and checked like any template before it gets there.
|
|
62
|
+
Claude out of the box; any other model in a few lines.
|
|
63
|
+
- **Any text rewritten in one click** — select a text or a button: *Shorter*,
|
|
64
|
+
*Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate* into 13
|
|
65
|
+
languages, or say what to change in your own words. The text changes in
|
|
66
|
+
place, links and variables kept; Ctrl+Z brings the previous one back.
|
|
67
|
+
- **Three subject lines on demand** — drawn from the words of the email, each
|
|
68
|
+
from its own angle; a click puts one in the subject, your own stays in the
|
|
69
|
+
list to come back to.
|
|
70
|
+
- **Your brand in every email** — give your website: the logo, colors and
|
|
71
|
+
font are read from it (or set by hand), and new emails, templates and
|
|
72
|
+
blocks start in them.
|
|
44
73
|
- **A gallery of ready-made emails** — newsletter, welcome, promotion, event
|
|
45
74
|
invitation… each one with the sender's footer and the unsubscribe link, so
|
|
46
75
|
nobody starts from a blank page or forgets the legal mentions.
|
|
47
76
|
- **Production features**: undo/redo, desktop/mobile canvas, autosave and local
|
|
48
77
|
drafts, versions, conflict detection between concurrent editors, image
|
|
49
78
|
uploads, server preview with quality checks (missing alt text, missing
|
|
50
|
-
unsubscribe link, unknown variables, Gmail clipping…)
|
|
79
|
+
unsubscribe link, unknown variables, Gmail clipping…) and a dark-mode view
|
|
80
|
+
(as the Gmail and Outlook apps repaint emails), test sends.
|
|
51
81
|
- **Merge-tag variables your users create themselves** — searchable menu,
|
|
52
82
|
insertion at the caret, in the subject line too, sample values for the
|
|
53
83
|
preview and fallbacks for recipients without a value.
|
|
@@ -56,9 +86,10 @@ Best Email Builder is the second option done once, properly, and given away:
|
|
|
56
86
|
- **English and French** built in, any other language through a dictionary.
|
|
57
87
|
|
|
58
88
|
```
|
|
59
|
-
React UI ─► EmailEditor ─►
|
|
60
|
-
(ui/) (core/) (
|
|
61
|
-
|
|
89
|
+
React UI ─► EmailEditor ─► NativeAdapter ─► the email: an MJML tree
|
|
90
|
+
(ui/) (core/) (engine/) │ canvas = the email compiled
|
|
91
|
+
│ by mjml-browser
|
|
92
|
+
│ project JSON + MJML
|
|
62
93
|
Server: validation ─► MJML compilation ─► variables ─► links ─► final HTML
|
|
63
94
|
```
|
|
64
95
|
|
|
@@ -110,9 +141,9 @@ and each has its own entry point, so one never brings the other's code:
|
|
|
110
141
|
|
|
111
142
|
| Block | Import | What it is |
|
|
112
143
|
|---|---|---|
|
|
113
|
-
| A — the
|
|
114
|
-
| B — the editor | `import { EmailBuilder } from 'best-email-builder/builder'` | The editor (above). Opens whatever the host hands it; knows nothing of the
|
|
115
|
-
| A + B | `import { EmailStudio } from 'best-email-builder'` | The two wired together: the
|
|
144
|
+
| A — the library | `import { TemplateLibrary } from 'best-email-builder/library'` | Your templates and the ready-made bases in a searchable list, the selected one's details, its rendered preview (desktop / mobile, light / dark). *Edit*, *New template* and *Start from this base* only call `onEdit(template)` / `onCreate(start)` — it never mounts an editor, and loads neither the editor's engine nor its MJML compiler. |
|
|
145
|
+
| B — the editor | `import { EmailBuilder } from 'best-email-builder/builder'` | The editor (above). Opens whatever the host hands it; knows nothing of the library. |
|
|
146
|
+
| A + B | `import { EmailStudio } from 'best-email-builder'` | The two wired together: the library, then the editor, then back to the library with the template just saved selected. The editor has no *Templates* button there — the bases are in the library. |
|
|
116
147
|
|
|
117
148
|
`best-email-builder` (the main entry) still exports everything; the two
|
|
118
149
|
sub-entries are for an application that wants one block only. Both use the
|
|
@@ -126,12 +157,12 @@ import { EmailStudio, createHttpStorage } from 'best-email-builder';
|
|
|
126
157
|
<EmailStudio storage={createHttpStorage()} locale="fr" sender={sender} />
|
|
127
158
|
```
|
|
128
159
|
|
|
129
|
-
#### Block B with your own
|
|
160
|
+
#### Block B with your own library
|
|
130
161
|
|
|
131
|
-
Your
|
|
162
|
+
Your library (a table, a sidebar, a modal — anything) only decides what the
|
|
132
163
|
editor opens, and hears back when it saves or closes:
|
|
133
164
|
|
|
134
|
-
| Your
|
|
165
|
+
| Your library wants to… | Pass to `EmailBuilder` |
|
|
135
166
|
|---|---|
|
|
136
167
|
| open a saved template | `templateId={id}` (the editor loads it from `storage`) |
|
|
137
168
|
| start from content it has (a base, an import, a copy) | `initialMjml={mjml}` and, optionally, `initialMeta={{ subject, preheader }}` |
|
|
@@ -150,7 +181,7 @@ import { EmailBuilder, createHttpStorage } from 'best-email-builder/builder';
|
|
|
150
181
|
const storage = createHttpStorage();
|
|
151
182
|
|
|
152
183
|
function Emails() {
|
|
153
|
-
const [open, setOpen] = useState(null); // null = my
|
|
184
|
+
const [open, setOpen] = useState(null); // null = my library
|
|
154
185
|
|
|
155
186
|
if (!open) {
|
|
156
187
|
return (
|
|
@@ -267,6 +298,150 @@ const subject = applyVariablesToText(template.subject, values, variables);
|
|
|
267
298
|
With an email provider that does its own merge, map the tags instead:
|
|
268
299
|
`mapVariables(html, { 'contact.firstname': '{{ contact.FIRSTNAME }}' })`.
|
|
269
300
|
|
|
301
|
+
### 4. Generating emails with AI (optional)
|
|
302
|
+
|
|
303
|
+
The *Generate* button asks one thing: what the email is for (a badge among
|
|
304
|
+
68, grouped: sales, holidays, events, news, account, orders, relationship,
|
|
305
|
+
organization), with an optional line of details. The email is written in the
|
|
306
|
+
interface's language, with the builder's sender and variables, and replaces
|
|
307
|
+
the one being edited — the dialog says so before, not after.
|
|
308
|
+
|
|
309
|
+
The model runs on the server, where its key is:
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
import Anthropic from '@anthropic-ai/sdk'; // npm install @anthropic-ai/sdk
|
|
313
|
+
import {
|
|
314
|
+
createEmailBuilderHandler,
|
|
315
|
+
createEmailGenerator,
|
|
316
|
+
claudeEmailModel,
|
|
317
|
+
compileMjml,
|
|
318
|
+
} from 'best-email-builder/server';
|
|
319
|
+
|
|
320
|
+
const handler = createEmailBuilderHandler({
|
|
321
|
+
/* … */
|
|
322
|
+
generator: createEmailGenerator({
|
|
323
|
+
model: claudeEmailModel({ client: new Anthropic() }), // ANTHROPIC_API_KEY
|
|
324
|
+
compile: compileMjml, // MJML's own warnings go back to the model
|
|
325
|
+
}),
|
|
326
|
+
rateLimits: { generatePerHour: 30, rewritePerHour: 200, subjectsPerHour: 100 }, // per user (defaults)
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
```jsx
|
|
331
|
+
<EmailBuilder storage={createHttpStorage({ generate: true })} />
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
- `claudeEmailModel({ client, model, effort, maxTokens })` — `claude-opus-5-5`
|
|
335
|
+
by default, `effort: 'medium'`; the answer is structured output (a JSON
|
|
336
|
+
schema: name, subject, preheader, MJML) and is streamed, so a long email does
|
|
337
|
+
not hit a request timeout. A generation takes from 20 seconds to a minute or
|
|
338
|
+
two: give the route that long (`maxDuration` on Vercel, the proxy's timeout).
|
|
339
|
+
- **Another provider** is a function `({ system, messages, schema, signal, task }) =>
|
|
340
|
+
Promise<string>` returning the JSON text — pass it as `model`.
|
|
341
|
+
- What comes out is never trusted as is: shorthand padding is expanded,
|
|
342
|
+
elements the builder does not edit (`mj-hero`, `mj-wrapper`, `mj-head`…)
|
|
343
|
+
are turned into sections and columns, stock photos are removed (the user
|
|
344
|
+
picks real images; the alt text stays), unknown variables become visible
|
|
345
|
+
`[placeholders]`, and the result must pass the same validation as a saved
|
|
346
|
+
template. When a problem remains, the model gets one more turn with the list.
|
|
347
|
+
- The prompt (`SYSTEM_PROMPT`, `buildUserPrompt`) and the purposes
|
|
348
|
+
(`EMAIL_PURPOSES`) are exported, to read or to build on.
|
|
349
|
+
|
|
350
|
+
**Rewrite with AI.** The same generator rewrites one text: with it, selecting
|
|
351
|
+
a text block or a button shows a *Rewrite with AI* section in its properties —
|
|
352
|
+
*Shorter*, *Longer*, *Fix mistakes*, *Friendlier*, *More formal*, *Translate*
|
|
353
|
+
(13 languages, under their own names) and a field to say what to change. The
|
|
354
|
+
answer replaces the text at once, in one step of the history: Ctrl+Z brings
|
|
355
|
+
the previous text back, so there is nothing to accept or compare. Selecting
|
|
356
|
+
another element cancels a rewrite in progress; if the text was edited
|
|
357
|
+
meanwhile, the user's words win and the answer is dropped.
|
|
358
|
+
|
|
359
|
+
- It is `POST {base}/rewrite`, enabled with `createHttpStorage({ generate: true })`
|
|
360
|
+
like generation; a generator without `rewrite` answers 501.
|
|
361
|
+
- Every link (the same `href`, none added) and every variable (the same keys,
|
|
362
|
+
none added; a fallback after `|` may be translated) must survive, and the
|
|
363
|
+
HTML must pass the validator — otherwise the model gets one more turn, then
|
|
364
|
+
nothing is changed. A button label comes back as plain words.
|
|
365
|
+
- `claudeEmailModel` asks a rewrite for `rewriteEffort: 'low'` (a few seconds);
|
|
366
|
+
another provider gets `task: 'rewrite'` in its request to tell the two apart.
|
|
367
|
+
- `REWRITE_ACTIONS`, `REWRITE_LANGUAGES` and `REWRITE_SYSTEM_PROMPT` are
|
|
368
|
+
exported.
|
|
369
|
+
|
|
370
|
+
**Suggest subjects.** Under the subject of the email, *Suggest subjects* asks
|
|
371
|
+
for three other subject lines, drawn from the words of the email, each from
|
|
372
|
+
its own angle (clear, curious, personal or timely). They come as a list to
|
|
373
|
+
pick from, with the user's own subject first: a click puts one in the field,
|
|
374
|
+
another click another one, the first row brings the user's subject back.
|
|
375
|
+
*More ideas* asks for three new ones, *Done* closes the list.
|
|
376
|
+
|
|
377
|
+
- It is `POST {base}/subjects` (`generator.suggestSubjects`), enabled with
|
|
378
|
+
`createHttpStorage({ generate: true })` too; 100 per user per hour.
|
|
379
|
+
- The subjects are plain lines, different from the current subject and from
|
|
380
|
+
each other, in the language of the email; a merge tag is allowed only from
|
|
381
|
+
the server's catalog. Otherwise the model gets one more turn; fewer than
|
|
382
|
+
three good ones are still shown.
|
|
383
|
+
- `claudeEmailModel` uses `rewriteEffort` for them as well (`task: 'subjects'`).
|
|
384
|
+
`emailWords(mjml)` (the words a reader sees) and `SUBJECTS_SYSTEM_PROMPT`
|
|
385
|
+
are exported.
|
|
386
|
+
|
|
387
|
+
The live demo has no server: there, *Generate*, *Rewrite with AI* and
|
|
388
|
+
*Suggest subjects* ask for the visitor's own Anthropic key, keep it in the
|
|
389
|
+
browser and call Anthropic directly.
|
|
390
|
+
|
|
391
|
+
### 5. Your brand (optional)
|
|
392
|
+
|
|
393
|
+
The *Brand* button opens one dialog. Its first field is the company's
|
|
394
|
+
website: *Get my brand* reads its logo, main color, headings color, font and
|
|
395
|
+
name, and fills the fields below. Everything can then be changed by hand
|
|
396
|
+
(upload a logo, pick a color), and a small preview follows every change.
|
|
397
|
+
*Save* keeps the brand for the whole workspace:
|
|
398
|
+
|
|
399
|
+
- the email being edited is repainted (the box *Also apply to this email* is
|
|
400
|
+
ticked by default) — one step of the history, Ctrl+Z brings the previous
|
|
401
|
+
look back;
|
|
402
|
+
- new emails, gallery templates, inserted blocks and AI-generated emails
|
|
403
|
+
start in the brand: the logo (or the name) at the top, the name in the
|
|
404
|
+
footer, buttons and banners in the main color, headings in their color.
|
|
405
|
+
|
|
406
|
+
Emails are repainted by **role**, not by exact color, so any email works —
|
|
407
|
+
a template, a generated one, one built by hand: filled buttons and colored
|
|
408
|
+
banners take the main color, and their text switches to white or dark
|
|
409
|
+
whichever reads; bold headings take the headings color; colored links take
|
|
410
|
+
the main color (or the headings color when the main one is too light to read
|
|
411
|
+
on white); gray footers and body text keep theirs.
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
import {
|
|
415
|
+
createBrandImporter,
|
|
416
|
+
createMemoryBrandRepository,
|
|
417
|
+
} from 'best-email-builder/server';
|
|
418
|
+
|
|
419
|
+
const handler = createEmailBuilderHandler({
|
|
420
|
+
// …
|
|
421
|
+
brandStore: createMemoryBrandRepository(), // one row: { get(), save(brand) }
|
|
422
|
+
brandImporter: createBrandImporter(), // optional: "Get my brand"
|
|
423
|
+
rateLimits: { brandImportsPerHour: 20 }, // per user (default)
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// In the browser:
|
|
427
|
+
const storage = createHttpStorage({ brand: true }); // { brand: { import: false } } without an importer
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
- Routes: `GET {base}/brand`, `PUT {base}/brand` (write permission),
|
|
431
|
+
`POST {base}/brand/import` with `{ url }` (write permission). The import
|
|
432
|
+
saves nothing: the user reviews the result first. A logo found on the site
|
|
433
|
+
is stored like an upload (PNG, JPEG or GIF; SVG and WebP logos are skipped,
|
|
434
|
+
since most email apps do not show them — the dialog says so).
|
|
435
|
+
- Without `brandImporter` the dialog has no website field; without
|
|
436
|
+
`brandStore` there is no *Brand* button.
|
|
437
|
+
- Some websites refuse automated readers (bot protection answers 403): the
|
|
438
|
+
dialog says so, and the brand is filled in by hand.
|
|
439
|
+
- `applyBrand(mjml, brand)` and `cleanBrand(brand)` are exported for your
|
|
440
|
+
own pipeline (e.g. rebranding stored templates).
|
|
441
|
+
|
|
442
|
+
The live demo keeps the brand in `localStorage` and has no website field
|
|
443
|
+
(reading a website needs a server).
|
|
444
|
+
|
|
270
445
|
## Component props
|
|
271
446
|
|
|
272
447
|
| Prop | Default | Description |
|
|
@@ -304,10 +479,16 @@ interface TemplateStorage {
|
|
|
304
479
|
getVersion(id, versionId): Promise<Version & { document; mjml }>;
|
|
305
480
|
uploadImage(file): Promise<{ path }>;
|
|
306
481
|
render?(input): Promise<{ html; warnings; quality }>; // enables Preview
|
|
482
|
+
generate?(input, { signal }): Promise<{ name; subject; preheader; mjml; warnings }>; // enables Generate
|
|
483
|
+
rewrite?(input, { signal }): Promise<{ text }>; // enables Rewrite with AI
|
|
484
|
+
suggestSubjects?(input, { signal }): Promise<{ subjects }>; // enables Suggest subjects
|
|
307
485
|
sendTest?(input): Promise<{ sent; failed; invalid; results }>; // enables Send test
|
|
308
486
|
listVariables?(): Promise<Variable[]>; // enable user-created variables
|
|
309
487
|
saveVariable?(variable, { create }): Promise<Variable>;
|
|
310
488
|
deleteVariable?(key): Promise<void>;
|
|
489
|
+
loadBrand?(): Promise<Brand>; // enable the Brand button
|
|
490
|
+
saveBrand?(brand): Promise<Brand>;
|
|
491
|
+
importBrand?(url, { signal }): Promise<{ brand; notes }>; // enables "Get my brand"
|
|
311
492
|
}
|
|
312
493
|
```
|
|
313
494
|
|
|
@@ -396,6 +577,20 @@ touch the rest of your page, whether or not you use Tailwind yourself.
|
|
|
396
577
|
`nosniff` and a sandboxing CSP.
|
|
397
578
|
- Previews are rendered in an iframe with an empty `sandbox`.
|
|
398
579
|
- Test sends are limited (10 addresses, 20 sends per user per hour by default).
|
|
580
|
+
- Reading a brand from a website is a request your server makes to an address
|
|
581
|
+
a user typed, so it is fenced: http(s) on ports 80 and 443 only, no
|
|
582
|
+
credentials in the address, and every address the name resolves to must be
|
|
583
|
+
public — loopback, private networks, link-local and cloud metadata
|
|
584
|
+
(`169.254.169.254`), carrier-grade NAT, multicast and IPv4 hidden in IPv6
|
|
585
|
+
(mapped, NAT64, 6to4) are refused, at the lookup itself so a DNS answer
|
|
586
|
+
cannot slip through. Redirects (4 at most) are checked the same way. Pages,
|
|
587
|
+
stylesheets and logos have size limits after decompression, the whole
|
|
588
|
+
reading 8 seconds, and imports are limited per user (20 per hour).
|
|
589
|
+
- Generated emails pass the same MJML validation as saved ones; generation is
|
|
590
|
+
limited per user (30 per hour by default) and needs the `write` permission.
|
|
591
|
+
A rewritten text keeps its links and variables (none added) and passes the
|
|
592
|
+
same validation; rewrites are limited too (200 per user per hour), and so
|
|
593
|
+
are subject ideas (100), whose merge tags come from the server's catalog.
|
|
399
594
|
|
|
400
595
|
## Development
|
|
401
596
|
|
|
@@ -434,7 +629,7 @@ channel described in [SECURITY.md](SECURITY.md).
|
|
|
434
629
|
|
|
435
630
|
## License
|
|
436
631
|
|
|
437
|
-
[MIT](LICENSE).
|
|
632
|
+
[MIT](LICENSE). MJML and mjml-browser are MIT.
|
|
438
633
|
|
|
439
634
|
## Author
|
|
440
635
|
|
package/dist/builder.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EmailBuilder,
|
|
3
3
|
EmailBuilderBoundary
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-MY6WPDT4.js";
|
|
5
5
|
import {
|
|
6
6
|
STARTER_MJML,
|
|
7
7
|
StorageError,
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
createHttpStorage,
|
|
11
11
|
createMemoryStorage,
|
|
12
12
|
createStarterMjml
|
|
13
|
-
} from "./chunk-
|
|
14
|
-
import "./chunk-
|
|
13
|
+
} from "./chunk-AT73Y7JA.js";
|
|
14
|
+
import "./chunk-JNHFZXDL.js";
|
|
15
15
|
import {
|
|
16
16
|
CUSTOM_GROUP,
|
|
17
17
|
DEFAULT_VARIABLES,
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
defineVariables,
|
|
22
22
|
mergeVariables,
|
|
23
23
|
translatorFor
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-HHLOE6OZ.js";
|
|
25
25
|
export {
|
|
26
26
|
CUSTOM_GROUP,
|
|
27
27
|
DEFAULT_VARIABLES,
|