Skip to content

Saved Blocks and Synced Sections

Topol provides two powerful features for reusing content across your email templates:

  • Saved Blocks - Save any section or content block and reuse it across multiple templates. When you insert a saved block, it creates a copy that you can edit independently.

  • Synced Sections - Save sections that stay synchronized across all templates. When you edit a synced section, the changes are automatically reflected everywhere it's used.

Enabling Saved Blocks

Users can save their structures by clicking "Save as Block" button while the structure they want to save is selected.

Save as block

To enable this feature, set savedBlocks to an empty array.

ts
savedBlocks: [],

To disable it, set savedBlocks to null.

ts
savedBlocks: null,

You can also hide saved blocks by setting savedBlocks to false.

ts
savedBlocks: false,

Each saved block within the savedBlocks array is an object and has the following structure:

ts
{
  id: 1, // ID of the block, if not set it uses the Array Index
  name: 'My saved block 001', // You can set a name or an image
  img: 'http://...', // The editor shows only name or image, not both, if both set it will show the image
  definition: 'json' // JSON of this saved block
},

Enabling Synced Sections

To enable synced sections, set syncedSectionsEnabled to true and configure the api.SAVED_SECTIONS endpoint. Synced sections are always API-backed, so they cannot be configured locally the way saved blocks can.

ts
const TOPOL_OPTIONS = {
  savedBlocks: true,
  syncedSectionsEnabled: true,
  api: {
    SAVED_SECTIONS: "https://your-domain.com/saved-blocks",
  },
};

syncedSectionsEnabled defaults to false. When true:

  • The Saved Blocks panel gains a type filter with the values all, saved_section, and synced_section.
  • A "Save as synced section" action appears on the section toolbar for any movable section that isn't already synced.
  • Sections backed by a synced section are rendered with a locked visual treatment in the canvas and can only be edited through the section actions menu, which updates the source on your backend.

Unlike regular saved blocks, synced sections maintain a live connection to every template where they're used. When the source is updated on your backend, the editor pulls the latest definition the next time it loads the template (or when you call TopolPlugin.refreshSyncedSections()).

Server-side resolution is required

A synced section in a saved template JSON is stored as a lightweight reference (just a syncedId on the section), not as the section's full content. Your backend must resolve those references by passing the matching synced sections to the Convert JSON template to HTML endpoint before generating the final HTML. Without that step, synced sections render as empty placeholders. See Server-Side Resolution below.

Advanced Configuration

If you opted into manual mode by setting savedBlocks to an array of local blocks (and have not configured api.SAVED_SECTIONS), there are three callbacks you need to implement to make saved blocks persist:

  • onBlockSave
  • onBlockEdit
  • onBlockRemove

All of them need to be implemented in the callbacks object. These callbacks do not fire when savedBlocks is true (API-based mode), and onBlockEdit / onBlockRemove also stop firing as soon as you configure api.SAVED_SECTIONS. Configuring that endpoint switches the panel into API mode regardless of whether savedBlocks is an array or true. Synced sections always go through the API-Based Configuration below and never trigger any of these callbacks.

Example:

ts
callbacks: {
  onBlockSave(json) {
    const name = window.prompt('Enter block name:')
    if (name !== null) {
      console.log('saving block', json)
    }
  },

  onBlockRemove(id) {
    if (window.confirm('Are you sure?')) {
      console.log('removing block', id)
    }
  },

  onBlockEdit(id) {
    const name = window.prompt('Block name:', 'My block 001')
    if (name !== null) {
      console.log('saving edited block', id)
    }
  }
}

Since it's your responsibility to handle those actions, you also need to update the list of saved blocks once they are updated. To do this, you can use the TopolPlugin.setSavedBlocks([savedBlocks]) function.

This function takes an array of all saved blocks you want to display in the editor.

ts
TopolPlugin.setSavedBlocks([
  {
    id: 1, // ID of the block, if not set it uses the Array Index
    name: "My saved block 001", // You can set a name or an image
    img: "http://...", // The editor shows only name or image, not both, if both set it will show the image
    definition: "json", // JSON of this saved block
  },
]);

API-Based Configuration

