Documentation Guides
Custom blocks
Define field-driven email blocks in your application and render them through the embedding SDK.
On this page
Define a block
Pass customBlocks as an array. Renderers run in your application, synchronously or through a promise. The iframe receives JSON definitions and an HTML body fragment. The SDK has no React runtime dependency.
<script src="https://weavour.com/editor/sdk/0.1.12/editor.js"></script>
<div id="email-editor" style="height: 800px"></div>
<script>
const escapeHTML = value => String(value).replace(/[&<>"']/g, char =>
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
const editor = Weavour.mount('#email-editor', {
editorKey: 'YOUR_PUBLIC_EDITOR_KEY',
userId: 'user-123', organizationId: 'org-456',
customBlocks: [{
id: 'product-card', version: 1, label: 'Product card',
defaultProps: { title: 'New product', href: 'https://example.com/product' },
controls: {
title: { type: 'text', label: 'Product name', required: true },
href: { type: 'url', label: 'Product link' },
},
render: props => '<table role="presentation" width="100%"><tbody><tr>' +
'<td style="padding:24px"><h2>' + escapeHTML(props.title) + '</h2>' +
'<a href="' + escapeHTML(props.href) + '">View product</a></td></tr></tbody></table>',
}],
onDocumentChange(document) { /* Save the complete envelope in your backend. */ },
});
</script>Definition reference
| Property | Contract |
|---|---|
| id | Stable ID: starts with a letter, then letters, digits, underscores or hyphens; at most 64 characters. |
| version | Positive integer. Together with id, identifies the exact renderer and controls. |
| label | Authored display name, at most 200 characters. Customer labels are not translated by the editor. |
| defaultProps | JSON object copied into newly inserted blocks; existing saved props stay unchanged. |
| controls | Field definitions keyed by prop name, each with a label and a compatible default. |
| render(props) | Returns an inline-styled HTML body fragment or Promise<string>; runs in your application. |
| iconSvg | Optional static SVG string replacing the automatic palette outline. |
Use React Email
Install React, @react-email/components and @react-email/render in your application. Tested with components 1.0.12, render 2.2.0 and React 19.3.1: Section, Row, Column, Text, Button and Img. Return a self-contained body fragment; omit Html, Head, Body and head styles.
import { defineCustomBlock } from '@weavour/editor';
import { WeavourEditor } from '@weavour/react';
import { Section, Text, Button } from '@react-email/components';
import { render } from '@react-email/render';
export const productCard = defineCustomBlock({
id: 'product-card', version: 1, label: 'Product card',
defaultProps: { title: 'New product', href: 'https://example.com/product' },
controls: {
title: { type: 'text', label: 'Product name', required: true },
href: { type: 'url', label: 'Product link' },
},
async render(props) {
const html = await render(
<Section><Text>{props.title}</Text>
<Button href={props.href} style={{ padding: '12px 24px' }}>View product</Button>
</Section>
);
// React Email adds a doctype even when rendering a body fragment.
return html.replace(/^<!doctype[^>]*>/i, '');
},
});
export function CampaignEditor() {
return <WeavourEditor
editorKey="YOUR_PUBLIC_EDITOR_KEY"
userId="user-123" organizationId="org-456"
customBlocks={[productCard]} style={{ height: 800 }}
/>;
}Controls and image props
Every control needs a label and a compatible default prop. Image controls open the scoped library and its crop flow. The image descriptor is { src, assetId, width, height, alt }; render from src and alt. Asset IDs never grant access. Additional bounded JSON props may hold saved customer data.
| Type | Value | Options |
|---|---|---|
| text / textarea | string | placeholder, required, maxLength |
| url | string | required; merge tags |
| image | descriptor | null | required; src, assetId, width, height, alt |
| number | number | min, max, step |
| select | string | options: [{ label, value }] |
| boolean | boolean | |
| color | string | #rrggbb |
Insert and edit
Custom blocks appear after standard blocks in the same Blocks grid, without a separate heading or separator. Saved organization blocks follow custom blocks in the same grid. Search blocks above the grid filters all three kinds by name; clear the field or press Escape to restore the full palette. Click, drag or focus a tile and press Enter to insert it. The Block pane generates controls from your definition; select fields use the same themed choice menu as standard controls. Move, duplicate, delete and undo work normally. While rendering, a new block reserves an empty selection area; updates retain the last preview and show a sidebar spinner. Failure and retry guidance stays in the sidebar, outside the email content.
Palette icons
Without iconSvg, the palette draws a wireframe from the browser layout of render(defaultProps). A matching saved snapshot is reused; otherwise the renderer runs once per definition/registry generation within the shared render limits. Icons are cached across tab and theme changes. Measurement loads no external resources; images without dimensions use placeholders. A failed render keeps a generic insertion icon. Set iconSvg to a static SVG string with a viewBox to override the outline and skip palette rendering. Supported geometry: g, path, rect, circle, ellipse, line, polyline and polygon. Paints follow the theme, preserving none. Scripts, events, styles, text, references and embedded content are rejected. Limits: 16 KiB UTF-8, 128 elements, 12 levels. Icons never change documents, undo history or email exports.
// Optional property on a custom-block definition. Omit for an automatic outline.
const iconSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">' +
'<rect x="3" y="3" width="18" height="18" rx="2"/>' +
'<path d="M7 8h10M7 12h6M7 16h8"/></svg>';
// Keep the renderer and controls; replace only the palette icon.
editor.updateConfig({ customBlocks: [{ ...productCard, iconSvg }] });Versioning and live configuration
Register exact (id, version) pairs; the palette inserts the highest registered version. Increment version whenever renderer behavior, defaults or control semantics change. Keep older versions registered to edit older content. Existing props and snapshots are never rewritten by changed definitions. Top-level customBlocks overrides editorConfig.customBlocks; updateConfig({ customBlocks }) replaces the registry, [] clears it and omission preserves it. React customBlocks props update without remounting.
// productCard is the definition from the React Email example above.
editor.updateConfig({ customBlocks: [productCard] });
// Include each custom ID when supplying an explicit insertion allowlist.
editor.updateConfig({
allowedBlocks: ['text', 'heading', 'section', 'custom:product-card'],
});
// Remove registrations without deleting existing email content.
editor.updateConfig({ customBlocks: [] });Saving, previews and export
Save the complete schema-3 onDocumentChange envelope and reopen it with emailDocument. It contains each block’s reference, props and validated HTML snapshot; callbacks and icon metadata are not saved. Draft autosave preserves unresolved edits. Export, test email, template creation and organization-block saving wait for current output and fail if it is unavailable. A missing renderer preserves valid saved snapshots for preview/export and disables field editing. Reopening a valid snapshot does not rerender that document node; the palette may separately render defaults to build its icon. Dashboard previews use snapshots. Organization blocks are reusable layouts that can contain native or custom blocks; saving one requires organizationId.
HTML policy and personalization
Return email tables, links, text and images with inline styles. Supported Outlook button comments are preserved. Scripts, event handlers, forms, embedded frames/objects, document elements, head CSS, classes requiring global styles and CSS URLs are rejected with a render error. Escape authored values. Known merge tokens remain literal in callbacks; use configured merge tags and conditional layouts for recipient personalization. No recipient data is fetched by Weavour.
Block restrictions
An explicit allowedBlocks array must include custom:product-card to allow insertion of this example. With no allowlist, registered custom IDs are available. Restrictions preserve existing nodes. Custom code transport is disabled in the public demo. Upgrade both SDK and hosted readers before enabling schema-3 writing; older peers fail explicitly.
Limits and troubleshooting
Per mount: 50 definition versions, 32 controls per version, 100 custom nodes, 32 KiB UTF-8 props per node, 100 KiB snapshot HTML, four active renders and 100 pending requests. Typing debounces for 150 ms; rendering times out after five seconds. Schema-3 documents have a combined 1,000,000-byte budget, including snapshots and preview metadata; bridge/API payload limits also apply. Renderers must finish promptly and avoid side effects; synchronous JavaScript cannot be forcibly interrupted. Save resolved data in props for reproducible content. Retry failed blocks, restore the exact registration or reduce output size. Full-email AI replacement is unavailable in custom-block documents.