Skip to content

Visual editor

Edit content on the page itself: preview tokens, editable() and the bridge script.

DocsContent

Docs menuVisual editor

The visual editor opens your site inside Manifolio with the drafts in place. Editors click a heading and type, pick a new photo from the media library, or jump to a field in the side panel. Design stays in your code: the editor changes content, never layout or styles.

It takes three things on the site: read drafts when the page carries a preview token, mark the elements that show a field, and load the bridge script in preview.

How it works

  1. In the admin, Visual editor (in an entry or in the menu) asks the API for a preview token: signed, valid for 30 minutes, scoped to one project and able only to read drafts.
  2. The admin opens your preview URL in an iframe with ?mf_preview=<token> added.
  3. Your server sees the token, reads drafts through the Preview API and renders the page as usual, with data-mf-edit attributes and the bridge script.
  4. The bridge talks only to the Manifolio admin that framed the page. Edits go back to the admin, which saves them as drafts (autosave, conflicts and versions work as in the entry editor). Publishing stays a deliberate click.

1. Read drafts with the token

TypeScript
import { createClient, previewTokenFrom } from "@manifolio/sdk";

export function cmsFor(request: Request) {
  const token = previewTokenFrom(request.url);   // null on normal visits
  return token
    ? createClient({ project: PROJECT_ID, preview: token })        // drafts, no key needed
    : createClient({ project: PROJECT_ID, key: MANIFOLIO_KEY });   // published content
}

Preview responses must not be cached. Skip your CDN or data cache whenever the token is present and send Cache-Control: private, no-store.

The token cannot publish, cannot write and cannot read another project. When it expires, the admin sends the bridge a fresh one, so navigation inside the editor keeps working.

2. Mark editable elements

Astro
---
import { editable } from "@manifolio/sdk";
const page = await cms.entry("page", "about");
---
<h1 {...editable(page, "title")}>{page.title}</h1>
<p {...editable(page, "lead")}>{page.lead}</p>
{page.sections.map((s, i) => (
  <h2 {...editable(page, ["sections", i, "title"])}>{s.title}</h2>
))}
<img {...editable(page, "hero")} src={imageUrl(page.hero, { w: 1600 })} alt={page.hero.alt} />

editable(entry, field, locale?) returns one attribute, data-mf-edit, with the entry type and id, the field path and the language. Lists and groups take a path: ["sections", 2, "items", 0, "title"] or "sections.2.items.0.title". The attribute is harmless on a public page; to keep public HTML clean, use client.editable(...), which returns nothing unless the client was created with a preview token.

What a click does depends on the field:

FieldIn the page
Text, slug, URL, emailedited in place; Enter saves, Esc cancels
Long textedited in place; ⌘ Enter saves, Enter adds a line
Long text rendered as Markdown, formatted textthe panel opens on that field
Mediathe media library opens; the picked file replaces the old one
Anything elsethe panel opens on that field

Typing in the panel updates text on the page instantly. After a save, photos and formatted text refresh in place; adding or reordering list items reloads the page and keeps the scroll position.

Sections (blocks)

A blocks field keeps the sections of a page; each block has _type and a stable _key. Mark the whole section with the block itself in the path, and the fields inside it the same way:

Astro
{page.sections.map((b) => (
  <section {...editable(page, ["sections", b])}>
    <h2 {...editable(page, ["sections", b, "title"])}>{b.title}</h2>
  </section>
))}

The section gets data-mf-block: hovering it in the editor outlines it and shows a bar to move it up or down, duplicate or delete it, and a + above and below that opens the section menu in the panel. The path uses the block key (sections[a1b2c3d4e5].title), so it survives reordering.

3. Load the bridge in preview

HTML
<script defer src="https://api.manifolio.com/v1/bridge.js" data-admin="https://app.manifolio.com"></script>

Add it only when the token is present. The script is under 4 KB gzipped, has no dependencies and does nothing when the page is not inside the Manifolio admin: it checks the parent window and the admin origin on every message, in both directions. It uses no eval, injects no <style> and works under a strict Content Security Policy. With npm, import { startBridge } from "@manifolio/sdk/bridge" does the same.

Preview URL

Set it in Settings → Visual editor. Manifolio fills in {slug}, {type} and {locale} for the entry being edited:

HTTP
https://example.com/{locale}/{slug}
https://example.com/preview?type={type}&slug={slug}&locale={locale}

The second form points at a small route on your site that redirects each type to its page, keeping mf_preview. Redirects must stay on the same origin. The address must use https; http works only on localhost.

Framing and headers

Browsers block framing when a site sends X-Frame-Options or a frame-ancestors rule that leaves the admin out. On preview responses, allow the admin and nothing else:

http
Content-Security-Policy: frame-ancestors 'self' https://app.manifolio.com
Cache-Control: private, no-store
Referrer-Policy: same-origin

Remove X-Frame-Options on those responses, or keep it on normal pages only. Analytics scripts can stay off in preview so editor visits are not counted.

Limits in this version

  • Formatted text is edited in the panel, not inside the page.
  • Sections change through the editor’s autosave; the page reloads after the save to show the new order.
  • Phones show a “use a larger screen” message: the editor needs the page and the panel side by side.