Skip to main content

🎯 Payload Better Preview – Block Sync in Live Editor

Payload Better Preview is a plugin that provides bi-directional synchronization between the Payload admin editor and the live preview panel. Click a block in the admin, and the preview scrolls to it (and vice versa). Hover over blocks to see their type, index, and name.


How it works

The system uses Payload's native live preview with an overlay that highlights blocks as you hover or click on them. Block synchronization works through:

  • Admin → Preview: Click a block in the admin editor → preview scrolls to that block
  • Preview → Admin: Click a block in the preview → admin editor jumps to that block (expands collapsed rows if needed)
  • Hover highlighting: See block type, index, and name badges in the preview

What's implemented

✅ Plugin registration

The plugin is registered in payload.config.ts:

import { betterPreview } from 'payload-better-preview'

export default buildConfig({
plugins: [
betterPreview(), // Auto-configured with defaults
],
// ...
})

✅ Frontend components

Two key client components handle the sync:

  1. <LivePreviewListener /> (in src/components/LivePreviewListener/index.tsx)

    • Renders <BetterPreview /> component
    • Only active when page is in draft mode inside Payload preview iframe
    • Handles auto-tagging of nested blocks
  2. <RenderBlocks /> (in src/blocks/RenderBlocks.tsx)

    • Adds required data-block* attributes to all block wrappers
    • Supports field prop to identify which Payload field the blocks come from

✅ Data attributes

Each rendered block has these attributes:

<section
data-block="hero" // Block type (matches Payload blockType)
data-block-index="0" // 0-based index in the blocks array
data-block-field="layout" // Payload field name (e.g., "layout", "hero", "sidebar")
data-block-name="Hero Dark" // Optional display name
>
{/* block content */}
</section>

Block structure

Standard blocks (in layout array)

// In page.tsx:
<RenderBlocks
blocks={page.layout} // Array of blocks
field="layout" // Payload field name
locale={locale}
/>

Hero block (single, not in array)

// In page.tsx:
<section
data-block="hero"
data-block-index="0"
data-block-field="hero" // Hero is its own field, not in layout array
data-block-name={hero.style}
>
<RenderHero {...hero} />
</section>

Nested blocks

For blocks that contain other blocks (future), the field path is constructed by concatenating:

parent-field-parent-index-child-field

Example:

  • Parent: layout-0 (first block in layout array)
  • Child content inside: layout-0-content
  • Grandchild: layout-0-content-1-sidebar

Configuration options

The plugin supports custom options (currently using defaults):

betterPreview({
accentColor: '#3b82f6', // Highlight color (default: blue)
scrollAlign: 'start', // Alignment when scrolling (start | center | end)
scrollOffset: 128, // Top offset in px (for sticky headers)
})

How to use in Live Preview mode

  1. Open a page in Payload admin
  2. Click "Preview" button (top-right corner) → opens live preview panel
  3. Click any block in the preview → admin editor scrolls to that block's row
  4. Click any block row in the admin → preview scrolls to that block and flashes
  5. Hover over blocks → see overlay with block type, index, name

Debugging block sync issues

Blocks don't sync?

  1. Check draft mode: Live preview only works in draft mode (draftMode() enabled)
  2. Verify data attributes: Open DevTools → Elements → check if block has:
    • data-block (blockType)
    • data-block-index (numeric index)
    • data-block-field (field name from Payload)
  3. Check browser console: Errors related to postMessage or sync events?
  4. Restart dev server: Sometimes iframe caching issues occur

Browser console error: "postMessage failed"?

  • This usually means the preview iframe is on a different origin
  • In multi-tenant setup, make sure preview URL uses the same origin as the main app

Hover highlighting not showing?

  • Make sure <BetterPreview /> is rendered in the page layout
  • Check that LivePreviewListener is in src/app/[tenant]/[locale]/(frontend)/[...slug]/page.tsx
  • Verify <BetterPreview /> is only rendered when draft && isInIframe

Customization

Add preview to other collections

  1. Add livePreview config to your collection (like Pages):

    // collections/MyCollection.ts
    {
    admin: {
    livePreview: {
    url: ({ data, req }) => {
    // Return preview URL based on your routing
    return `https://mysite.com/preview?id=${data.id}`
    }
    }
    }
    }
  2. Ensure your preview page includes <LivePreviewListener>

  3. Add data-block* attributes to all preview components

Custom accent color

// In payload.config.ts:
betterPreview({
accentColor: '#10b981', // Green instead of blue
})

Browser support

  • ✅ Chrome/Chromium 90+
  • ✅ Firefox 88+
  • ✅ Safari 14+
  • ✅ Edge 90+

Works on desktop and tablet (requires modern browser with postMessage and MutationObserver support).


Known limitations

  • No preview for draft-only fields: Live preview only syncs published/draft content
  • Nested blocks: Full support requires proper data-block-field path construction
  • Custom richText: Lexical richText blocks require additional setup (not yet implemented)

Resources