For a more advanced setup, you can use the API-based feature. This allows you to store saved blocks and synced sections on your server with support for folders, search, pagination, and preview images.

To enable API-based saved blocks and synced sections, set savedBlocks to true and configure the api.SAVED_SECTIONS endpoint:

ts
const TOPOL_OPTIONS = {
  savedBlocks: true,
  syncedSectionsEnabled: true, // Enable synced sections
  api: {
    SAVED_SECTIONS: "https://your-domain.com/saved-blocks",
  },
};

Both saved blocks and synced sections use the same API.SAVED_SECTIONS endpoint. They are differentiated by the type field in the API responses and requests.

Before implementing the endpoints check how to work with API endpoints

List Saved Blocks, Synced Sections and Folders

Request

  • URL: /{API.SAVED_SECTIONS}
  • Method: GET
  • Content-Type: application-json

Params:

keyvaluerequired
keyauthorization api keytrue
hostnamehostnametrue
entity_identity_id corresponds to userId in Optionstrue
per_pagenumber of items per pagefalse
current_pagenumber of page to returnfalse
sort_by"name" or "date" or "type"false
desc"true"false
folder_idid of folder to returnfalse
searchstring to searchfalse
type"saved_section" or "synced_section" to filter by typefalse

This endpoint is called when the editor lists all saved blocks/synced sections, changes sort, page or folder, searches by string, or when you call TopolPlugin.refreshSyncedSections().

Response

The data.data array mixes folders, saved sections, and synced sections and the type field discriminates between them. Synced sections additionally carry a translation field used by Multilingual Templates (see Multi-language Support below).

json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": 1,
        "name": "Footer",
        "type": "synced_section",
        "created_at": "2026-01-15T14:48:00.000Z",
        "definition": {
          "tagName": "mj-section",
          "attributes": { "padding": "10px 10px" },
          "children": [
            {
              "tagName": "mj-column",
              "attributes": { "width": "100%" },
              "children": [
                {
                  "uid": "abc123",
                  "tagName": "mj-text",
                  "content": "Unsubscribe",
                  "attributes": {}
                }
              ],
              "uid": "col-uid-1"
            }
          ],
          "uid": "P2fM6_fcp"
        },
        "image": "https://url-to-preview-image.jpg",
        "folder_id": 2,
        "translation": {
          "en": { "abc123": { "content": "Unsubscribe" } },
          "de": { "abc123": { "content": "Abmelden" } }
        }
      },
      {
        "id": 2,
        "name": "Hero banner",
        "type": "saved_section",
        "created_at": "2026-01-12T09:21:00.000Z",
        "definition": { "tagName": "mj-section", "children": [], "uid": "..." },
        "image": "https://url-to-preview-image.jpg",
        "folder_id": null
      },
      {
        "id": 3,
        "name": "Newsletters",
        "type": "folder",
        "created_at": "2026-01-10T08:00:00.000Z",
        "folder_id": null
      }
    ],
    "lastPage": 5,
    "parentFolderId": null
  }
}

definition is the MJML section as a nested JSON object, and translation (synced sections only) is a locale-keyed map where each locale maps block UIDs to their translated content. The Convert JSON template to HTML endpoint accepts the same fields but JSON-encoded as strings. See Server-Side Resolution.

Understanding parentFolderId

The parentFolderId field tells the editor which folder is the parent of the folder currently being viewed. The editor uses this value for breadcrumb navigation and to traverse back up the folder hierarchy.

How to compute it: When the request includes a folder_id parameter, look up that folder in your database and return its parent folder's ID. If the folder is at the root level (has no parent), return null.

ScenarioparentFolderId value
Root level (no folder_id in request)null
Root-level folder (folder has no parent)null
Nested folder (folder's parent is folder 5)5

WARNING

parentFolderId must be the ID of the parent of the queried folder — not the queried folder's own ID. Returning the folder's own ID will cause infinite API requests and broken breadcrumb navigation.

Create Saved Block, Synced Section or Folder

  • URL: /{API.SAVED_SECTIONS}
  • Method: POST

This endpoint is called when a new saved block, synced section, or folder is created.

Request

json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "type": "saved_section",
  "name": "name of new item",
  "definition": "json of MJML section",
  "folder_id": 1
}

