Documentation
Visual editing & live preview
The entry editor can render your real site side-by-side with the form and stream draft edits into it live — unsaved changes, personalization variants and all. Your site opts in with a small postMessage listener; no SDK required.
1. Configure the collection's preview
In the collection's settings, set:
| baseUrl | Your site's origin, e.g. https://www.example.com. Must be http(s). |
| pathTemplate | The page path, composed from entry fields with {fieldApiName} tokens — e.g. /blog/{slug}. Defaults to /{slug}. |
| slugField | Which field is the entry's slug (default slug). Also used by the public API's get-by-slug endpoint. |
The editor then loads baseUrl + pathTemplate in an iframe with ?preview=1 appended. Open in new tab opens the same URL without the flag — the published page.
Turn it on from the entry editor's ⋮ (More actions) menu → Visual editing. A preview pane opens beside the form; the same menu item (now Hide preview) closes it.
2. The bridge protocol
Two message types, both plain postMessage:
| gluso:preview-ready | Your page → editor, once your listener is set up. Tells the editor to (re)send the current draft. |
| gluso:preview | Editor → your page, on ready and debounced (~250 ms) after every edit. Carries { status, data } where data is the draft's field values keyed by API name. |
When personalization is enabled, data arrives with the editor's currently-selected variant combination already resolved — flipping variants in the editor re-renders your page with that audience's values, using the same fallback rules as production (see Variants & fallbacks).
3. Implement the listener
On your page, when ?preview=1 is present: announce readiness, then re-render from each message. Framework-agnostic version:
// Runs on your page when it's loaded by the editor with ?preview=1
const GLUSO_ORIGIN = 'https://your-gluso-app.com'
if (new URLSearchParams(location.search).has('preview')) {
window.addEventListener('message', (event) => {
if (event.origin !== GLUSO_ORIGIN) return
if (event.data?.type !== 'gluso:preview') return
// event.data.data = draft field values keyed by API name
render(event.data.data)
})
// Ask the editor to send the current draft
window.parent.postMessage({ type: 'gluso:preview-ready' }, GLUSO_ORIGIN)
}In a component framework, feed the message into state and let the page re-render — a complete React hook:
// lib/usePreview.ts
'use client'
import { useState, useEffect } from 'react'
const GLUSO_ORIGIN = 'https://your-gluso-app.com'
export function usePreview<T>(initial: T): T {
const [data, setData] = useState<T>(initial)
useEffect(() => {
if (!new URLSearchParams(location.search).has('preview')) return
const onMessage = (event: MessageEvent) => {
if (event.origin !== GLUSO_ORIGIN) return
if (event.data?.type === 'gluso:preview') setData(event.data.data)
}
window.addEventListener('message', onMessage)
window.parent.postMessage({ type: 'gluso:preview-ready' }, GLUSO_ORIGIN)
return () => window.removeEventListener('message', onMessage)
}, [])
return data // published data initially; live draft while previewing
}Same idea everywhere: Astro needs a client:load island (or inline script) since preview data is inherently client-side; in Nuxt/Vue put the listener in onMounted and write into a ref. For Next.js App Router, the hook must live in a 'use client' component that the server page passes data into — see the Next.js guide.
Security notes
- Always check
event.originagainst your gluso origin before trusting a message — anyone can iframe your preview URL. - The editor does the same in reverse: it only posts to and accepts messages from the configured
baseUrlorigin. - Preview data is draft content. If drafts are sensitive, gate the preview route (e.g. behind a token or auth) — the bridge works the same either way.
- Your site must be allowed to render inside an iframe from the gluso origin — if you set
X-Frame-Optionsor aframe-ancestorsCSP, add your gluso app's origin (scoped to the preview route is fine).
Component & section pages
Pages built from component fields — a hero, an array of stacked sections, a sidebar panel — preview live with no extra work. The whole page's draft data is what streams over the bridge, so data.heroSection, data.sections[] and the rest arrive on every edit. Render each block by its componentTypeId and editing any nested field re-renders just that section. See the section-registry pattern in the Next.js guide.
What updates when
- Typing in any field — including a nested component/section field → a fresh
gluso:previewafter the ~250 ms debounce. - Switching the previewed variant combination → resolved data for that combo.
- Saving the draft → the editor reloads the iframe so server-rendered parts refresh too.
- Publishing → the live page (no
preview=1) picks it up through your normal data fetching.