unlayer-config
npx skills add https://github.com/unlayer/unlayer-skills --skill unlayer-config
Agent 安装分布
Skill 文档
Configure 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 |