For synced sections, set type: "synced_section". When the template containing the section uses multilingual templates, the editor also sends a translation field: a locale-keyed map of block UID to translated fragment. You must persist it alongside the definition so it can be returned to clients later.

json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "type": "synced_section",
  "name": "Footer",
  "definition": { "tagName": "mj-section", "children": [], "uid": "..." },
  "translation": {
    "en": { "abc123": { "content": "Unsubscribe" } },
    "de": { "abc123": { "content": "Abmelden" } }
  },
  "folder_id": 1
}

Response

STATUS 200

json
{
  "success": true,
  "data": {
    "id": 2,
    "name": "name of saved block, synced section, or folder",
    "type": "saved_section",
    "created_at": "ISO 8601 time",
    "definition": "MJML section as nested JSON object",
    "translation": "locale-keyed translation map (synced sections only)",
    "folder_id": 1
  }
}

Get Saved Block or Synced Section Detail

Request

  • URL: /{API.SAVED_SECTIONS}/{id}
  • Method: GET
  • params: key, hostname, entity_id

This endpoint is called when inserting a saved block into a template. For synced sections, it is also called once per synced section during TopolPlugin.refreshSyncedSections() to pull the latest definition of every synced section in the open template.

Response

STATUS 200

json
{
  "success": true,
  "data": {
    "id": 1,
    "name": "name of saved block or synced section",
    "type": "synced_section",
    "created_at": "ISO 8601 time",
    "definition": "MJML section as nested JSON object",
    "translation": "locale-keyed translation map (synced sections only)",
    "folder_id": 2
  }
}

Edit Saved Block, Synced Section or Folder

Request

  • URL: /{API.SAVED_SECTIONS}/{id}
  • Method: PATCH

This endpoint is called when editing the name, content, or folder location of a saved block, synced section, or folder. The editor sends only the fields that changed.

json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "folder_id": 1,
  "definition": "MJML section as nested JSON object",
  "name": "edited name"
}

Translations are not re-sent on edit

The PATCH request does not include a translation field, even for synced sections in multilingual templates. The synced section's translation map is captured at creation time (via POST) and is updated separately, not through this endpoint. Your backend should not expect translation updates on PATCH.

Response

STATUS 200

Delete Saved Blocks, Synced Sections or Folders

  • URL: /{API.SAVED_SECTIONS}/delete
  • Method: POST

This endpoint is called when the user deletes saved blocks, synced sections, or folders. For synced sections, the last loaded version remains in templates where it was used.

Request

json
{
  "entity_id": "entity user_id",
  "hostname": "origin",
  "key": "api key",
  "blocksToDelete": [1, 2, 3]
}

Response

STATUS 200

Refresh Synced Sections

Call refreshSyncedSections on the TopolPlugin instance to re-pull every synced section that is currently placed in the open template and re-fetch the panel listing from api.SAVED_SECTIONS.

js
TopolPlugin.refreshSyncedSections();

Signature: TopolPlugin.refreshSyncedSections(): void. Takes no arguments and returns nothing.

What it does:

  • For every section in the open template that carries a syncedId, it issues a GET /{API.SAVED_SECTIONS}/{id} request and replaces the section in the canvas with the latest version returned by your API.
  • If the API responds successfully, the new definition is spliced into the MJML tree in place and any stale translations for the old block UIDs are dropped from the template's langs array before the fresh content is merged in.
  • If the API returns no result for an id (for example, the synced section was deleted server-side, or the response is not of type synced_section), the syncedId is stripped from the section and it becomes a regular, editable section. Its last-loaded content stays in place.

When to call it:

  • After your own UI creates, edits, or deletes a synced section outside the editor.
  • When multiple users edit synced sections concurrently and you want the open editor to pull the latest versions on demand.
  • When you push updates to your synced section store via background jobs and want the editor to reflect them without a full page reload.

If api.SAVED_SECTIONS is not configured, this call is a no-op.

Preview Images

When creating a saved block or synced section, you can store a preview image on your server. The editor sends the block definition, and you can generate a preview image from it. Return the image URL in the image field when listing items.

Server-Side Resolution

