best-email-builder 0.0.0-stage → 0.1.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 +19 -0
- package/LICENSE +21 -0
- package/README.md +329 -2
- package/dist/chunk-2PMZMNKA.js +652 -0
- package/dist/chunk-2PMZMNKA.js.map +1 -0
- package/dist/chunk-3EOCXTI2.js +652 -0
- package/dist/chunk-3EOCXTI2.js.map +1 -0
- package/dist/index.js +4265 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +1256 -0
- package/dist/server.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +33 -0
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +162 -0
- package/dist/types/blocks/button/index.d.ts +3 -0
- package/dist/types/blocks/columns/index.d.ts +4 -0
- package/dist/types/blocks/divider/index.d.ts +3 -0
- package/dist/types/blocks/image/index.d.ts +3 -0
- package/dist/types/blocks/index.d.ts +2 -0
- package/dist/types/blocks/shared.d.ts +58 -0
- package/dist/types/blocks/spacer/index.d.ts +3 -0
- package/dist/types/blocks/structure/index.d.ts +8 -0
- package/dist/types/blocks/text/index.d.ts +11 -0
- package/dist/types/core/EmailEditor.d.ts +386 -0
- package/dist/types/core/constants.d.ts +18 -0
- package/dist/types/core/document.d.ts +32 -0
- package/dist/types/core/events.d.ts +30 -0
- package/dist/types/core/mjmlAttributes.d.ts +53 -0
- package/dist/types/core/registry.d.ts +22 -0
- package/dist/types/features/autosave/index.d.ts +19 -0
- package/dist/types/features/autosave/localDraft.d.ts +21 -0
- package/dist/types/features/autosave/scheduler.d.ts +35 -0
- package/dist/types/features/index.d.ts +4 -0
- package/dist/types/features/preview/PreviewButton.d.ts +17 -0
- package/dist/types/features/preview/index.d.ts +2 -0
- package/dist/types/features/templates/starter.d.ts +20 -0
- package/dist/types/features/test-email/TestEmailButton.d.ts +3 -0
- package/dist/types/features/test-email/index.d.ts +2 -0
- package/dist/types/features/variables/VariableDialog.d.ts +31 -0
- package/dist/types/features/variables/VariableInsertButton.d.ts +21 -0
- package/dist/types/features/variables/VariablePicker.d.ts +22 -0
- package/dist/types/features/variables/VariablesMenu.d.ts +18 -0
- package/dist/types/features/variables/catalog.d.ts +96 -0
- package/dist/types/features/variables/index.d.ts +2 -0
- package/dist/types/features/variables/service.d.ts +52 -0
- package/dist/types/features/versions/VersionsMenu.d.ts +3 -0
- package/dist/types/features/versions/index.d.ts +2 -0
- package/dist/types/i18n/fr.d.ts +2 -0
- package/dist/types/i18n/index.d.ts +22 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/renderer/links.d.ts +19 -0
- package/dist/types/renderer/prepareMjml.d.ts +10 -0
- package/dist/types/server/assets.d.ts +84 -0
- package/dist/types/server/compileMjml.d.ts +14 -0
- package/dist/types/server/errors.d.ts +17 -0
- package/dist/types/server/handler.d.ts +39 -0
- package/dist/types/server/index.d.ts +19 -0
- package/dist/types/server/node.d.ts +7 -0
- package/dist/types/server/render.d.ts +80 -0
- package/dist/types/server/sendTest.d.ts +57 -0
- package/dist/types/server/templates.d.ts +240 -0
- package/dist/types/server/variables.d.ts +54 -0
- package/dist/types/storage/client.d.ts +200 -0
- package/dist/types/ui/BlockSidebar.d.ts +4 -0
- package/dist/types/ui/ElementActions.d.ts +13 -0
- package/dist/types/ui/EmailBuilder.d.ts +47 -0
- package/dist/types/ui/EmailBuilderBoundary.d.ts +14 -0
- package/dist/types/ui/EmailCanvas.d.ts +17 -0
- package/dist/types/ui/PropertiesPanel.d.ts +11 -0
- package/dist/types/ui/Toolbar.d.ts +21 -0
- package/dist/types/ui/context.d.ts +33 -0
- package/dist/types/ui/fields.d.ts +13 -0
- package/dist/types/ui/primitives/button.d.ts +12 -0
- package/dist/types/ui/primitives/cn.d.ts +2 -0
- package/dist/types/ui/primitives/color-picker.d.ts +10 -0
- package/dist/types/ui/primitives/fields.d.ts +6 -0
- package/dist/types/ui/primitives/misc.d.ts +55 -0
- package/dist/types/ui/primitives/overlays.d.ts +85 -0
- package/dist/types/ui/useEmailEditorState.d.ts +12 -0
- package/dist/types/validation/images.d.ts +30 -0
- package/dist/types/validation/mjmlValidator.d.ts +33 -0
- package/dist/types/validation/projectValidator.d.ts +30 -0
- package/dist/types/validation/quality.d.ts +20 -0
- package/dist/types/validation/urls.d.ts +14 -0
- package/package.json +107 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] — 2026-10-10
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `<EmailBuilder>` React component: blocks (text, image, button, divider,
|
|
13
|
+
spacer, 1/2/3 columns), drag and drop, element actions (move, duplicate,
|
|
14
|
+
delete), undo/redo, desktop/mobile canvas, properties panel.
|
|
15
|
+
- Save / load with versions, conflict detection, autosave and local drafts.
|
|
16
|
+
- Merge-tag variables, server preview with quality checks, test sends, image uploads.
|
|
17
|
+
- `best-email-builder/server`: Fetch-API handler, template and asset services, memory
|
|
18
|
+
repositories, MJML rendering, personalization helpers, Node bridge.
|
|
19
|
+
- English and French interfaces; `--eb-*` theming; dark theme.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Best Email Builder contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Best Email Builder
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/best-email-builder)
|
|
4
|
+
[](https://www.npmjs.com/package/best-email-builder)
|
|
5
|
+
[](https://github.com/SergeMiro/best-email-builder/actions/workflows/ci.yml)
|
|
6
|
+
[](https://sergemiro.github.io/best-email-builder/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
A drag-and-drop **email builder for React** that produces emails which actually
|
|
10
|
+
render in Outlook, Gmail, Apple Mail and on mobile.
|
|
11
|
+
|
|
12
|
+
**[Live demo](https://sergemiro.github.io/best-email-builder/)** — runs entirely in your
|
|
13
|
+
browser (templates are kept in `localStorage`). ·
|
|
14
|
+
**[npm](https://www.npmjs.com/package/best-email-builder)** — `npm install best-email-builder`
|
|
15
|
+
|
|
16
|
+
## Built for the whole developer community
|
|
17
|
+
|
|
18
|
+
Every product that talks to its users ends up needing an email editor, and the
|
|
19
|
+
choice is usually between a paid SaaS widget you cannot style or own, and a
|
|
20
|
+
canvas you have to wire to MJML, storage, validation and sending yourself.
|
|
21
|
+
Best Email Builder is the second option done once, properly, and given away:
|
|
22
|
+
|
|
23
|
+
- **Free and MIT-licensed** — use it in commercial products, fork it, ship it.
|
|
24
|
+
- **No account, no API key, no vendor.** It is a React component and a server
|
|
25
|
+
handler you run in your own app, against your own database.
|
|
26
|
+
- **Readable from end to end.** The engine is hidden behind one adapter, every
|
|
27
|
+
block and feature is a registry entry, and the rules a template must obey
|
|
28
|
+
live in one validation folder — so it is easy to change what you need.
|
|
29
|
+
- **Open to contributions.** Bug reports, new blocks, new locales, storage
|
|
30
|
+
recipes for your database and fixes for a mail client that renders something
|
|
31
|
+
oddly are all welcome — see [Contributing](#contributing).
|
|
32
|
+
|
|
33
|
+
- **MJML under the hood** — every block compiles to battle-tested, table-based
|
|
34
|
+
email HTML. You never write a `<table>` by hand.
|
|
35
|
+
- **Your UI, your storage, your rendering.** The builder is a React component
|
|
36
|
+
with its own interface; GrapesJS is only the internal canvas engine, hidden
|
|
37
|
+
behind an adapter. Templates are stored by *your* backend, as a versioned JSON
|
|
38
|
+
project (the source of truth) plus server-compiled HTML.
|
|
39
|
+
- **Server-side rendering only.** The browser never sends HTML: the server
|
|
40
|
+
validates the project and the MJML, compiles, and stores its own HTML.
|
|
41
|
+
- **Production features**: undo/redo, desktop/mobile canvas, autosave and local
|
|
42
|
+
drafts, versions, conflict detection between concurrent editors, image
|
|
43
|
+
uploads, server preview with quality checks (missing alt text, missing
|
|
44
|
+
unsubscribe link, unknown variables, Gmail clipping…), test sends.
|
|
45
|
+
- **Merge-tag variables your users create themselves** — searchable menu,
|
|
46
|
+
insertion at the caret, in the subject line too, sample values for the
|
|
47
|
+
preview and fallbacks for recipients without a value.
|
|
48
|
+
- **Framework-agnostic server**: one Fetch-API handler for Next.js, Hono, Bun,
|
|
49
|
+
Deno, Cloudflare Workers — and Express / Node `http` through a bridge.
|
|
50
|
+
- **English and French** built in, any other language through a dictionary.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
React UI ─► EmailEditor ─► GrapesJsAdapter ─► GrapesJS + grapesjs-mjml
|
|
54
|
+
(ui/) (core/) (adapters/) internal engine
|
|
55
|
+
│ project JSON + MJML
|
|
56
|
+
Server: validation ─► MJML compilation ─► variables ─► links ─► final HTML
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install best-email-builder
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Peer dependencies: `react` and `react-dom` ≥ 18.2. The server entry needs
|
|
66
|
+
Node ≥ 20 (or any runtime with the Fetch API and `node:crypto`).
|
|
67
|
+
|
|
68
|
+
## Quick start
|
|
69
|
+
|
|
70
|
+
### 1. The component
|
|
71
|
+
|
|
72
|
+
```jsx
|
|
73
|
+
import { EmailBuilder, createHttpStorage } from 'best-email-builder';
|
|
74
|
+
import 'best-email-builder/styles.css';
|
|
75
|
+
|
|
76
|
+
const storage = createHttpStorage({ baseUrl: '/api/email-builder' });
|
|
77
|
+
|
|
78
|
+
export default function TemplateEditor({ templateId, onClose }) {
|
|
79
|
+
return (
|
|
80
|
+
<div style={{ height: '100vh' }}>
|
|
81
|
+
<EmailBuilder
|
|
82
|
+
storage={storage}
|
|
83
|
+
templateId={templateId} // omit to create a new template
|
|
84
|
+
onClose={onClose}
|
|
85
|
+
onSaved={(template) => console.log('saved', template.id)}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The builder fills its parent: give the parent a height. It is a client-only
|
|
93
|
+
component (the canvas needs the DOM). With Next.js, load it with
|
|
94
|
+
`dynamic(() => import(...), { ssr: false })` or render it from a
|
|
95
|
+
`'use client'` component.
|
|
96
|
+
|
|
97
|
+
### 2. The server routes
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
// app/api/email-builder/[...path]/route.js (Next.js App Router)
|
|
101
|
+
import {
|
|
102
|
+
createEmailBuilderHandler,
|
|
103
|
+
createMemoryTemplateRepository,
|
|
104
|
+
createMemoryAssetRepository,
|
|
105
|
+
createMemoryVariableRepository,
|
|
106
|
+
} from 'best-email-builder/server';
|
|
107
|
+
|
|
108
|
+
const handler = createEmailBuilderHandler({
|
|
109
|
+
templates: createMemoryTemplateRepository(), // replace with your database
|
|
110
|
+
assets: createMemoryAssetRepository(), // replace with your database / object storage
|
|
111
|
+
variableStore: createMemoryVariableRepository(), // optional: user-created variables
|
|
112
|
+
authorize: async (request, { action }) => {
|
|
113
|
+
const user = await getUserFromSession(request); // your auth
|
|
114
|
+
return user ? { id: user.id, name: user.email } : null; // null → 401
|
|
115
|
+
},
|
|
116
|
+
publicUrl: process.env.PUBLIC_URL, // absolute image URLs in sent emails
|
|
117
|
+
transport: { // optional: enables "Send test"
|
|
118
|
+
send: ({ to, subject, html, text }) => mailer.sendMail({ from: 'no-reply@example.com', to, subject, html, text }),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
export { handler as GET, handler as POST, handler as PUT, handler as DELETE };
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Express or plain Node:
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
import express from 'express';
|
|
129
|
+
import { createEmailBuilderHandler, toNodeListener } from 'best-email-builder/server';
|
|
130
|
+
|
|
131
|
+
const app = express();
|
|
132
|
+
app.use(toNodeListener(createEmailBuilderHandler({ /* … */ })));
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Uploaded images are served **publicly** at `GET {basePath}/assets/<key>`
|
|
136
|
+
(mail clients download them without a session). Make sure that path is not
|
|
137
|
+
behind your authentication middleware.
|
|
138
|
+
|
|
139
|
+
### 3. Sending a campaign
|
|
140
|
+
|
|
141
|
+
The builder stores the compiled HTML; at send time, personalize it per
|
|
142
|
+
recipient:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import { personalize, applyVariablesToText } from 'best-email-builder/server';
|
|
146
|
+
|
|
147
|
+
const variables = await handler.catalog(); // system + user-created variables
|
|
148
|
+
const values = { 'contact.firstname': 'Ada', 'contact.email': 'ada@example.com' };
|
|
149
|
+
const html = personalize(template.html, {
|
|
150
|
+
values,
|
|
151
|
+
variables,
|
|
152
|
+
unsubscribeUrl: signedUnsubscribeUrl(recipient),
|
|
153
|
+
baseUrl: 'https://app.example.com', // makes uploaded images absolute
|
|
154
|
+
});
|
|
155
|
+
const subject = applyVariablesToText(template.subject, values, variables);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
With an email provider that does its own merge, map the tags instead:
|
|
159
|
+
`mapVariables(html, { 'contact.firstname': '{{ contact.FIRSTNAME }}' })`.
|
|
160
|
+
|
|
161
|
+
## Component props
|
|
162
|
+
|
|
163
|
+
| Prop | Default | Description |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
| `storage` | `createHttpStorage()` | Where templates and images go (see *Storage*). |
|
|
166
|
+
| `templateId` | `null` | Template to open; `null` starts from the starter template (or a local draft). |
|
|
167
|
+
| `locale` | `'en'` | `'en'`, `'fr'`, or any BCP 47 tag used with `messages`. |
|
|
168
|
+
| `messages` | — | Extra or overriding translations (`{ 'Save': 'Speichern', … }`). |
|
|
169
|
+
| `onNotify` | built-in notices | `(level, message) => void` to use your own toaster. |
|
|
170
|
+
| `variables` | `DEFAULT_VARIABLES` | Merge tags offered in the *Variables* menu. Pass the same list to the server. |
|
|
171
|
+
| `sender` | — | `{ name, address, logoUrl, logoWidth }` used by the starter template footer. |
|
|
172
|
+
| `starterMjml` | — | Replace the starter template entirely. |
|
|
173
|
+
| `initialMjml`, `initialMeta` | — | Open a NEW template on given MJML and metadata (`{ name, subject, preheader, description }`) — e.g. a template converted from another format. Ignored with `templateId`. |
|
|
174
|
+
| `testRecipients` | `[]` | Addresses pre-filled in the *Send test* dialog. |
|
|
175
|
+
| `onClose` | — | Shows a *Back* button; called after confirming unsaved changes. |
|
|
176
|
+
| `onSaved` | — | Called with the saved template. |
|
|
177
|
+
| `onDirtyChange` | — | Called when the unsaved state changes. |
|
|
178
|
+
| `dirtyCheckRef` | — | Ref receiving a function `() => boolean` (synchronous dirty check). |
|
|
179
|
+
|
|
180
|
+
## Storage
|
|
181
|
+
|
|
182
|
+
The component only knows this contract — implement it to talk to any backend,
|
|
183
|
+
or use `createHttpStorage()` with the server handler:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
interface TemplateStorage {
|
|
187
|
+
load(id): Promise<Template>;
|
|
188
|
+
create(payload): Promise<{ template; warnings }>;
|
|
189
|
+
duplicate(id): Promise<{ template; warnings }>;
|
|
190
|
+
save(id, payload): Promise<{ template; warnings; version }>;
|
|
191
|
+
listVersions(id): Promise<Version[]>;
|
|
192
|
+
getVersion(id, versionId): Promise<Version & { document; mjml }>;
|
|
193
|
+
uploadImage(file): Promise<{ path }>;
|
|
194
|
+
render?(input): Promise<{ html; warnings; quality }>; // enables Preview
|
|
195
|
+
sendTest?(input): Promise<{ sent; failed; invalid; results }>; // enables Send test
|
|
196
|
+
listVariables?(): Promise<Variable[]>; // enable user-created variables
|
|
197
|
+
saveVariable?(variable, { create }): Promise<Variable>;
|
|
198
|
+
deleteVariable?(key): Promise<void>;
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`createMemoryStorage()` implements it in memory (demos, tests, Storybook-like
|
|
203
|
+
playgrounds).
|
|
204
|
+
|
|
205
|
+
## Server repositories
|
|
206
|
+
|
|
207
|
+
The handler applies the business rules (validation, compilation, versions,
|
|
208
|
+
concurrency); persistence is delegated to two small repositories. The memory
|
|
209
|
+
implementations (`createMemoryTemplateRepository`, `createMemoryAssetRepository`)
|
|
210
|
+
are the reference: copy their shape for SQL, MongoDB, S3…
|
|
211
|
+
|
|
212
|
+
- `templates.update(id, fields, { expectedRevision })` must be **atomic**
|
|
213
|
+
(`UPDATE … WHERE id = ? AND revision = ?`) and return `{ conflict: true, current }`
|
|
214
|
+
on mismatch — this is what prevents two editors from overwriting each other.
|
|
215
|
+
- `templates.addVersion(templateId, version, { keep })` numbers versions and
|
|
216
|
+
keeps the latest `keep` (50 by default).
|
|
217
|
+
- `assets.findBySha256 / insert / get` — images are deduplicated by content hash.
|
|
218
|
+
|
|
219
|
+
The full contracts are documented as JSDoc types in
|
|
220
|
+
[`src/server/templates.js`](src/server/templates.js) and
|
|
221
|
+
[`src/server/assets.js`](src/server/assets.js).
|
|
222
|
+
|
|
223
|
+
## Variables
|
|
224
|
+
|
|
225
|
+
The editor only inserts tags; values are filled on the server.
|
|
226
|
+
|
|
227
|
+
**System variables** come from your application (read-only in the interface):
|
|
228
|
+
|
|
229
|
+
| Tag | Sample |
|
|
230
|
+
|---|---|
|
|
231
|
+
| `{{contact.firstname}}`, `{{contact.lastname}}`, `{{contact.email}}` | Jane, Doe, jane.doe@example.com |
|
|
232
|
+
| `{{company.name}}` | Example Inc. |
|
|
233
|
+
| `{{campaign.name}}` | Newsletter |
|
|
234
|
+
| `{{ unsubscribe }}` | the unsubscribe link (`unsubscribeUrl`) |
|
|
235
|
+
|
|
236
|
+
Define your own list with `defineVariables([...])` and pass it to both
|
|
237
|
+
`<EmailBuilder variables>` and `createEmailBuilderHandler({ variables })`.
|
|
238
|
+
|
|
239
|
+
**Custom variables** are created by users from the *Variables* menu
|
|
240
|
+
(*New variable*): a name ("Order number"), a tag proposed from it
|
|
241
|
+
(`{{order_number}}`, frozen once created since templates contain it), a group,
|
|
242
|
+
a **sample value** used by the preview and test sends, and a **value if empty**
|
|
243
|
+
for recipients without one. They are shared by every template and stored by the
|
|
244
|
+
`variableStore` repository (`createMemoryVariableRepository` is the reference).
|
|
245
|
+
|
|
246
|
+
Using them:
|
|
247
|
+
|
|
248
|
+
- the menu has a search field (↑/↓ and Enter work), and a search without
|
|
249
|
+
result offers to create the variable under that name;
|
|
250
|
+
- a click inserts the tag at the caret of the text being edited; the
|
|
251
|
+
unsubscribe link becomes the URL of a selected button;
|
|
252
|
+
- the `{ }` buttons next to *Subject* and *Preview text* insert a tag there;
|
|
253
|
+
- `{{contact.firstname|there}}` sets an inline fallback for one place;
|
|
254
|
+
- the preview flags unknown tags (`{{contact.fistname}}`) before they reach anyone.
|
|
255
|
+
|
|
256
|
+
Values are HTML-escaped. A missing value falls back to the inline fallback,
|
|
257
|
+
then to the variable's *value if empty*, then to an empty string — a raw tag
|
|
258
|
+
never reaches a recipient.
|
|
259
|
+
|
|
260
|
+
## Theming
|
|
261
|
+
|
|
262
|
+
All colours, radii and fonts are CSS custom properties prefixed `--eb-`
|
|
263
|
+
(see [`src/styles.css`](src/styles.css)). Override them on `:root`:
|
|
264
|
+
|
|
265
|
+
```css
|
|
266
|
+
:root {
|
|
267
|
+
--eb-primary: #0f766e;
|
|
268
|
+
--eb-primary-foreground: #fff;
|
|
269
|
+
--eb-font-sans: 'Inter', sans-serif;
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
A dark theme applies under a `.dark` ancestor. The stylesheet uses Tailwind
|
|
274
|
+
CSS v4 with an `eb:` prefix and a reset scoped to the builder: it does not
|
|
275
|
+
touch the rest of your page, whether or not you use Tailwind yourself.
|
|
276
|
+
|
|
277
|
+
## Security model
|
|
278
|
+
|
|
279
|
+
- The server never stores HTML from the browser: it compiles its own.
|
|
280
|
+
- MJML and projects are validated against an allow-list (no scripts, event
|
|
281
|
+
handlers, `javascript:` URLs, `mj-include`, conditional comments…).
|
|
282
|
+
- Uploaded images are type-checked from their bytes (PNG, JPEG, GIF; SVG is
|
|
283
|
+
refused), limited to 2 MB, keyed by content hash and served with
|
|
284
|
+
`nosniff` and a sandboxing CSP.
|
|
285
|
+
- Previews are rendered in an iframe with an empty `sandbox`.
|
|
286
|
+
- Test sends are limited (10 addresses, 20 sends per user per hour by default).
|
|
287
|
+
|
|
288
|
+
## Development
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
npm install
|
|
292
|
+
npm run dev # Ladle playground on http://localhost:61010
|
|
293
|
+
npm test # unit tests (node:test)
|
|
294
|
+
npm run lint
|
|
295
|
+
npm run typecheck
|
|
296
|
+
npm run test:stories # Playwright against the playground
|
|
297
|
+
npm run example # full example app (React + server)
|
|
298
|
+
npm run demo # static demo (the GitHub Pages one)
|
|
299
|
+
npm run test:e2e # Playwright against the example app
|
|
300
|
+
npm run build # dist/ (ESM + styles.css + types)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the internals and
|
|
304
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
305
|
+
|
|
306
|
+
## Contributing
|
|
307
|
+
|
|
308
|
+
This project is built for and with the developer community. Good first
|
|
309
|
+
contributions:
|
|
310
|
+
|
|
311
|
+
- **a new block** — one folder in `src/blocks/` and one line in its registry;
|
|
312
|
+
- **a new interface language** — a dictionary like `src/i18n/fr.js`
|
|
313
|
+
(`npm run check:i18n` lists the missing keys);
|
|
314
|
+
- **a storage recipe** — the memory repositories are the reference shape for
|
|
315
|
+
PostgreSQL, MongoDB, S3 and friends;
|
|
316
|
+
- **a rendering report** — a template that looks wrong in a given mail client,
|
|
317
|
+
with a screenshot and the client version.
|
|
318
|
+
|
|
319
|
+
Open an [issue](https://github.com/SergeMiro/best-email-builder/issues) to
|
|
320
|
+
discuss a larger change first. Security problems go through the private
|
|
321
|
+
channel described in [SECURITY.md](SECURITY.md).
|
|
322
|
+
|
|
323
|
+
## License
|
|
324
|
+
|
|
325
|
+
[MIT](LICENSE). GrapesJS and grapesjs-mjml are BSD-3-Clause, MJML is MIT.
|
|
326
|
+
|
|
327
|
+
## Author
|
|
328
|
+
|
|
329
|
+
Built and maintained by [Sergiy Miro](https://smiro.dev/projects) — more
|
|
330
|
+
open-source work and case studies on [smiro.dev](https://smiro.dev/projects).
|