Visual Editor

The visual editor activates on the public site for logged-in admins via a floating "Edit page" button. Click any block to open a resizable drawer with full field editing, live preview, drag-reorder, insert, delete, and duplicate — without leaving the front end.

Overview

The visual editor is a Sanity-style overlay that activates on the public site for logged-in admins. While anyone else sees the rendered page exactly as it ships, an authenticated admin gets a floating Edit page button in the corner; clicking it switches the page into edit mode. Each rendered block gains a hover outline and an edit handle, and clicking any block opens a resizable side drawer with full field forms for that block's schema.

Edits are previewed live — typing into a field re-renders just that block via POST /admin/api/block-types/preview and swaps its DOM range in place — and committed with a single Save button that fires PATCH /admin/api/nodes/{id} with the full updated blocks_data. No new write endpoints were added for the editor. It speaks the same admin API as the regular node editor; the value is in the UX, not the contract.

Capabilities

  • Block outlines — hover borders positioned with requestAnimationFrame; zero scroll lag.
  • Drawer panel — resizable from the left edge; the chosen width is persisted in localStorage.
  • Field support — all 21 canonical kernel field types: string, textarea, richtext (Tiptap), number, range, email, url, date, color, toggle, select, radio, checkbox, image, gallery, file, link, reference, term, object (collapsible), and array (drag-reorderable rows). Extension-contributed field types are also rendered — the same registry the regular admin uses.
  • Media picker — image, gallery, and file fields open the full media library inline; uploads, search, and crop selection all work without leaving the page.
  • Live preview — typing into any field re-renders the block server-side and swaps the rendered HTML into the page DOM. No reload, no flicker.
  • Insert blocks — an + Add block button above the outline plus inline gap buttons between blocks. New blocks render immediately on the page; the overlay assigns synthetic marker indices >= 100,000 to placeholders so they cannot collide with real kernel block indices until the save round-trip renumbers them.
  • Delete & duplicate — actions materialise on the live page; no save needed to see the result.
  • Drag-reorder — powered by dnd-kit; reordering blocks updates the rendered layout in real time.
  • Save — a single PATCH commits all pending changes at once, atomically. Cancelling closes the drawer and resets the page to the last saved state.

How it works

The extension ships a compiled Go plugin (bin/visual-editor) plus a plain JS bundle (editor-ui/dist/editor.js). It declares two responsibilities in its manifest:

  • Capabilities: events:subscribe, events:emit, log:write — minimal. The editor writes nothing to the database; saves go through the existing admin node API and inherit its capability checks.
  • Subscription: the plugin subscribes to the kernel's render.body_end event. When a public page renders for a logged-in admin, the kernel fires that event and the plugin returns a <script> tag pointing at editor.js. For anonymous visitors, the kernel skips the event payload — anonymous traffic sees no editor code at all.

On the client, editor.js:

  1. Reads the HTML-comment markers the kernel wraps each rendered block in. The exact format (in internal/cms/public_handler_editor_markers.go) is <!--squilla:block:start:<index>:<type-slug>--> ... <!--squilla:block:end:<index>-->. Those markers map DOM ranges back to blocks_data indices so the overlay knows which block lives where.
  2. Overlays the edit UI on top of the live page using position: fixed elements positioned with requestAnimationFrame. The overlay does not touch the rendered markup; it sits above it.
  3. Talks to the plugin's HTTP handler at /admin/api/ext/visual-editor/* for block-preview rendering and field-type metadata, and to the kernel's /admin/api/nodes/{id} for saves.
SECURITY
The overlay is injected only when the request is authenticated as an admin. Anonymous visitors see no editor code at all — the kernel skips the script tag because render.body_end only delivers the editor's response when the session check passes. There is no "hidden if not editing" client-side branch to bypass.

Enabling the visual editor

Activate the extension from Admin → Extensions or via MCP. auto_activate is true in the manifest, so a fresh install activates it on first boot.

core.extension.activate({ slug: "visual-editor" })

Theme requirements

None. The kernel wraps block output in HTML-comment markers automatically when the visual-editor extension is active, regardless of which theme is loaded — themes do not need to opt in or expose a hook. Deactivating the extension removes both the markers and the injected script in the same render pass.

Limits and caveats

  • The editor is for blocks, not chrome. Layouts, partials, and the page header/footer are not editable inline — use the regular admin Layout Editor for those.
  • Drafts vs published. Saves write to the same blocks_data as regular admin edits; if the node is published, your save is live immediately. Switch the node to draft first if you want to iterate without exposing intermediate states.
  • Concurrency. The kernel does not lock during edits. Two admins editing the same node simultaneously will overwrite each other's last-write-wins. Coordinate out of band, or use draft mode.