Engineering Notes · Architecture

Reading time · 9 min

Building a Visual Editor Around Portable Code

How StackLiberate edits websites through a cross-origin bridge and typed patches without entangling editor machinery in the exported artifact.

A visual editor communicating with a sandboxed website preview through a typed message bridge, with patches flowing toward an independent exported artifact.

Visual editors have a natural tendency to become entangled with the pages they modify.

The editor inserts selection markers. It registers event listeners. It wraps elements in containers that make positioning easier to compute. It adds attributes, data annotations, and style overrides that support the editing experience.

None of that belongs in the finished website.

When the editing environment and the published artifact share the same representation, separating them cleanly becomes a deferred problem — one that grows more difficult as the editor gains capabilities.

StackLiberate approaches this differently. The editor never modifies the website's source HTML directly. Instead, it operates through a communication layer that projects editing intent onto a sandboxed view of the page, while the canonical state remains an ordered set of typed operations applied against stable identifiers.

The editor exists around the code. It does not live inside it.

The iframe boundary is architectural, not cosmetic

The StackLiberate editor renders the user's website inside a cross-origin iframe served from a separate subdomain.

This is not a convenience choice for CSS isolation. It is a security and architectural boundary that enforces a discipline: the parent application cannot reach into the iframe's DOM.

No contentDocument access. No direct node manipulation. No shared JavaScript scope.

Every interaction between the editor and the rendered page passes through a typed postMessage bridge. The parent sends commands — highlight this element, apply this text change, query this selector — and the iframe responds with structured results.

This constraint forces all DOM-level operations to be explicit, serializable, and auditable.

A direct-access editor can silently accumulate side effects: event listeners that persist across operations, style mutations that were never recorded, elements that exist in the DOM but not in any saved state. The postMessage boundary prevents that category of error by requiring every cross-boundary action to be a defined message with a defined response.

The bridge is the editor's only hand

The bridge script is a small, purpose-built program injected into the iframe. It is the only JavaScript that runs inside the preview page.

All user-authored scripts are stripped before the page is rendered in the editor. The bridge is the sole active agent inside the frame.

It serves several roles:

  • Listens for pointer events and reports element identity to the parent.
  • Applies visual projections (selection highlights, hover indicators) without mutating the underlying HTML.
  • Executes DOM queries on behalf of the parent (reading text content, computing bounding boxes, enumerating elements).
  • Applies patch projections so the user sees the effect of their edits in real time.
  • Reports scroll position and viewport dimensions for overlay alignment.

The bridge does not decide what happens. It executes requests from the parent and reports observations back. The parent holds the editing logic, undo stack, and persistence layer. The bridge holds the rendering surface.

This separation means the editor's intelligence lives where it can be tested, versioned, and reasoned about independently of the browser's rendering engine.

Stable identity makes structured editing possible

A visual editor that records changes as "replace the innerHTML of the node at this DOM path" is fragile.

Paths break when sections are reordered. Indexes shift when repeater items are added. Query selectors match different elements when the page structure changes for responsive layouts.

StackLiberate assigns each editable element a deterministic data-sl-id — a content-addressable hash derived from the element's position in the authored structure. These identifiers are baked into the stored HTML at crawl or generation time, not invented at runtime by the bridge.

Every patch references its target by this stable identifier, with a path-based selector as a fallback for resilience.

This means:

  • Patches survive page reloads without recomputation.
  • Patches remain valid after unrelated sections are reordered.
  • Patches compose: multiple operations against the same element produce a predictable combined result.
  • Version restore replays patches against the same identifiers the user originally targeted.
  • Export applies patches mechanically — there is no heuristic matching involved.

The identifiers are part of the website's internal representation, not the editor's transient state. They persist in R2 alongside the HTML itself.

Patches are the unit of change

Every user edit — changing text, replacing an image, modifying a style, hiding a section, updating a link — is recorded as a typed patch object.

A patch declares:

  • The operation type (text, image, style, link, hide, attr, duplicate-item, remove-item).
  • The target element by data-sl-id.
  • The specific change (new text content, new src, CSS properties to modify, visibility state).

The original HTML is never mutated in storage. Instead, the current state of the website is always:

current state = original HTML + ordered patches

