Before you start
You need:
- A licence key from your Debloq dashboard.
- Your site’s domain added to the project the key belongs to. The key works
only on those domains, plus
localhostfor development.
Some options below belong to specific plans. If your plan doesn’t include an option, the editor ignores it and the matching method rejects. Everything else works the same on every plan.
Embed the editor
With a script tag
<div id="editor" style="height: 80vh"></div>
<script src="https://editor.debloq.io/stable/embed.js"></script>
<script>
const editor = Debloq.createEmailEditor('#editor', {
licenseKey: 'YOUR_LICENSE_KEY'
})
</script>
The editor fills the element you give it, so give that element a height.
With npm
npm install debloq-email-editor-react # or -vue, or -angular
Content Security Policy
If your site sends a Content-Security-Policy header, allow the editor:
frame-src https://editor.debloq.io;
script-src https://editor.debloq.io;
script-src is needed only for the script tag; the npm packages bundle the
loader. Without frame-src, the editor area stays blank and the browser console
reports a CSP violation.
What you get back
createEmailEditor returns a handle:
| Member | Use |
|---|---|
mounted |
A Promise that resolves with the editor API when the editor is ready, or null if it can’t open. |
ready(fn) |
Calls fn(api) when the editor is ready. Returns a function that removes the listener. |
api |
The editor API once ready, otherwise null. |
update(options) |
Changes options after start. |
destroy() |
Removes the editor from the page. |
const api = await editor.mounted
if (api) {
const { html } = await api.exportHtml()
}
Every editor method returns a Promise.
Load and save designs
A design is a JSON object describing the email. Store it in your database as is, and pass it back later to keep editing. You don’t need to read or change its contents.
Open a saved design
Debloq.createEmailEditor('#editor', {
licenseKey: 'YOUR_LICENSE_KEY',
modelValue: savedDesign // object or JSON string
})
Without modelValue, the editor opens an empty email. To open a different
design later:
await api.loadDesign(otherDesign)
Save
const { design, html, text } = await api.exportHtml()
await fetch('/api/emails/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ design, html, text })
})
Save design to reopen the email later, and html and text to send it.
Autosave
Debloq.createEmailEditor('#editor', {
licenseKey: 'YOUR_LICENSE_KEY',
modelValue: savedDesign,
onChange: ({ design }) => saveToServer(design)
})
onChange runs shortly after the author stops editing. See Events.
Customize the look
Theme
Match the editor to your product.
theme: 'dark' // 'light', 'dark' or 'auto' (follows the user's system)
theme: {
mode: 'auto',
accent: '#0f766e', // any CSS colour, or var(--your-brand)
fontFamily: 'Inter, sans-serif', // font of the editor interface
radius: 2, // corner radius in px; keep it small
tokens: { '--panel': '#fafafa' } // override individual colours
}
The theme styles the editor interface only; it never changes the email. Change
it at any time with api.setTheme(theme) or editor.update({ theme }).
Logo
The top-left of the editor toolbar shows the Debloq logo. Replace or hide it:
brandLogo: false // hide
brandLogo: 'Acme Mail' // your product name
brandLogo: { text: 'Acme', icon: '🚀' } // name with an emoji or glyph
brandLogo: { logo: 'https://acme.com/logo.svg', href: 'https://acme.com' } // image with a link
Brand kit
Give authors one-click access to your customers’ brand colours and fonts in every colour and font picker.
brand: {
colors: ['#4f46e5', { name: 'Ink', value: '#111827' }],
fonts: ['Poppins', 'Merriweather']
}
Update it at any time with api.setBrand(brand).
Fonts
Add fonts to the font picker, alongside the built-in web-safe and Google fonts.
fonts: [
'Bebas Neue', // a Google font by name
{ name: 'Brand Sans', stack: "'Brand Sans', Arial, sans-serif", url: 'https://acme.com/fonts.css' },
{ family: 'Roboto Slab', weights: [400, 700] } // a Google font with weights
]
Exported emails link web fonts and fall back to the stack in clients that don’t support them.
Customize the toolbar
The editor has its own top toolbar with your logo, Templates, Preflight and Export buttons.
chrome: false
Hides that toolbar so you can put these actions in your own UI, using
openTemplates(), openPreflight(), openExport() and
exportHtml().
When the toolbar is hidden, a small bar at the top of the canvas keeps the
device switcher, undo/redo and preview. Control it with canvasToolbar:
canvasToolbar: false // hide it too, and build these controls yourself
canvasToolbar: true // show it even when chrome is on
See Control the editor from your app for building your own controls.
Keyboard shortcuts (undo, redo, Esc to close dialogs, Alt+↑/↓ to move a block) work while the editor has focus.
Personalization and links
Merge tags
Authors insert personalization tags from System Tags in the text toolbar.
mergeTags: [
{ label: 'First name', value: '{{ first_name }}' },
{ label: 'Order number', value: '{{ order.number }}' }
]
Use your sending platform’s syntax; the editor inserts value exactly as
written and leaves it unchanged in the export. Without this option, the editor
offers first name, last name, full name, email, company and unsubscribe link.
Special links
Ready-made links offered wherever authors add a link (text, buttons, menus).
specialLinks: [
{ label: 'Unsubscribe', value: '{{ unsubscribe_url }}' },
{ label: 'View in browser', value: '{{ view_in_browser_url }}' },
{ label: 'My account', value: 'https://acme.com/account' }
]
Without this option the editor offers Unsubscribe, View in browser and Manage
preferences. Pass [] to remove them. Update with api.setSpecialLinks(list).
UTM tracking
Tag every outbound link in the exported email.
utm: { source: 'newsletter', medium: 'email', campaign: 'spring-sale', term: '', content: '' }
Only http and https links are tagged; merge tags, mailto: and tel:
links are left alone. Update with api.setUtm(config); pass null to stop
tagging.
Countdown timers
Timer blocks can show a live countdown that keeps ticking in the inbox. Switch a Timer block to Live mode and it exports an image from Debloq’s countdown service, which redraws the remaining time each time the email is opened.
Debloq hosts and runs the countdown service for you; there is nothing to configure. If live timers aren’t included in your plan, the timer is exported as a fixed snapshot of the time remaining at export.
Templates
Replace the template gallery with your own templates.
templates: [
{
id: 'welcome',
name: 'Welcome',
description: 'Greet new customers',
accent: '#0f766e',
design: welcomeDesign // a design saved earlier
},
{
id: 'receipt',
name: 'Receipt',
build: () => fetch('/api/templates/receipt').then((r) => r.json())
}
]
Give each template either a design or a build function that returns one.
build runs in your page, so it can call your own API. Without this option the
editor shows its built-in templates. Open the gallery from your own button
with api.openTemplates().
Custom components
Add your own blocks to the palette, such as a product card or a signature. Authors drag them in and edit them like any built-in block.
customComponents: [
{
type: 'product-card',
label: 'Product',
icon: '<svg viewBox="0 0 24 24">…</svg>',
group: 'content',
defaults: {
title: 'Running shoe',
price: '$99',
image: 'https://acme.com/shoe.png',
url: 'https://acme.com/shoe'
},
fields: [
{ key: 'title', label: 'Title', type: 'text' },
{ key: 'price', label: 'Price', type: 'text' },
{ key: 'image', label: 'Image', type: 'image' },
{ key: 'url', label: 'Link', type: 'linkurl' }
],
template: `
<img src="{{ props.image }}" width="200" alt="{{ props.title }}" style="display:block">
<h3 style="margin:8px 0">{{ props.title }}</h3>
<p style="margin:0">{{ props.price }} · <a href="{{ props.url }}">Buy now</a></p>
`
}
]
| Property | Description |
|---|---|
type |
Required. A unique id: lowercase letters, digits and hyphens, starting with a letter. It can’t match a built-in block. |
template |
Required. The block’s email HTML. |
label |
Name shown in the palette. Default: type. |
icon |
Inline SVG shown in the palette. |
group |
Palette tab. 'content' or 'layouts' adds it to that tab; any other name creates a tab. Default 'Custom'. |
defaults |
Starting values for new blocks. Strings, numbers and booleans. |
fields |
Settings the author can edit. See below. |
text |
Plain-text version of the block, using the same placeholders. Default: the template with its tags removed. |
Template. {{ props.key }} is replaced with the block’s current value for
key, HTML-escaped. Any other {{ … }}, such as {{ first_name }}, is left
as written for your sending platform. The same template is used on the canvas
and in the exported email, so write email-safe HTML with inline styles. Scripts,
event handlers and unsafe links are removed from the canvas preview only; the
export contains your template as written.
Fields. Each field edits one value in the settings panel:
{ key: 'title', label: 'Title', type: 'text', group: 'content' }
type |
Control | Extra properties |
|---|---|---|
text |
One-line text | placeholder maxlength |
textarea |
Multi-line text | rows |
linkurl |
Link, with the special links | placeholder |
number |
Number | min max step suffix |
slider |
Slider | min max step suffix |
color |
Colour picker | allowTransparent |
select |
Drop-down | options: [['s', 'Small'], ['l', 'Large']] or [{ value, label }] |
toggle |
On/off switch | |
align |
Left, centre, right | |
image |
Image picker, using your image options | |
note |
Help text; no value | text |
group puts the field under Content, Style or Layout
('content', 'style', 'layout'; default 'content'). Keys can’t start
with _.
Every custom component also gets background colour, border, rounded corners,
alignment and padding controls, applied to the cell around your template. To
replace one, add a field with the same key (backgroundColor, borderRadius
or align).
The editor reads customComponents when it opens. Definitions with an invalid
or duplicate type or no template are skipped with a warning in the browser
console. Saved designs keep the block’s values; keep the type stable so they
still open. Available on plans that include custom components.
Images
Uploads
Send uploaded images to your own storage.
imageUpload: async (file) => {
const body = new FormData()
body.append('file', file)
const res = await fetch('/api/uploads', { method: 'POST', body })
const { url } = await res.json()
return url // the public URL of the image
}
Without it, or if your function throws or returns nothing, the image is embedded in the design itself, which many email clients won’t display. It is also used to convert icons into images before export, so they show in every email client.
Your media library
Show your customer’s existing images in the image picker.
imageLibrary: async () => {
const res = await fetch('/api/media')
return res.json() // [{ url, thumb?, alt?, name? }, …]
}
Stock photos
Add a stock photo search, for example backed by Unsplash through your server.
stockSearch: async (query) => {
const res = await fetch(`/api/stock?q=${encodeURIComponent(query)}`)
return res.json() // [{ url, thumb?, alt?, name? }, …]
},
onStockSelect: async (image) => {
// Called when the author inserts a stock photo, e.g. to report a download.
await fetch('/api/stock/used', { method: 'POST', body: JSON.stringify(image) })
}
Change these later with api.setImageLibrary(fn), api.setStockSearch(fn) and
api.setStockPick(fn); pass null to remove.
Saved blocks, swatches and uploads
The editor keeps three small libraries for the author. By default they last only for the current session. Connect them to your backend to keep them per user:
savedBlocksStore: {
load: () => fetch('/api/me/saved-blocks').then((r) => r.json()),
save: (list) => fetch('/api/me/saved-blocks', { method: 'PUT', body: JSON.stringify(list) })
},
brandSwatchStore: { load, save }, // colours saved from the colour picker
assetStore: { load, save } // recently uploaded images
load() returns the list (or a Promise of it). save(list) receives the
whole list every time it changes. Store the items as given.
Send test emails
Add a Send test action to the export dialog.
onSendTest: async ({ to, html, text, design }) => {
await fetch('/api/send-test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to, html, text })
})
}
to is the address the author entered. If your function throws, the author
sees that the test failed. Without this option the action is hidden.
Draft recovery
Protect authors from losing work if the tab closes before they save.
Debloq.createEmailEditor('#editor', {
licenseKey: 'YOUR_LICENSE_KEY',
modelValue: savedDesign,
autosaveKey: 'campaign-42', // one key per email
onReady: async (api) => {
if (api.draft?.available && confirm('Restore unsaved changes?')) {
await api.restoreDraft()
}
}
})
The editor keeps a copy of the email in the author’s browser under that key.
After you save to your server, call api.discardDraft().
Control the editor from your app
Build your own toolbar around the editor:
const api = await editor.mounted
deviceSelect.onchange = (e) => api.setDevice(e.target.value) // 'desktop', 'tablet', 'mobile'
undoButton.onclick = () => api.undo()
redoButton.onclick = () => api.redo()
previewButton.onclick = () => api.togglePreview()
templatesButton.onclick = () => api.openTemplates()
checkButton.onclick = () => api.openPreflight()
exportButton.onclick = () => api.openExport()
api.devices lists the device sizes available to you, as { id, label }.
api.controls gives the state when the editor opened: device, canUndo,
canRedo, preview, and the number of preflight issues (issueCount,
errorCount, warningCount).
Export the email
HTML
const { html, text, design } = await api.exportHtml()
html: a complete email, built to render in Gmail, Outlook, Apple Mail and mobile clients.text: a plain-text version for the plain-text part of the message.design: the design, to save.
Merge tags stay in the output for your sending platform to fill in.
AMP for Email
const { html, text, amp } = await api.exportAmp()
if (amp.html) {
// Send amp.html as the text/x-amp-html part, alongside html and text.
} else {
console.warn(amp.errors) // why the AMP version isn't valid
}
amp.html is null when the email can’t be made into valid AMP; always send
html as well. amp.warnings lists differences between the AMP and HTML
versions.
Events
| Option | Called with | When |
|---|---|---|
onReady |
api |
The editor is ready to use. |
onChange |
{ design, html, text } |
Shortly after the author stops editing. html and text are included when your plan includes export. |
onUpdateModelValue |
design |
Immediately on every edit. |
onLocked |
reason |
The editor can’t open. See below. |
onError |
Error |
The editor failed to start, or something unexpected happened. |
Loading a design yourself doesn’t trigger onChange or onUpdateModelValue.
React, Vue and Angular
The components take every option as a prop or input, and update the editor when a prop changes. Give their container a height.
React
import { useRef } from 'react'
import { EmailEditor } from 'debloq-email-editor-react'
export function Designer({ design, onSave }) {
const editor = useRef(null)
async function save() {
onSave(await editor.current.exportHtml())
}
return (
<>
<div style={{ height: 700 }}>
<EmailEditor
ref={editor}
licenseKey="YOUR_LICENSE_KEY"
modelValue={design}
theme={{ mode: 'auto', accent: '#0f766e' }}
onChange={({ design }) => console.log('changed')}
/>
</div>
<button onClick={save}>Save</button>
</>
)
}
The ref gives every method plus api, controls and
devices. className and style apply to the container. In Next.js and other
server-rendered apps, use it in a client component.
Vue
<script setup>
import { ref } from 'vue'
import { EmailEditor } from 'debloq-email-editor-vue'
const design = ref(savedDesign)
const editor = ref(null)
async function save() {
const { html, text } = await editor.value.exportHtml()
}
</script>
<template>
<div style="height: 700px">
<EmailEditor
ref="editor"
v-model="design"
license-key="YOUR_LICENSE_KEY"
:theme="{ mode: 'auto', accent: '#0f766e' }"
/>
</div>
</template>
v-model keeps design in sync with the editor. Events: @ready, @change,
@update:modelValue, @locked, @error. The template ref gives every
method.
Angular
import { Component, ViewChild } from '@angular/core'
import { DebloqEmailEditorComponent, type EmailDesign } from 'debloq-email-editor-angular'
@Component({
selector: 'app-designer',
standalone: true,
imports: [DebloqEmailEditorComponent],
template: `
<debloq-email-editor
style="height: 700px"
licenseKey="YOUR_LICENSE_KEY"
[(modelValue)]="design"
[options]="options"
(locked)="onLocked($event)">
</debloq-email-editor>
`
})
export class DesignerComponent {
@ViewChild(DebloqEmailEditorComponent) editor!: DebloqEmailEditorComponent
design: EmailDesign | null = null
options = { theme: { mode: 'auto', accent: '#0f766e' }, brandLogo: 'Acme Mail' }
onLocked(reason: string) {}
async save() {
const { html, text } = await this.editor.exportHtml()
}
}
licenseKey, version and modelValue are inputs; put every other option in
options. Outputs: ready, change, modelValueChange, locked, error.
The component has loadDesign, getDesign, exportHtml and exportAmp;
use editor.api for the other methods.
Options reference
Live options can be changed after the editor opens (editor.update(), or a
changed prop in React, Vue and Angular). The others apply when the editor
opens.
| Option | Description | Live |
|---|---|---|
licenseKey |
Your licence key. | |
version |
Editor version. See Versions. | |
modelValue |
The design to open. | ✓ |
theme |
Editor colours and fonts. | ✓ |
brandLogo |
Toolbar logo. | |
brand |
Brand colours and fonts. | ✓ |
fonts |
Extra fonts. | |
chrome |
Show the editor’s top toolbar. Default true. |
|
canvasToolbar |
Show the canvas toolbar. Default: only when chrome is false. |
|
mergeTags |
Merge tags. | |
specialLinks |
Special links. | ✓ |
utm |
UTM tracking. | ✓ |
templates |
Your templates. | |
customComponents |
Your own blocks. | |
imageUpload |
Upload handler. | |
imageLibrary |
Your media library. | ✓ |
stockSearch onStockSelect |
Stock photos. | ✓ |
savedBlocksStore brandSwatchStore assetStore |
Author libraries. | |
onSendTest |
Send test. | |
autosaveKey |
Draft recovery. | |
onReady onChange onUpdateModelValue onLocked onError |
Events. | ✓ |
Methods reference
| Method | Does |
|---|---|
loadDesign(design) |
Opens a design (object or JSON string). Clears undo history. |
getDesign() |
Returns the current design. |
exportHtml() |
Returns { html, text, design }. |
exportAmp() |
Returns { html, text, design, amp }. |
openTemplates() |
Opens the template gallery. |
openPreflight() |
Opens the preflight checks. |
openExport() |
Opens the export dialog. |
setDevice(id) |
Shows the 'desktop' 'tablet' or 'mobile' view. |
setPreview(on) togglePreview() |
Turns preview mode on or off. |
undo() redo() |
Undo and redo. |
setTheme(theme) |
Changes the theme. |
setBrand(brand) |
Changes the brand kit. |
setSpecialLinks(list) |
Changes the special links. |
setUtm(config) |
Changes UTM tracking. |
setImageLibrary(fn) setStockSearch(fn) setStockPick(fn) |
Change the image sources. |
hasDraft() getDraft() restoreDraft() discardDraft() |
Draft recovery. |
| Property | Contains |
|---|---|
devices |
Available device views, { id, label }[]. |
controls |
Editor state when it opened. |
draft |
{ available, savedAt } when draft recovery is on. |
A method rejects if the editor isn’t ready yet, or if your plan doesn’t include that feature.
When the editor doesn’t open
Instead of the editor, the author sees a message, onLocked(reason) is called
and mounted resolves with null.
| Reason | What to do |
|---|---|
MissingKey |
Pass your licenseKey. |
UnknownKey |
Check the key in your Debloq dashboard. |
Revoked |
The key was revoked. Create a new one in your dashboard. |
Inactive |
Your subscription isn’t active. Check billing in your dashboard. |
DomainMismatch |
Add this site’s domain to your project in the dashboard. |
ServiceUnavailable |
Temporary. The author can press Retry. |
PremiumUnavailable |
Temporary. The author can press Retry. |
After a successful retry, onReady is called and the editor works normally.
Versions
version |
Loads |
|---|---|
'latest' (default) |
The newest release |
'stable' |
The recommended release |
'2.0.4' |
Exactly that release |
Releases never change after publication, so pinning a version keeps the editor exactly as you tested it. When pinning, use the same version in the script URL:
<script src="https://editor.debloq.io/2.0.4/embed.js"></script>
<script>
Debloq.createEmailEditor('#editor', { licenseKey: 'YOUR_LICENSE_KEY', version: '2.0.4' })
</script>