unlayer-config
Scannednpx machina-cli add skill unlayer/unlayer-skills/unlayer-config --openclawConfigure the Editor
Overview
Unlayer's behavior is controlled through unlayer.init() options and runtime methods. This skill covers features, appearance, dynamic content, security, and file storage.
Where to find keys:
- Project ID — Dashboard > Project > Settings
- Project Secret (for HMAC) — Dashboard > Project > Settings > API Keys
- Cloud API Key (for image/PDF export) — Dashboard > Project > Settings > API Keys
Dashboard: console.unlayer.com
Feature Flags
Control what's available in the editor:
unlayer.init({
features: {
audit: true, // Content validation
preview: true, // Preview button
undoRedo: true, // Undo/redo
stockImages: true, // Stock photo library
userUploads: true, // User upload tab
preheaderText: true, // Email preheader field
textEditor: {
spellChecker: true,
tables: false, // Tables inside text blocks
cleanPaste: 'confirm', // true | false | 'basic' | 'confirm'
emojis: true,
},
// Paid features:
ai: true, // AI text generation
collaboration: false, // Real-time collaboration
sendTestEmail: false, // Test email button
},
});
See references/feature-flags.md for all flags (AI sub-options, image editor, color picker, etc.).
Appearance & Theming
unlayer.init({
appearance: {
theme: 'modern_dark', // 'modern_light' | 'modern_dark' | 'classic_light' | 'classic_dark'
panels: {
tools: {
dock: 'right', // 'left' | 'right' (default: 'right')
collapsible: true,
},
},
actionBar: {
placement: 'top', // 'top' | 'bottom' | 'top_left' | 'top_right' | 'bottom_left' | 'bottom_right'
},
},
});
// Change at runtime:
unlayer.setAppearance({ theme: 'modern_dark' });
// Or just the theme:
unlayer.setTheme('modern_dark');
Custom Fonts
unlayer.init({
fonts: {
showDefaultFonts: true,
customFonts: [{
label: 'Poppins',
value: "'Poppins', sans-serif",
url: 'https://fonts.googleapis.com/css?family=Poppins:400,700',
weights: [400, 700], // or [{ label: 'Regular', value: 400 }, { label: 'Bold', value: 700 }]
}],
},
});
Tab Configuration
unlayer.init({
tabs: {
content: { enabled: true, position: 1 },
blocks: { enabled: true, position: 2 },
body: { enabled: true, position: 3 },
images: { enabled: true, position: 4 },
uploads: { enabled: true, position: 5 },
},
});
Merge Tags
Merge tags are placeholders replaced at send time (e.g., {{first_name}}). They use your template engine's syntax (Handlebars, Liquid, Jinja, etc.):
unlayer.setMergeTags({
first_name: {
name: 'First Name',
value: '{{first_name}}', // Your template syntax
sample: 'John', // Shown in editor preview
},
last_name: {
name: 'Last Name',
value: '{{last_name}}',
sample: 'Doe',
},
company: {
name: 'Company', // Nested group
mergeTags: {
name: { name: 'Company Name', value: '{{company.name}}', sample: 'Acme Inc' },
logo: { name: 'Logo URL', value: '{{company.logo}}' },
},
},
products: {
name: 'Products',
rules: {
repeat: {
name: 'Repeat for Each Product',
before: '{{#each products}}', // Loop start — syntax depends on your template engine
after: '{{/each}}', // Loop end
sample: true, // Show sample data in editor
},
},
mergeTags: {
name: { name: 'Product Name', value: '{{this.name}}' },
price: { name: 'Price', value: '{{this.price}}' },
image: { name: 'Image URL', value: '{{this.image}}' },
},
},
});
// Autocomplete trigger (optional)
unlayer.setMergeTagsConfig({ autocompleteTriggerChar: '{{', sort: true });
Design Tags (Editor-Only Placeholders)
Design tags are replaced in the editor UI but NOT in exports — useful for showing personalized content to the template author:
unlayer.setDesignTags({
business_name: 'Acme Corp',
current_user_name: 'Jane Smith',
});
unlayer.setDesignTagsConfig({ delimiter: ['{{', '}}'] });
Display Conditions (Paid)
Wrap content in conditional blocks for your template engine:
unlayer.setDisplayConditions([
{
type: 'segment',
label: 'VIP Customers',
description: 'Only shown to VIP segment',
before: '{% if customer.vip %}', // Your template engine syntax
after: '{% endif %}',
},
{
type: 'segment',
label: 'New Subscribers',
description: 'First 30 days only',
before: '{% if subscriber.age_days < 30 %}',
after: '{% endif %}',
},
]);
Special Links
Pre-defined links users can insert (unsubscribe, preferences, etc.):
unlayer.setSpecialLinks({
unsubscribe: {
name: 'Unsubscribe',
href: '{{unsubscribe_url}}',
target: '_blank',
},
preferences: {
name: 'Preferences',
specialLinks: {
email_prefs: { name: 'Email Preferences', href: '{{preferences_url}}' },
profile: { name: 'Profile Settings', href: '{{profile_url}}' },
},
},
});
HMAC Security
Prevents users from impersonating each other. Generate the HMAC signature server-side using your Project Secret (Dashboard > Project > Settings > API Keys):
Node.js:
const crypto = require('crypto');
const signature = crypto
.createHmac('sha256', 'YOUR_PROJECT_SECRET') // From Dashboard
.update(String(userId))
.digest('hex');
Python/Django:
import hmac, hashlib
signature = hmac.new(
b'YOUR_PROJECT_SECRET',
bytes(str(request.user.id), encoding='utf-8'),
digestmod=hashlib.sha256
).hexdigest()
Client-side — pass the server-generated signature:
unlayer.init({
user: {
id: userId, // Must match what you signed
signature: signatureFromServer, // HMAC from your backend
name: 'John Doe', // Optional
email: 'john@acme.com', // Optional
},
});
See references/security.md for Ruby and PHP examples.
File Storage & Image Upload
Custom Upload (Your Server)
unlayer.registerCallback('image', (file, done) => {
const data = new FormData();
data.append('file', file.attachments[0]);
fetch('/api/uploads', { method: 'POST', body: data })
.then((r) => {
if (!r.ok) throw new Error('Upload failed');
return r.json();
})
.then((result) => done({ progress: 100, url: result.url }))
.catch((err) => console.error('Upload error:', err));
});
Your backend should return:
{ "url": "https://your-cdn.com/images/uploaded-file.png" }
File Manager (Browse Uploaded Images)
Requires user.id in init — images are scoped per user:
unlayer.init({
user: { id: 123 }, // Required for file manager
features: { userUploads: { enabled: true, search: true } },
});
unlayer.registerProvider('userUploads', (params, done) => {
// params: { page, perPage, searchText }
fetch(`/api/images?userId=123&page=${params.page}&perPage=${params.perPage}`)
.then((r) => r.json())
.then((data) => {
done(
data.items.map((img) => ({
id: img.id, // Required
location: img.url, // Required — the image URL
width: img.width, // Optional but recommended
height: img.height, // Optional but recommended
contentType: img.contentType, // Optional: 'image/png'
source: 'user', // Required: must be 'user'
})),
{ hasMore: data.hasMore, page: params.page, total: data.total }
);
});
});
Your backend should return:
{
"items": [
{ "id": "img_1", "url": "https://...", "width": 800, "height": 600, "contentType": "image/png" }
],
"hasMore": true,
"total": 42
}
See references/file-storage.md for upload progress with XHR, image deletion, and Amazon S3 setup.
Localization
unlayer.init({
locale: 'es-ES',
textDirection: 'rtl', // 'ltr' | 'rtl' | null
translations: {
es: { Save: 'Guardar', Cancel: 'Cancelar' },
},
});
Validation
// Global validator — runs on all content
unlayer.setValidator(async ({ html, design, defaultErrors }) => {
return [...defaultErrors]; // Return modified error list
});
// Per-tool validator
unlayer.setToolValidator('text', async ({ html, defaultErrors }) => {
return defaultErrors;
});
// Run audit on demand
unlayer.audit((result) => {
// result: { status: 'FAIL' | 'PASS', errors: [{ id, icon, severity, title, description }] }
if (result.status === 'FAIL') {
console.log('Issues found:', result.errors);
}
});
safeHtml (XSS Protection)
unlayer.init({
safeHtml: true, // Sanitize HTML via DOMPurify
// Or with custom options:
safeHtml: {
domPurifyOptions: {
FORCE_BODY: true,
},
},
// WRONG: safeHTML (capital HTML) is DEPRECATED — use safeHtml
});
Common Mistakes
| Mistake | Fix |
|---|---|
safeHTML (uppercase) | Use safeHtml (camelCase) — old casing deprecated |
features.blocks = false hides tab | It disables blocks but NOT the tab — use tabs config |
Deprecated colorPicker.presets | Use colorPicker.colors instead (string[] or ColorGroup[]) |
Missing user.id for file manager | File Manager requires user.id in init |
| Project Secret exposed in frontend | Never put the secret in client code — generate HMAC server-side |
| Merge tag syntax mismatch | Match your template engine: {{var}} (Handlebars), ${var} (JS), {% %} (Jinja) |
Troubleshooting
| Problem | Fix |
|---|---|
| Merge tags don't appear | Check setMergeTags() is called after editor:ready or passed in init() |
| HMAC signature rejected | Ensure user.id matches exactly what you signed, and secret is correct |
| File manager shows empty | Check user.id is set, userUploads.enabled = true, provider returns correct format |
| Theme doesn't apply | Use unlayer.setAppearance({ theme: 'modern_dark' }) or unlayer.setTheme('modern_dark') after init |
Paid Features
| Feature | How to Enable |
|---|---|
| Custom CSS/JS | customCSS, customJS in init |
| Display conditions | setDisplayConditions() |
| Style guide | setStyleGuide() |
| Export Image/PDF/ZIP | Cloud API key required |
| AI features | features.ai |
| Collaboration | features.collaboration |
Resources
Source
git clone https://github.com/unlayer/unlayer-skills/blob/main/unlayer-config/SKILL.mdView on GitHub Overview
This skill configures Unlayer's editor via unlayer.init() options and runtime methods. It covers features, appearance, dynamic content, security, and file storage, aligning the editor with your workflow. You’ll wire in project keys, enable or disable features, customize fonts and tabs, and set up merge tags and validation.
How This Skill Works
It reads project credentials from the dashboard (Project ID, Project Secret for HMAC, Cloud API Key for exports) and applies settings via unlayer.init with features, appearance, fonts, tabs, and mergeTags. At runtime you can push changes with methods like unlayer.setAppearance, setTheme, and setMergeTags. This enables dynamic theming, content validation, and asset handling in your emails or pages.
When to Use It
- When you need content validation and live previews during design
- When branding requires a consistent theme, fonts, and tool layout
- When you manage dynamic content with merge tags and nested groups
- When securing assets, HMACs, and cloud image storage for exports
- When localizing UI and fonts for multilingual or regional teams
Quick Start
- Step 1: Call unlayer.init with your chosen features, appearance, fonts, and tabs
- Step 2: Set up merge tags with unlayer.setMergeTags and test samples
- Step 3: Supply Project ID, Project Secret, and Cloud API Key from the Dashboard and test in staging
Best Practices
- Map features to your workflow and disable unused flags to reduce complexity
- Test merge tag syntax and nested groups with sample data before deployment
- Secure project keys and API keys, rotating them per project
- Preview emails or pages after enabling new themes or fonts
- Document your init settings and maintain versioned configs
Example Use Cases
- Enable modern_dark theme with right-hand tool panels and audit + undoRedo flags
- Enable ai for draft generation and stockImages for campaigns
- Add a custom font like Poppins with a live font URL
- Configure merge tags with first_name, last_name and a nested company group
- Turn on image uploads and file storage, with localization for a multilingual team