Appearance
Reusable Content
Five options control what users can insert into a page and reuse across pages: savedBlocks gives each user a personal section library, syncedSectionsEnabled keeps shared sections identical across pages, premadeBlocks and premadeTemplates supply ready-made designs, and customBlocks registers block types of your own. A sixth option, contentBlocks, trims which built-in block types appear at all.
savedBlocks
The savedBlocks option (boolean, default false) enables the user's personal block library:
typescript
{
savedBlocks: true
}With it enabled, users save custom sections (hero headers, footers, content modules) into a personal library and reuse them across landing pages. The library is backed by your api.SAVED_SECTIONS endpoint; there is no local, in-memory variant, so the option only works together with the endpoint.
syncedSectionsEnabled
The syncedSectionsEnabled option (boolean, default false) enables sections that stay consistent across multiple pages:
typescript
{
syncedSectionsEnabled: true
}A synced section is linked, not copied: updating it updates every page that uses it, which suits headers, footers, disclaimers, and promotional banners. Without the option, sections are edited independently per page. Synced sections are stored through the same api.SAVED_SECTIONS endpoint as saved blocks.
Re-rendering on your backend
When you convert a saved page to HTML yourself through the JSON to HTML endpoint, pass the current content of every synced section the page uses in the syncedSections request field. The endpoint swaps each referenced section (syncedId) for the supplied definition before rendering, so pages pick up the latest version of a shared header or footer even if they were saved before it changed. Without the field, the page renders with the section content embedded in the saved template JSON.
premadeBlocks
The premadeBlocks option (array | object | false, optional) controls the library of ready-to-use design sections that users can insert with a single click.
When it is not set, the editor shows its built-in premade block library with predefined sections such as hero banners, feature lists, testimonials, and pricing tables.
To supply your own blocks, pass an array of named groups. Each group holds blocks with a name, a preview image img, and the section definition JSON. A plain array replaces the built-in library:
typescript
premadeBlocks: [
{
name: "Heroes",
blocks: [
{
name: "Hero 1",
img: "https://example.com/previews/hero-1.png",
definition: { /* section JSON, e.g. exported from a saved template */ }
}
]
}
]To extend the built-in set instead of replacing it, use the wrapper form with override: false; your groups are added alongside the default library (override: true behaves like the plain array and replaces it):
typescript
premadeBlocks: {
override: false,
blocks: [ /* same array of groups as above */ ]
}To disable the premade blocks library completely, pass false:
typescript
premadeBlocks: falsepremadeTemplates
The premadeTemplates option (boolean, default false) enables the full-page template library:
typescript
{
premadeTemplates: true
}With it enabled, users browse and start from complete premade templates (product pages, lead-generation pages, event signup pages) instead of a blank page or whatever template you load. The picker can also be opened from your own UI:
js
LPE.openPremadeTemplatesSelection();By default the library is served by Topol from app.topol.io. To serve your own library instead, point the PREMADE_TEMPLATES, PREMADE_TEMPLATE_CATEGORIES, and PREMADE_TEMPLATES_KEYWORDS endpoints at your backend; see Premade Templates API below.
A companion option, premadeTemplatesOptions (object, optional), tunes the picker's UI:
typescript
{
premadeTemplates: true,
premadeTemplatesOptions: {
hideSearch: true
}
}hideSearch(boolean): removes the search field from the template picker, which suits a small curated library where search adds nothing.showDelete(boolean): accepted by the schema but not currently wired to any UI, so it has no effect.
customBlocks
The customBlocks option (array, default []) registers your own block types, which users then add and configure like native blocks. This fits unique components such as dynamic CTAs, custom layouts, or platform-specific integrations.
Basic custom block:
typescript
customBlocks: [
{
key: "custom-text-key",
name: "My Customised Text",
icon: "box",
disabled: false,
type: "text",
attributes: {
align: "right",
padding: "10px 10px"
},
content: "Custom Text block"
}
]The fields every custom block accepts:
key(string, required): your identifier for the block. It is written into the template JSON, so keep it stable.name(string, required): the label shown in the Elements panel.type(string, optional):"text","image","button","video", or"mix"for a block that combines several elements.icon(string, optional):"box"or"rss"for the two built-in icons, anhttps://image URL, or an inline<svg>…</svg>string. Anything else falls back to the"box"icon.attributes(object, optional): the block's default attributes, withmdAttributes,smAttributes, andxsAttributesoverriding them at the tablet, mobile-landscape, and mobile breakpoints.content(string, optional): the block's initial content.disabled(boolean, optional) anddisabledBadge(string, optional): grey the block out in the Elements panel and label it, the same waycontentBlocksdoes for built-in blocks.
Mixed block with multiple elements:
typescript
customBlocks: [
{
key: "custom-mix",
name: "Custom mixed block",
icon: "box",
disabled: false,
type: "mix",
blocks: [
{
type: "text",
attributes: { align: "left" },
content: "My custom content"
},
{
type: "button",
attributes: {
align: "center",
"background-color": "#417505"
},
content: "Click me!"
}
]
}
]Custom HTML with dialog:
typescript
customBlocks: [
{
key: "custom-block-key",
name: "My Custom Block",
dialog: true,
icon: "box",
content: "<p>initial content</p>",
dialogButtonText: "Open Custom Dialog",
disabled: false
}
]A dialog-style custom block needs the onOpenCustomBlockDialog callback. Like all callbacks, it is passed as a top-level property next to config:
typescript
const LPE = LandingPageEditor({
config: TOPOL_OPTIONS,
onOpenCustomBlockDialog({ block }) {
// open your own dialog UI here
console.log("Custom block dialog requested:", block);
},
});WARNING
There is currently no public method to push edited content back into the custom block from your dialog.
contentBlocks
The contentBlocks option (object, optional) controls which block types appear in the Elements panel, and whether they are disabled. It is an object keyed by block type; valid keys are text, image, gif, button, divider, spacer, social, video, form, html, raw, and custom-raw. Each entry accepts:
hidden(boolean): removes the block from the Elements panel entirely.disabled(boolean): keeps the block visible but not usable.disabledText(string): tooltip text shown on a disabled block.disabledBadge(string): badge label shown on a disabled block (e.g."PRO").
typescript
{
contentBlocks: {
html: { hidden: true },
form: { disabled: true, disabledText: "Upgrade to use forms", disabledBadge: "PRO" }
}
}When contentBlocks is not set, all block types are available. The disabled variant is useful for upsell flows, where a block advertises a higher plan instead of vanishing.
API Integration for Saved Blocks
Saved blocks and synced sections are stored on your server, with support for folders, search, pagination, and preview images. Enable the features and point api.SAVED_SECTIONS at your backend:
typescript
const TOPOL_OPTIONS = {
savedBlocks: true,
syncedSectionsEnabled: true,
api: {
SAVED_SECTIONS: "https://your-domain.com/saved-blocks"
}
};Both features share the single api.SAVED_SECTIONS endpoint and are differentiated by the type field in requests and responses, which is one of saved_section, synced_section, or folder. In the URLs below, {API.SAVED_SECTIONS} stands for the base URL you configured.
WARNING
Before implementing the endpoints, check how to work with API endpoints.
List Saved Blocks, Synced Sections and Folders
- URL:
/{API.SAVED_SECTIONS} - Method:
GET - Content-Type:
application/json
Params:
| key | value | required |
|---|---|---|
| key | authorization api key | true |
| hostname | hostname | true |
| entity_id | entity_id corresponds to userId in Options | true |
| per_page | number of items per page | false |
| current_page | number of page to return | false |
| sort_by | "name" or "date" or "type" | false |
| desc | "true" | false |
| folder_id | id of folder to return | false |
| search | string to search | false |
| type | "saved_section" or "synced_section" to filter by type | false |
This endpoint is called when the editor lists all saved blocks/synced sections, changes sort, page or folder, searches by string, or whenever LPE.refreshSyncedSections() is called on the editor instance.
Response:
json
{
"success": true,
"data": {
"data": [
{
"id": 1,
"name": "name of saved block, synced section, or folder",
"type": "saved_section",
"created_at": "ISO 8601 time",
"definition": { "tagName": "mj-section", "children": ["..."] },
"image": "https://url-to-preview-image.jpg",
"folder_id": 2
}
],
"lastPage": 5,
"parentFolderId": null
}
}In all saved-sections payloads, definition is the section JSON as an object (or an array of section objects), not a serialized string.
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": { "tagName": "mj-section", "children": ["..."] },
"folder_id": 1
}Response:
json
{
"success": true,
"data": {
"id": 2,
"name": "name of saved block, synced section, or folder",
"type": "saved_section",
"created_at": "ISO 8601 time",
"definition": { "tagName": "mj-section", "children": ["..."] },
"folder_id": 1
}
}Get Synced Section Detail
- URL:
/{API.SAVED_SECTIONS}/{id} - Method:
GET - Params:
key,hostname,entity_id
This endpoint is called only for synced sections: when a synced section is inserted into a template, and when a template containing synced sections is refreshed so the latest content is displayed. Regular saved blocks are inserted from the listing data and never hit this endpoint.
Response:
json
{
"success": true,
"data": {
"id": 1,
"name": "name of the synced section",
"type": "synced_section",
"created_at": "ISO 8601 time",
"definition": { "tagName": "mj-section", "children": ["..."] },
"folder_id": 2
}
}The response's type must be exactly "synced_section", and success must be true. If either is missing, the editor treats the section as missing and drops it from the page instead of reporting an error, so this is worth checking first when synced sections disappear on load.
Edit Saved Block, Synced Section or Folder
- 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.
Request:
json
{
"entity_id": "entity user_id",
"hostname": "origin",
"key": "api key",
"folder_id": 1,
"definition": { "tagName": "mj-section", "children": ["..."] },
"name": "edited name"
}Response:
HTTP 200 OK
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:
HTTP 200 OK
Refresh Synced Sections
Call refreshSyncedSections on the editor instance to update the synced sections listing and reload the template with the latest changes:
js
const LPE = LandingPageEditor({ config: TOPOL_OPTIONS });
LPE.render("#landing-page-editor");
// later, e.g. when another user changed a synced section:
LPE.refreshSyncedSections();This matters when multiple users work in the editor simultaneously: when someone else creates, edits, or deletes a synced section, calling this function keeps the current view up to date.
Premade Templates API
The premade template library is served by three endpoints, which default to Topol's own collection on app.topol.io and can be repointed at your backend through the api option:
| Endpoint key | Serves |
|---|---|
PREMADE_TEMPLATES | The template listing, and one template's full detail at {PREMADE_TEMPLATES}/{id} |
PREMADE_TEMPLATE_CATEGORIES | The categories offered as filters |
PREMADE_TEMPLATES_KEYWORDS | The keywords offered as filters |
The listing is a GET with per_page and current_page query parameters, plus search, categories, and keywords when the user filters. Both the listing and the detail must answer with a { "success": true, "data": ... } envelope; a missing or falsy success is reported to the user as a fetch failure.
When the URL points at app.topol.io, the editor authenticates with your Public API key:
json
{
"accept": "application/json",
"Authorization": "Bearer <api-key>"
}WARNING
That header is added only for app.topol.io URLs; endpoints on your own domain get no Topol credentials, so secure them with apiAuthorizationHeader instead.
Note that apiAuthorizationHeader also reaches app.topol.io, and its string form sets Authorization, replacing the key the default library expects. If you use the string form and keep Topol's premade templates, host your own template endpoints or use the object form with a custom header name.
