Preview and live editing
Editors preview and edit on your real site, so a draft looks exactly like the published page. There are two levels:
- Preview: the editor opens your site with a token, and your site renders the draft.
- Live editing: the editor shows your site in an iframe, sends every keystroke as an updated document, and lets editors click blocks on the page to edit them.
@avvyr/cms-nuxt has both built in. On other frameworks you implement the parts below.
Preview
Set the site's preview URL in the app (site settings) or with previewUrl on PATCH /v1/cms/sites/{siteId}. Usually it is the site's root URL. When an editor clicks preview, the editor opens:
{previewUrl}?entryId={entryId}&path=%2Fom-oss&locale=sv&token={token}
On your server, read the draft with the preview key and pass the token on:
curl "https://api.avvyr.com/v1/content/{siteId}/draft/sv/pages/om-oss" \
-H "x-cms-key: cms_prv_…" \
-H "x-cms-preview-token: {token}"
const page = await cms.draftPage('/om-oss', { locale: 'sv', previewToken: token })
Draft routes return the same document as the published routes, with draft: true and Cache-Control: private, no-store. References resolve against draft paths.
- Never cache draft responses, and never send the preview key to the browser.
- Mark preview pages
noindex. - A token is valid for one hour. After that the draft routes return
401 CMS_PREVIEW_TOKEN_INVALID; the editor makes a new one each time.
Live editing
The editor in the Avvyr app loads your preview URL in an iframe with two extra parameters, cmsEditor=1 and cmsOrigin={editor origin}, and talks to the page with postMessage.
Let the app frame your site
Send this header on HTML responses, and no X-Frame-Options: DENY:
Content-Security-Policy: frame-ancestors 'self' https://app.avvyr.com
Start the bridge only when it is safe
Start listening only when all of these hold:
- the URL has
tokenandcmsEditor=1; - the page is framed (
window.parent !== window); cmsOriginis on your own allow-list (defaulthttps://app.avvyr.com).
Then accept messages only from window.parent with that exact origin, and only with the envelope { source: "avvyr-cms", v: 1 }.
Messages
Every message is { source: "avvyr-cms", v: 1, type, ...payload }.
| From the editor | What your site does |
|---|---|
update { document } | Replace the rendered document in place, without reloading. Sent about every 250 ms while someone types. |
highlight { blockId | null } | Outline that block and scroll it into view. |
reload | Reload the page. |
| To the editor | When |
|---|---|
ready { path, locale, entryId? } | After the page has mounted, and after every route change |
hover { blockId | null } | The pointer moves onto another block |
select { blockId } | A block is clicked. Stop the click there, so links and buttons inside it do nothing. |
navigate { path } | An internal link is clicked. Do not navigate; the editor opens that page. |
To find blocks, wrap each rendered block in an element with data-cms-block="{block id}" and data-cms-type="{block type}". With nested blocks, the innermost one wins.
In Nuxt
@avvyr/cms-nuxt does all of this. While ?token= is in the URL, useCmsPage loads the draft through the server and adds noindex. The live editing bridge starts on its own under the conditions above, and <CmsBlocks> renders the data-cms-block wrappers. Configure which editors may frame the site:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@avvyr/cms-nuxt'],
cms: {
editorOrigins: ['https://app.avvyr.com'],
},
routeRules: {
'/**': { headers: { 'Content-Security-Policy': "frame-ancestors 'self' https://app.avvyr.com" } },
},
})
Setting editorOrigins replaces the default, so list every origin you edit from. See Nuxt layer.