unlayer-config

Original🇺🇸 English
Translated

Configures the Unlayer editor — feature flags, appearance, theming, merge tags, design tags, display conditions, special links, HMAC security, file storage, image uploads, localization, custom fonts, validation.

14installs
Added on

NPX Install

npx skill4agent add unlayer/unlayer-skills unlayer-config

Tags

Translated version includes tags in frontmatter

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

Feature Flags

Control what's available in the editor:
javascript
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

javascript
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

javascript
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

javascript
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.):
javascript
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:
javascript
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:
javascript
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.):
javascript
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:
javascript
const crypto = require('crypto');
const signature = crypto
  .createHmac('sha256', 'YOUR_PROJECT_SECRET')  // From Dashboard
  .update(String(userId))
  .digest('hex');
Python/Django:
python
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:
javascript
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)

javascript
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:
json
{ "url": "https://your-cdn.com/images/uploaded-file.png" }

File Manager (Browse Uploaded Images)

Requires
user.id
in init — images are scoped per user:
javascript
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:
json
{
  "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

javascript
unlayer.init({
  locale: 'es-ES',
  textDirection: 'rtl',        // 'ltr' | 'rtl' | null
  translations: {
    es: { Save: 'Guardar', Cancel: 'Cancelar' },
  },
});

Validation

javascript
// 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)

javascript
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

MistakeFix
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 frontendNever put the secret in client code — generate HMAC server-side
Merge tag syntax mismatchMatch your template engine:
{{var}}
(Handlebars),
${var}
(JS),
{% %}
(Jinja)

Troubleshooting

ProblemFix
Merge tags don't appearCheck
setMergeTags()
is called after
editor:ready
or passed in
init()
HMAC signature rejectedEnsure
user.id
matches exactly what you signed, and secret is correct
File manager shows emptyCheck
user.id
is set,
userUploads.enabled = true
, provider returns correct format
Theme doesn't applyUse
unlayer.setAppearance({ theme: 'modern_dark' })
or
unlayer.setTheme('modern_dark')
after init

Paid Features

FeatureHow to Enable
Custom CSS/JS
customCSS
,
customJS
in init
Display conditions
setDisplayConditions()
Style guide
setStyleGuide()
Export Image/PDF/ZIPCloud API key required
AI features
features.ai
Collaboration
features.collaboration

Resources