# Unlayer Config

> Configures Unlayer's email, page, popup, and document builders — feature flags, appearance, theming, merge tags, design tags, display conditions, special links, HMAC security, file storage, image uploads, localization, custom fonts, and validation. For the standalone Image Editor, use unlayer-image-editor.

- Skill: `unlayer/unlayer-config` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add unlayer/unlayer-config`
- Raw SKILL.md: https://api.skillmd.com/api/skills/unlayer/unlayer-config/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: unlayer (https://skillmd.com/u/unlayer)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/unlayer/unlayer-config

---


# 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.

This includes the image editor opened from an image inside a builder. For the standalone Image
Editor and `@unlayer/react-image-editor`, use `unlayer-image-editor`.

**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](https://console.unlayer.com)

---

## 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](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](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](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

| 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

- [Appearance](https://docs.unlayer.com/builder/appearance)
- [Merge Tags](https://docs.unlayer.com/builder/dynamic-content/merge-tags)
- [Design Tags](https://docs.unlayer.com/builder/dynamic-content/design-tags)
- [Display Conditions](https://docs.unlayer.com/builder/dynamic-content/display-conditions)
- [Special Links](https://docs.unlayer.com/builder/special-links)
- [Security (HMAC)](https://docs.unlayer.com/builder/security)
- [File Storage](https://docs.unlayer.com/builder/file-storage)
- [File Manager](https://docs.unlayer.com/builder/file-manager)
- [Localization](https://docs.unlayer.com/builder/localization)
- [Color Management](https://docs.unlayer.com/builder/color-management)

