Visual Editor
Preview and edit content in real time — see every change reflected instantly in your running application without leaving the Localess UI.
The Localess Visual Editor opens your frontend application inside an iframe alongside the content form. As you edit fields on the right, the changes are pushed to the preview on the left in real time. Clicking on any marked element in the preview jumps directly to the corresponding field in the form.

How it works
- Localess renders your app inside an iframe in the left panel of the document editor.
- A small sync script loaded by your app opens a message channel to the Localess parent window.
- When a field changes in the form, Localess sends an
inputorchangeevent through the channel. - Your app receives the event, updates its local state, and re-renders — no page reload needed.
- Editable elements marked with
data-ll-idanddata-ll-schemaattributes are highlighted so editors can click them to jump to the right field in the form — and the selected block gets a toolbar to move, duplicate or delete it right in the preview.
The editor toolbar switches between Visual Editor — the preview beside the form — and Form Mode, the form alone.
Setup
Integrating the Visual Editor takes one step in Localess and three in your frontend application.
Step 1 — Add a preview environment
In Settings → Visual Editor, add one or more environments, each with a Name (e.g. Production, Development) and a URL. Editors pick the environment from the preview toolbar.
A plain URL loads URL + locale/ + fullSlug, leaving the locale segment out for the default locale — so end the URL with /: https://preview.example.com/ loads https://preview.example.com/blog/post.
When your routes have a different shape, use placeholders instead. A URL containing any placeholder is filled in rather than appended to:
| Placeholder | Value for blog/hello in German |
|---|---|
{fullSlug} | blog/hello |
{slug} | hello |
{parentSlug} | blog — empty at the root |
{documentId} | The document's ID |
{locale} | de — the space's fallback locale on the default locale |
{locale/} | de/ — nothing on the default locale |
For example, https://{locale}.example.com/{fullSlug} puts the locale in the host, and https://preview.example.com/posts/{slug} drops the parent folders. Values are URL-encoded per path segment, and unknown placeholders such as {lang} are rejected. Each URL field shows the example URL it opens for a sample document.
The URL must use https:// — or http://localhost / http://127.0.0.1 for a local dev server — on a different origin from the Localess admin app. A relative URL, another scheme or the admin app's own origin shows Invalid preview URL; a plain http:// address on any other host is blocked by the browser. The preview runs in a sandboxed iframe, and messages are only exchanged with that origin — so a URL that redirects to another origin (for example, to www.) won't connect. Use the final address.
Step 2 — Load the sync script
The sync script must be loaded in the browser. Use the SDK helper or add the script tag manually.
Via @localess/live-preview:
import { loadLocalessSync } from "@localess/live-preview";
// Call once on the client side, e.g. in your root layout or app entry point
loadLocalessSync("https://my-localess.web.app");Or manually in HTML:
<script
id="localess-js-sync"
type="text/javascript"
async
src="https://my-localess.web.app/scripts/sync-v1.js"
></script>Replace https://my-localess.web.app with your Localess instance origin.
The script tag takes two optional attributes:
data-debugturns on the script's debug mode: console logging and short on-page messages in the preview. It is off by default, so content editors see neither — the script then logs one line when it connects, plus warnings for real misconfiguration. The SDKs set it when you passdebug: truealongsideenableSync.data-sdknames the SDK that loaded the script, e.g.@localess/react@<version>. It is what the editor's connection-status tooltip shows. The SDKs set it for you;loadLocalessSync(origin, { debug, sdk })accepts both.
Framework-specific SDKs can load the script automatically. Set enableSync: true when registering the provider:
| SDK | Option |
|---|---|
@localess/react | localessInit({ enableSync: true }) |
@localess/angular | provideLocaless({ enableSync: true }) |
@localess/vue | app.use(Localess, { enableSync: true }) |
@localess/nuxt | localess: { enableSync: true } in nuxt.config.ts |
@localess/svelte | localessInit({ enableSync: true }) |
@localess/astro | localess({ enableSync: true }) in astro.config.mjs |
Step 3 — Mark editable elements
Add data-ll-id and data-ll-schema attributes to the root element of each content block so the Visual Editor can highlight and select it. Add data-ll-field to individual field elements for field-level selection.
Use the helper functions from @localess/live-preview to set these attributes:
import { localessEditable, localessEditableField } from "@localess/live-preview";
// Marks the block root as editable
// Returns: { 'data-ll-id': '...', 'data-ll-schema': '...' }
localessEditable(data)
// Marks a specific field as editable
// Returns: { 'data-ll-field': 'title' }
localessEditableField<MyPage>('title')React example:
const HeroSection = ({ data, links }) => (
<section {...localessEditable(data)}>
<h1 {...localessEditableField<HeroSection>('title')}>{data.title}</h1>
<p {...localessEditableField<HeroSection>('subtitle')}>{data.subtitle}</p>
</section>
);Vue example:
<template>
<section v-bind="localessEditable(data)">
<h1 v-bind="localessEditableField('title')">{{ data.title }}</h1>
</section>
</template>Svelte example — in @localess/svelte, localessEditable is an action, applied with use::
<section use:localessEditable={data}>
<h1 {...localessEditableField('title')}>{data.title}</h1>
</section>
localessEditableandlocalessEditableFieldalways return the attributes — they don't check whether the page is in the Visual Editor. Outside it nothing reads them: the highlighting and click handling come from the sync script once it connects, so the attributes are safe to leave in production.
Block actions in the preview
When a block is selected — clicked in the preview or opened in the form — the preview shows a toolbar in its top-right corner. What it offers depends on where the block sits in the document:
| Block | Toolbar |
|---|---|
| In a list — a Schemas field | Move up, Move down, Duplicate, Delete. The first block has no Move up, the last no Move down |
| In a single Schema field | Delete only |
| The document root | No toolbar |
Delete takes a second click to confirm. Every action is an unsaved edit, just like one made in the form: the editor applies it to the document and sends the updated content to your app as a change event, so nothing beyond Step 4 is needed. The toolbar finds the block through its data-ll-id, which must be the block's _id — exactly what localessEditable(data) sets.
Step 4 — Subscribe to edit events
Listen for input and change events from window.localess and update your application state. window.localess only exists once the sync script has loaded inside the Visual Editor iframe, so subscribe after loadLocalessSync resolves and guard the access:
import { loadLocalessSync } from "@localess/live-preview";
// `content` is the document this page fetched
loadLocalessSync("https://my-localess.web.app").then(() => {
const unsubscribe = window.localess?.onChange((event) => {
// An edit to another document rendered on the page
if (event.documentId !== content.id) return;
// Replace your server-fetched content with the live editor data
setPageData(event.data);
});
});onChange(callback) subscribes to input and change; on(type, callback) takes one event type or an array of them. Both return a function that removes the subscription, and off(type, callback) removes a callback as well.
The input event fires whenever a field value changes — every keystroke — for a real-time preview. The change event fires when blocks are added, removed, duplicated or reordered, and once when the preview connects, carrying the current content. Subscribe to both: input alone misses structural edits and the initial content.
Events reference
| Event | When it fires | Payload |
|---|---|---|
input | Whenever a field value changes (every keystroke) | { type: 'input', documentId: string, data: ContentData } |
change | When blocks are added, removed, duplicated or reordered — and once when the preview connects, with the current content | { type: 'change', documentId: string, data: ContentData } |
save | When the document is saved | { type: 'save', documentId: string } |
publish | When the document is published | { type: 'publish', documentId: string } |
unpublish | When the document is unpublished | { type: 'unpublish', documentId: string } |
pong | Heartbeat response from the editor | { type: 'pong' } |
enterSchema | Editor cursor enters a schema block | { type: 'enterSchema', id, schema, field?, root? } |
hoverSchema | Editor cursor hovers over a schema block | { type: 'hoverSchema', id, schema, field? } |
leaveSchema | Editor cursor leaves a schema block | { type: 'leaveSchema' } |
documentId is the id of the edited document — the Content.id the API returned for it — so a page rendering several documents applies each edit only to the matching one. root: true on enterSchema means the editor went back to the document root, which clears the selection in the preview.
Known limitation: live edits carry only
data, not the resolvedlinks,referencesandassetsmaps from your original fetch. A link, reference or asset picked in the editor doesn't resolve in the preview — a content link points to/not-found, a new reference renders empty, a new image has noaltor dimensions — until the page fetches the document again.@localess/astroand the@localess/react/rscLocalessDocumentre-render from the API on Save; with the other SDKs, save and then reload the preview.
Troubleshooting
The status icon at the end of the preview's URL bar shows whether the page is connected:
- Grey — the page loaded, but no sync script answered. The tooltip reads Page loaded — no Localess SDK detected.
- Green — connected. The tooltip names the SDK and version that loaded the script, or reads older sync script when the page runs a sync script from an older Localess deployment.
The editor shows a notice above the preview when something is off:
| Notice | What to check |
|---|---|
| This page isn't connected to the Visual Editor. — shown when the page loaded but hasn't connected within about three seconds | Sync is enabled (enableSync) and content is rendered with LocalessDocument or subscribed as in Step 4; the SDK's origin points to this Localess instance; the page is served from the environment URL's origin, without redirecting to another one |
| This page loads Localess from …, but this editor runs on … | The page loads the sync script from a different Localess origin. Edits still show, but content and assets come from the other instance — set the SDK's origin to the editor's address |
| This page doesn't show '…'. | None of the page's top-level data-ll-id blocks belongs to the open document — usually because you navigated to another page inside the preview. Edits won't appear there; Back to … reloads the document's preview URL |
| No editable blocks found on this page. | The page is connected but has no data-ll-id elements. Render content with LocalessDocument, or add localessEditable to your components as in Step 3 |
Reloads and navigation inside the preview reconnect automatically — the leaving page tells the editor, and the next page connects again once its sync script loads.
Update your Localess deployment together with the SDK. The SDKs apply a live edit only when its
documentIdmatches the rendered document. An older Localess deployment sends edits withoutdocumentId, so with a current SDK the preview connects but shows no edits.
SDK integrations
Official SDKs provide built-in Visual Editor support with enableSync and helper utilities. For frameworks without a dedicated SDK, use @localess/live-preview directly following the manual setup steps above.
| Framework | SDK | Guide |
|---|---|---|
| TypeScript / Node | @localess/client + @localess/live-preview | TypeScript SDK |
| Angular | @localess/angular | Angular SDK |
| React / Next.js | @localess/react | React SDK |
| Nuxt | @localess/nuxt | Nuxt SDK |
| Svelte / SvelteKit | @localess/svelte | Svelte SDK |
| Astro | @localess/astro | Astro SDK |
Draft & Publish
Nothing goes live until you say so. Localess keeps every change in Draft until you explicitly promote it to Published — giving your team a safe staging buffer built into the CMS.
Webhooks
Receive real-time HTTP notifications when content, translations, or assets change in Localess — and trigger downstream systems automatically.