Localess

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.

Visual Editor

How it works

  1. Localess renders your app inside an iframe in the left panel of the document editor.
  2. A small sync script loaded by your app opens a message channel to the Localess parent window.
  3. When a field changes in the form, Localess sends an input or change event through the channel.
  4. Your app receives the event, updates its local state, and re-renders — no page reload needed.
  5. Editable elements marked with data-ll-id and data-ll-schema attributes 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:

PlaceholderValue 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-debug turns 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 pass debug: true alongside enableSync.
  • data-sdk names 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:

SDKOption
@localess/reactlocalessInit({ enableSync: true })
@localess/angularprovideLocaless({ enableSync: true })
@localess/vueapp.use(Localess, { enableSync: true })
@localess/nuxtlocaless: { enableSync: true } in nuxt.config.ts
@localess/sveltelocalessInit({ enableSync: true })
@localess/astrolocaless({ 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>

localessEditable and localessEditableField always 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:

BlockToolbar
In a list — a Schemas fieldMove up, Move down, Duplicate, Delete. The first block has no Move up, the last no Move down
In a single Schema fieldDelete only
The document rootNo 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

EventWhen it firesPayload
inputWhenever a field value changes (every keystroke){ type: 'input', documentId: string, data: ContentData }
changeWhen blocks are added, removed, duplicated or reordered — and once when the preview connects, with the current content{ type: 'change', documentId: string, data: ContentData }
saveWhen the document is saved{ type: 'save', documentId: string }
publishWhen the document is published{ type: 'publish', documentId: string }
unpublishWhen the document is unpublished{ type: 'unpublish', documentId: string }
pongHeartbeat response from the editor{ type: 'pong' }
enterSchemaEditor cursor enters a schema block{ type: 'enterSchema', id, schema, field?, root? }
hoverSchemaEditor cursor hovers over a schema block{ type: 'hoverSchema', id, schema, field? }
leaveSchemaEditor 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 resolved links, references and assets maps 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 no alt or dimensions — until the page fetches the document again. @localess/astro and the @localess/react/rsc LocalessDocument re-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:

NoticeWhat to check
This page isn't connected to the Visual Editor. — shown when the page loaded but hasn't connected within about three secondsSync 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 documentId matches the rendered document. An older Localess deployment sends edits without documentId, 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.

FrameworkSDKGuide
TypeScript / Node@localess/client + @localess/live-previewTypeScript SDK
Angular@localess/angularAngular SDK
React / Next.js@localess/reactReact SDK
Nuxt@localess/nuxtNuxt SDK
Svelte / SvelteKit@localess/svelteSvelte SDK
Astro@localess/astroAstro SDK

On this page