This has several consequences that matter for portability:

Deterministic replay. Given the same base HTML and the same patch list, any system that understands the patch schema will produce the same output. The editor, the export pipeline, and the preview system all share this contract.

Non-destructive editing. The original crawled or generated HTML remains intact. Users can remove patches, reorder them, or restore earlier versions without needing to reverse-engineer what the HTML looked like before a change.

Bounded operations. A text patch cannot accidentally inject a script. A style patch cannot modify element structure. The type system constrains what each operation can express, and a validation layer enforces those constraints before persistence.

Export independence. The export pipeline does not need the editor to be running. It applies patches to HTML using Cheerio on the server, resolves asset references, and produces a standalone directory of files.

Five layers between intent and persistence

Not every patch the editor produces should be stored. User input can contain invalid HTML, disallowed attributes, content that violates safety rules, or operations that target nonexistent elements.

StackLiberate validates patches through five layers before they reach persistent storage:

  • Operation allowlist. Only declared patch types are accepted. Unknown operations are rejected at the schema level.
  • Content safety. Text patches are checked for prohibited content patterns.
  • DOMPurify sanitization. Any HTML fragment within a patch is sanitized to remove dangerous elements and attributes.
  • Attribute blocklist. Event handlers (on*), srcdoc, and javascript: URLs are stripped regardless of context.
  • Export-time sanitization. Even after patches are stored, the export pipeline applies a final sanitization pass to guard against any content that bypassed earlier checks.

This layered approach means that the portable output is validated at both write time and read time. The exported website cannot contain content categories that the validation pipeline is designed to exclude, regardless of how patches were produced — manually through the visual editor or programmatically through the AI editing interface.

AI edits are patches, not magic

The AI editing feature in StackLiberate allows users to describe changes in natural language. The underlying implementation produces the same patch types as manual editing.

When a user asks the AI to "make the headline shorter" or "change the button color to blue," Claude receives the current HTML (via the bridge), identifies target elements by their data-sl-id, and generates patches that express the requested change.

Those patches then pass through the same five-layer validation pipeline as any manually created patch.

This architectural choice matters because it means AI-generated changes do not bypass the safety or portability guarantees of the system. An AI edit is not a privileged operation that can reach parts of the page that manual editing cannot. It is a convenience layer that produces the same bounded operations.

The AI cannot inject scripts, cannot modify elements outside the declared editable surface, and cannot produce output that would be stripped at export time. Its power is expressed entirely within the same constraints that apply to a user clicking and typing in the visual editor.

The editor sees a projection, not the truth

What the user sees in the editing canvas is not identical to what will be exported.

The editor view includes:

  • Selection highlights on the currently targeted element.
  • Hover indicators showing available edit targets.
  • The bridge script itself.
  • Temporarily applied patches that may not yet be saved.
  • Development-specific asset paths that differ from production URLs.

The exported website includes none of those things.

This difference is intentional, not an inconsistency to be eliminated. The editor's job is to show the user what the website will look like with their changes applied. It does this by projecting patches onto the rendered page in real time. But the projection exists only in the iframe — it never contaminates the stored representation.

The parity contract is:

Content, structure, styles, assets, navigation, section order, and visibility must be consistent between the editor projection and the exported artifact.

Editing instrumentation, selection state, development tooling, and bridge mechanics are explicitly excluded from that contract.

Why this matters for portability

The design ensures that the exported website never depends on the editor having been present.

There are no orphaned event listeners. No selection markers left in the markup. No invisible containers that were added for layout computation. No scripts that need to run for the page to render correctly. No bridge. No postMessage. No iframe.

The export pipeline receives the base HTML and the ordered patch list. It applies patches using a server-side DOM library. It resolves asset URLs to local paths. It writes files. The result is a directory of static web assets that any standard HTTP server can deliver.

The architecture does not merely allow portability. It makes non-portability difficult to introduce accidentally.

Every capability added to the editor must be expressible as a typed patch. Every patch must target a stable identifier. Every patch must pass validation. Every validated patch must produce the same result whether applied in the browser or on the server.

If a proposed feature cannot meet those constraints, it does not ship.

That discipline is the cost of building a visual editor that does not become part of the website it edits.