Public Site

Preview Listener

Overview

Vextro's admin panel includes a live preview sidebar (VextroLivePreview) that embeds your public site in an iframe and broadcasts field values to it over postMessage as editors type. The vextro/preview subpath provides the frontend listener that your public site uses to receive and apply those updates in real time.

The preview system has two sides:

  • Admin side -- VextroLivePreview (built into the admin panel; no extra configuration needed)
  • Frontend side -- initVextroPreviewListener (add to your public Astro page or layout)

Quick Start

Add the listener to your preview page or layout:

---
// src/pages/posts/[slug].astro
---
<html>
<head>...</head>
<body>
  <h1 data-vextro-field="title">Default Title</h1>
  <p data-vextro-field="description">Default description</p>
  <img data-vextro-field="coverImageUrl" data-vextro-attr="src" />

  <script>
    import 'vextro/preview';
  </script>
</body>
</html>

When an editor types in the admin panel with preview open, the listener updates the DOM automatically.


initVextroPreviewListener

The underlying function that vextro/preview calls automatically on module load. Consumer pages should rely on auto-init and not call it directly.

Any import from vextro/preview -- named or side-effect -- triggers module auto-initialization. Calling initVextroPreviewListener() after that registers a second listener on window. Use the side-effect import and let auto-init handle registration:

// Correct: auto-init runs when the module loads
import 'vextro/preview';

The function is exported for advanced contexts where ES module side-effects are suppressed (SSR, test runners). In normal browser usage, rely on auto-init.

The listener attaches a message event handler to window. On each vextro-preview-update message it:

  1. Finds all elements with a data-vextro-field attribute matching a key in the incoming data
  2. Updates textContent by default, or updates the attribute named by data-vextro-attr if present
  3. Dispatches a custom vextro:preview-data event for advanced handling

DOM Attributes

data-vextro-field

Marks an element as a preview target. Set the value to the field name that should update it.

<!-- Updates textContent with the "title" field value -->
<h1 data-vextro-field="title">Placeholder</h1>

<!-- Updates textContent with the "excerpt" field value -->
<p data-vextro-field="excerpt">...</p>

data-vextro-attr

When present alongside data-vextro-field, the listener updates the named attribute instead of textContent.

<!-- Updates the src attribute with the "heroImageUrl" field value -->
<img data-vextro-field="heroImageUrl" data-vextro-attr="src" alt="" />

<!-- Updates the href attribute with the "ctaUrl" field value -->
<a data-vextro-field="ctaUrl" data-vextro-attr="href">Read more</a>

Custom Event

For fields that require more complex handling (rich text, arrays, nested structures), listen to the vextro:preview-data event on window. This fires for every incoming preview message regardless of data-vextro-field bindings.

import { renderTipTapToHtml } from 'vextro/content';
import type { VextroPreviewEventDetail } from 'vextro/preview';

window.addEventListener('vextro:preview-data', (e: CustomEvent<VextroPreviewEventDetail>) => {
  const { data, collectionSlug, documentId } = e.detail;

  // Manually update a rich text block with new content
  if (data.body) {
    document.querySelector('#body').innerHTML = renderTipTapToHtml(data.body);
  }
});

Event detail

type VextroPreviewEventDetail = {
  data: Record<string, unknown>;  // All field values from the admin form
  collectionSlug?: string;         // Collection the preview document belongs to
  documentId?: string;             // ID of the document being previewed
};

Admin Setup

The preview sidebar is built into the admin panel. To add a preview button to a collection's document editor, add preview to the collection's actions config:

// convex/collections/posts.ts
import { defineVextroCollection } from 'vextro';

export const posts = defineVextroCollection({
  slug: 'posts',
  // ...
  preview: {
    url: 'https://yoursite.com/posts/{slug}',
  },
});

The {slug} placeholder (or any field name in curly braces) is replaced with the current document's field value before the iframe loads. The {_id} placeholder resolves to the document ID.


Message Protocol

The admin panel sends messages in this shape. You do not need to handle this directly when using initVextroPreviewListener.

type VextroPreviewMessage = {
  type: 'vextro-preview-update';
  data: Record<string, unknown>;  // Field values; _collectionSlug and _id are nested here
};

Security

The listener accepts messages from any origin because preview may be served from a different domain than the admin panel. The vextro:preview-data custom event detail does not expose the message origin. If your security model requires origin validation, add a native window.addEventListener('message', handler) listener and check event.origin there -- the native MessageEvent always carries the sender's origin.

initVextroPreviewListener only updates the DOM -- it never writes data back to Convex or the admin panel. It is safe to include in production builds; the listener is a no-op when no admin preview message is received.

Previous
Content Rendering