Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
56 changes: 56 additions & 0 deletions apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,62 @@
"favicon": "/favicon.png",
"navigation": {
"anchors": [
{
"anchor": "Editor",
"icon": "pen-to-square",
"groups": [
{
"group": "Overview",
"pages": [
"editor/overview",
"editor/getting-started"
]
},
{
"group": "Features",
"pages": [
"editor/features/bubble-menu",
"editor/features/slash-commands",
"editor/features/link-editing",
"editor/features/buttons",
"editor/features/column-layouts",
"editor/features/email-export",
"editor/features/theming",
"editor/features/styling"
]
},
{
"group": "API Reference",
"pages": [
"editor/api-reference/compose-react-email",
"editor/api-reference/email-node",
"editor/api-reference/email-mark",
"editor/api-reference/use-editor",
{
"group": "UI",
"icon": "window",
"pages": [
"editor/api-reference/ui/bubble-menu",
"editor/api-reference/ui/link-bubble-menu",
"editor/api-reference/ui/button-bubble-menu",
"editor/api-reference/ui/image-bubble-menu",
"editor/api-reference/ui/slash-command"
]
},
"editor/api-reference/extensions-list",
"editor/api-reference/theming-api"
]
},
{
"group": "Advanced",
"pages": [
"editor/advanced/custom-extensions",
"editor/advanced/extensions",
"editor/advanced/event-bus"
]
}
]
},
{
"anchor": "Documentation",
"icon": "book-open",
Expand Down
37 changes: 14 additions & 23 deletions apps/docs/editor/advanced/custom-extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -228,9 +228,11 @@ export function MyEditor() {
}
```

## Creating based on an existing Node
## Wrapping existing TipTap extensions

Wrap an existing TipTap node with email serialization support:
Both `EmailNode` and `EmailMark` provide a `.from()` method that wraps an existing TipTap
extension with email serialization support. This is useful when you want to reuse a community
TipTap extension and add email export support without rewriting it.

```tsx
import { EmailNode } from '@react-email/editor/core';
Expand All @@ -243,34 +245,23 @@ const MyEmailNode = EmailNode.from(MyTipTapNode, ({ children, style }) => {
});
```

This is useful when you want to reuse a community TipTap extension and add email export support.

## EmailMark

For inline marks (like bold, italic, or custom annotations), use `EmailMark`:

```tsx
import { EmailMark } from '@react-email/editor/core';
import { Mark } from '@tiptap/core';

const Highlight = EmailMark.create({
name: 'highlight',

parseHTML() {
return [{ tag: 'mark' }];
},

renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0];
},
const MyTipTapMark = Mark.create({ /* ... */ });

renderToReactEmail({ children, style }) {
return (
<mark style={{ ...style, backgroundColor: '#fef08a' }}>{children}</mark>
);
},
const MyEmailMark = EmailMark.from(MyTipTapMark, ({ children, style }) => {
return <mark style={{ ...style, backgroundColor: '#fef08a' }}>{children}</mark>;
});
```

<Info>
For full API details on all methods (`create`, `from`, `configure`, `extend`), see the
[`EmailNode`](/editor/api-reference/email-node) and [`EmailMark`](/editor/api-reference/email-mark)
reference pages.
</Info>

## Configure and extend

Both `EmailNode` and `EmailMark` support TipTap's standard customization methods:
Expand Down
128 changes: 128 additions & 0 deletions apps/docs/editor/api-reference/compose-react-email.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: "composeReactEmail"
sidebarTitle: "composeReactEmail"
description: "Convert editor content to email-ready HTML and plain text."
icon: "file-export"
---

The `composeReactEmail` function serializes the editor's document tree into email-ready HTML
and a plain text fallback.

## Import

```tsx
import { composeReactEmail } from '@react-email/editor/core';
```

## Signature

```tsx
async function composeReactEmail(params: {
editor: Editor;
preview: string | null;
}): Promise<{ html: string; text: string }>;
```

## Parameters

<ResponseField name="editor" type="Editor" required>
The TipTap editor instance. The function reads the editor's JSON document and walks through
each registered extension to serialize nodes and marks.
</ResponseField>

<ResponseField name="preview" type="string | null" required>
Preview text shown in inbox list views before the email is opened. Pass `null` to omit.
</ResponseField>

## Return value

Returns a `Promise` that resolves to an object with:

| Field | Type | Description |
|-------|------|-------------|
| `html` | `string` | Full HTML email string, ready to send |
| `text` | `string` | Plain text version for email clients that don't support HTML |

## How it works

The serialization pipeline:

1. **Read** the editor's JSON document
2. **Traverse** each node and mark in the document tree
3. **Call** `renderToReactEmail()` on each [`EmailNode`](/editor/api-reference/email-node) and [`EmailMark`](/editor/api-reference/email-mark) extension
4. **Apply** theme styles via the `SerializerPlugin` (if [`EmailTheming`](/editor/features/theming) is configured)
5. **Wrap** the content in a `BaseTemplate` component
6. **Render** to an HTML string and plain text version

## Usage

### Basic export

```tsx
import { composeReactEmail } from '@react-email/editor/core';

const { html, text } = await composeReactEmail({ editor, preview: null });
```

### With preview text

```tsx
const { html, text } = await composeReactEmail({
editor,
preview: 'Check out our latest updates!',
});
```

### With theming

When the [`EmailTheming`](/editor/features/theming) extension is in your extensions array,
theme styles are automatically injected into the exported HTML. No extra configuration needed:

```tsx
import { StarterKit } from '@react-email/editor/extensions';
import { EmailTheming } from '@react-email/editor/plugins';

const extensions = [StarterKit, EmailTheming.configure({ theme: 'basic' })];

// Later, when exporting:
const { html } = await composeReactEmail({ editor, preview: null });
// html includes all theme styles inline
```

### Full example with export panel

```tsx
import { composeReactEmail } from '@react-email/editor/core';
import { useCurrentEditor } from '@tiptap/react';
import { useState } from 'react';

function ExportPanel() {
const { editor } = useCurrentEditor();
const [html, setHtml] = useState('');
const [exporting, setExporting] = useState(false);

const handleExport = async () => {
if (!editor) return;
setExporting(true);
const result = await composeReactEmail({ editor, preview: null });
setHtml(result.html);
setExporting(false);
};

return (
<div>
<button onClick={handleExport} disabled={exporting}>
{exporting ? 'Exporting...' : 'Export HTML'}
</button>
{html && (
<textarea
readOnly
value={html}
rows={16}
style={{ width: '100%', fontFamily: 'monospace' }}
/>
)}
</div>
);
}
```
135 changes: 135 additions & 0 deletions apps/docs/editor/api-reference/email-mark.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: "EmailMark"
sidebarTitle: "EmailMark"
description: "Base class for creating editor marks with email serialization."
icon: "paintbrush-fine"
---

`EmailMark` extends TipTap's `Mark` class with an additional `renderToReactEmail()` method
that controls how the mark is serialized when exporting to email HTML via
[`composeReactEmail`](/editor/api-reference/compose-react-email).

Marks are used for inline formatting like bold, italic, links, and custom text annotations.

## Import

```tsx
import { EmailMark } from '@react-email/editor/core';
```

## EmailMark.create

Create a new email-compatible mark from scratch.

```tsx
const Highlight = EmailMark.create({
name: 'highlight',

parseHTML() {
return [{ tag: 'mark' }];
},

renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0];
},

renderToReactEmail({ children, style }) {
return (
<mark style={{ ...style, backgroundColor: '#fef08a' }}>{children}</mark>
);
},
});
```

The config object accepts all standard [TipTap Mark options](https://tiptap.dev/docs/editor/api/mark)
plus the `renderToReactEmail` method.

### renderToReactEmail props

| Prop | Type | Description |
|------|------|-------------|
| `children` | `React.ReactNode` | The content wrapped by this mark |
| `style` | `React.CSSProperties` | Resolved theme styles for this mark (empty object if no theme) |
| `mark` | `Mark` | The ProseMirror mark instance |
| `node` | `Node` | The ProseMirror node that contains this mark |
| `extension` | `EmailMark` | The extension instance, useful for accessing `options` |

## EmailMark.from

Wrap an existing TipTap mark with email serialization support. This is useful when you want to
reuse a community TipTap extension and add email export support without rewriting it.

```tsx
import { EmailMark } from '@react-email/editor/core';
import { Mark } from '@tiptap/core';

const MyTipTapMark = Mark.create({
Comment thread
joaopcm marked this conversation as resolved.
name: 'customHighlight',

parseHTML() {
return [{ tag: 'mark' }];
},

renderHTML({ HTMLAttributes }) {
return ['mark', HTMLAttributes, 0];
},
});

const MyEmailMark = EmailMark.from(MyTipTapMark, ({ children, style }) => {
return (
<mark style={{ ...style, backgroundColor: '#fef08a' }}>{children}</mark>
);
});
```

The second argument is the `renderToReactEmail` renderer component. It receives the same props
as described above.

## .configure

Configure options on an `EmailMark` (same as TipTap's `.configure()`):

```tsx
import { Bold } from '@react-email/editor/extensions';

const CustomBold = Bold.configure({ HTMLAttributes: { class: 'custom-bold' } });
```

## .extend

Extend an `EmailMark` with additional behavior:

```tsx
import { Link } from '@react-email/editor/extensions';

const CustomLink = Link.extend({
addKeyboardShortcuts() {
return {
'Mod-k': () => this.editor.commands.toggleLink({ href: '' }),
};
},
});
```

You can also override `renderToReactEmail` when extending:

```tsx
const CustomLink = Link.extend({
renderToReactEmail({ children, style, mark }) {
return (
<a
href={mark.attrs.href}
style={{ ...style, color: '#0066cc', textDecoration: 'underline' }}
>
{children}
</a>
);
},
});
```

## See also

- [Custom Extensions](/editor/advanced/custom-extensions) — tutorial on building custom extensions
- [`EmailNode`](/editor/api-reference/email-node) — the equivalent class for block nodes
- [`composeReactEmail`](/editor/api-reference/compose-react-email) — the function that calls `renderToReactEmail`
Loading
Loading