External widget — preview bridge
When a finder is open in the dashboard builder’s Live Preview panel, the widget runs inside an <iframe> on a dedicated preview page instead of on a customer site. The parent frame needs to push real-time design changes into the iframe without a full page reload. This is the preview bridge: a custom DOM event contract between Next.js’s LivePreview.tsx and the widget’s App.svelte.
For the Next.js side of this contract — how LivePreview.tsx posts messages and what it debounces — see nextjs-to-external.md. This page covers only the widget’s receiver logic.
Boot-time preview detection (getPreviewBridgeState)
App.svelte:42–54 — On every widget boot, before any CDN fetch, the widget calls getPreviewBridgeState() to check whether it is running in preview mode:
// App.svelte:42–54
function getPreviewBridgeState() {
if (typeof window === 'undefined') {
return { mode: false, session: '', payload: null };
}
const params = new URLSearchParams(window.location.search);
return {
mode: Boolean(window.__LF_PREVIEW_MODE || params.get('previewSession')),
session: window.__LF_PREVIEW_SESSION || params.get('previewSession') || '',
payload: window.__LF_PREVIEW_PAYLOAD ?? null,
};
}The function checks two separate signal sources:
| Signal | Source | Set by |
|---|---|---|
window.__LF_PREVIEW_MODE | Global boolean | Parent frame injects before iframe load |
window.__LF_PREVIEW_SESSION | Global string (UUID) | Parent frame injects before iframe load |
window.__LF_PREVIEW_PAYLOAD | Global object | Parent frame injects before iframe load |
?previewSession= query param | URL search string | Preview page URL constructed by Next.js |
__LF_PREVIEW_MODE, __LF_PREVIEW_SESSION, and __LF_PREVIEW_PAYLOAD are intentionally undocumented in customer-facing docs — they are a private implementation contract between the dashboard and the widget runtime.
Boot-time payload hydration
App.svelte:228–258 — Inside boot() (called from onMount), the widget checks bridge.payload before attempting any CDN fetch:
// App.svelte:236–239
if (bridge.payload) {
applyPayload(bridge.payload, { isPreview: true });
return;
}If window.__LF_PREVIEW_PAYLOAD is present it is applied immediately — no pointer fetch, no versioned CDN fetch, no localStorage read. This covers the initial render when the builder opens the Live Preview panel.
If bridge.payload is null but bridge.mode is true and no finderKey is available, the widget throws 'Preview payload unavailable.' (App.svelte:241–243).
💡 Tip:
LivePreview.tsxalso persists the last-sent payload to SessionStorage so the preview page can seedwindow.__LF_PREVIEW_PAYLOADon load. The widget itself never reads SessionStorage — it only reads the global that the preview page injects before the Svelte app mounts.
Runtime update listener (lf-preview-payload)
App.svelte:203–225 — A custom DOM event listener is registered on window inside onMount and removed via the returned cleanup function:
// App.svelte:203–226
const handlePreviewPayload = (event) => {
const detail = event?.detail ?? {};
if (!detail?.payload) { return; }
if (previewSession && detail.previewSession && detail.previewSession !== previewSession) {
return;
}
previewMode = true;
previewSession = detail.previewSession || previewSession;
try {
applyPayload(detail.payload, { isPreview: true, preserveUiState: hasAppliedPayload });
} catch (err) {
error = err instanceof Error ? err.message : 'Failed to load finder.';
} finally {
loading = false;
}
};
window.addEventListener('lf-preview-payload', handlePreviewPayload);The event name is lf-preview-payload. The Next.js LivePreview.tsx component posts a postMessage to the iframe; the preview page’s finder-preview route translates that postMessage into a CustomEvent dispatched on window with a detail object of shape { payload, previewSession }. See nextjs-to-external.md for the translation layer.
⚠️ Warning: The event is dispatched on the widget’s own
window, not the parent frame’swindow. The widget never directly listens tomessageevents — the preview page acts as a trusted intermediary.
previewSession UUID validation
App.svelte:210–212 — If both the stored previewSession and the incoming detail.previewSession are non-empty, and they do not match, the event is silently dropped:
if (previewSession && detail.previewSession && detail.previewSession !== previewSession) {
return;
}This guards against stale events delivered after a session change (e.g., the user switches to a different finder in the same browser tab). Session UUIDs are generated by the dashboard on preview panel open; the widget receives the UUID from window.__LF_PREVIEW_SESSION or ?previewSession= at boot.
preserveUiState — hot-reload without full remount
App.svelte:218 — When handlePreviewPayload calls applyPayload, the second argument sets preserveUiState: hasAppliedPayload:
applyPayload(detail.payload, { isPreview: true, preserveUiState: hasAppliedPayload });hasAppliedPayload is true after the first successful applyPayload call (App.svelte:191). When preserveUiState is true, initializeFinderStores (stores.js:229–348) keeps the visitor’s current search query, open filter panel, radius, sort selection, and active location intact — only the design tokens and location data are updated from the new payload. See stores-and-state.md for the full preserveUiState branching in initializeFinderStores.
Behavioral changes in preview mode
When previewMode is true (set from bridge.mode at boot or from a received event):
- Analytics suppressed —
applyPayloadskips theanalytics.capture('app_loaded', ...)call (App.svelte:194–198):if (!isPreview) { analytics.capture(...); }. - CDN bypass — payload comes from
window.__LF_PREVIEW_PAYLOADor the event detail, never from the CDN fetch chain. - Origin allowlist not checked — domain authorization is enforced by the backend on the origin API endpoint, not the widget. The preview page runs on the dashboard domain, which is always authorized.
Related pages
- overview.md — widget architecture overview
- stores-and-state.md — how
preserveUiStatepropagates through stores - nextjs-to-external.md — LivePreview postMessage protocol (Next.js side)
- init-flow.md — boot sequence and
onMountstructure