When a template is saved, every synced section in the canvas is stored as a placeholder. The section node carries only a syncedId referencing the synced section in your database, not its content. Before generating final HTML, your backend must resolve those placeholders into the live definitions stored on your side.

The Convert JSON template to HTML endpoint accepts an optional syncedSections array for this purpose. Each item must contain:

FieldTypeRequiredDescription
idnumberyesThe synced section ID (matches syncedId on the section node)
definitionstringyesJSON-encoded MJML definition of the section
translationstringnoJSON-encoded locale-keyed translation map (required for multilingual templates)

Request example:

json
{
  "definition": "{ ...template JSON... }",
  "options": { "language": "en" },
  "syncedSections": [
    {
      "id": 1,
      "definition": "{\"tagName\":\"mj-section\",\"uid\":\"GbG0yTG8t\",\"children\":[...]}",
      "translation": "{\"en\":{\"abc123\":{\"content\":\"Unsubscribe\"}},\"de\":{\"abc123\":{\"content\":\"Abmelden\"}}}"
    }
  ]
}

How resolution works:

  1. The endpoint scans the template's body sections for any node carrying a syncedId.
  2. For each match, it looks up the matching item in syncedSections by id.
  3. The section's child columns and blocks are spliced into the template in place of the placeholder. Block UIDs are regenerated to avoid collisions when the same synced section is used twice in one template.
  4. If the template is multilingual, the synced section's translation map is merged into the template's langs[].translations so that each language mutation renders the correct localised content.
  5. If a syncedId has no matching entry in the syncedSections array, the placeholder is left alone and the section renders empty, so make sure you fetch and forward every referenced synced section.

Always populate syncedSections for any template that may contain them

The HTML output returned to your customers is wrong if you skip this step. The frontend onSave callback hands you the list of synced section IDs used in the template as its fourth syncedSections argument. Look those IDs up in your api.SAVED_SECTIONS storage and pass the resulting definitions (and translations, if multilingual) to convertJson2Html alongside the template. You do not need to walk the template JSON yourself.

Keep stored HTML in sync when a synced section changes

Because a synced section can be edited independently of the templates that use it, the HTML you have stored for those templates becomes stale as soon as the source changes. To keep stored HTML up to date:

  1. When your api.SAVED_SECTIONS storage receives a POST or PATCH for a synced section, look up every template in your database whose saved syncedSections ID list contains that section's ID.
  2. For each of those templates, call Convert JSON template to HTML again, passing the latest version of every referenced synced section in the syncedSections request field.
  3. Overwrite the stored HTML for each template with the freshly rendered output.

If you also support concurrent editing (multiple users on the same template at the same time), call TopolPlugin.refreshSyncedSections() in the open editor sessions so the canvas pulls the latest synced section definition without a full reload.

Multi-language Support

If you're using multilingual templates, synced sections can include translations. The translation field is a nested JSON object with the following shape: a top-level locale key, then a block UID, then the translated content/attributes for that block in that locale.

json
{
  "id": 1,
  "name": "Header block",
  "type": "synced_section",
  "definition": {
    "tagName": "mj-section",
    "children": [
      {
        "tagName": "mj-column",
        "children": [
          { "uid": "abc123", "tagName": "mj-text", "content": "..." }
        ]
      }
    ],
    "uid": "P2fM6_fcp"
  },
  "translation": {
    "en": { "abc123": { "content": "Unsubscribe" } },
    "de": { "abc123": { "content": "Abmelden" } },
    "fr": { "abc123": { "content": "Se désabonner" } }
  }
}

Each per-block translation may carry content (the inner HTML/text of the block) and attributes (for example href for buttons or src/alt for images). The block UIDs in this map are the UIDs from the synced section's own definition - when the section is rendered into a template, they are remapped to the new generated UIDs in the resulting MJML tree.

When this section is later sent to Convert JSON template to HTML in the syncedSections array, both definition and translation must be re-serialised as JSON-encoded strings - that endpoint accepts them as strings, not nested objects.

Regular (non-synced) saved blocks do not carry a translation field, as translations for ordinary saved blocks are merged into the host template's langs array at insert time and stored on the template itself.