🎯 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:
-
<LivePreviewListener />(insrc/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
- Renders
-
<RenderBlocks />(insrc/blocks/RenderBlocks.tsx)- Adds required
data-block*attributes to all block wrappers - Supports
fieldprop to identify which Payload field the blocks come from
- Adds required
✅ 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
- Open a page in Payload admin
- Click "Preview" button (top-right corner) → opens live preview panel
- Click any block in the preview → admin editor scrolls to that block's row
- Click any block row in the admin → preview scrolls to that block and flashes
- Hover over blocks → see overlay with block type, index, name
Debugging block sync issues
Blocks don't sync?
- Check draft mode: Live preview only works in draft mode (
draftMode()enabled) - 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)
- Check browser console: Errors related to
postMessageor sync events? - 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
LivePreviewListeneris insrc/app/[tenant]/[locale]/(frontend)/[...slug]/page.tsx - Verify
<BetterPreview />is only rendered whendraft && isInIframe
Customization
Add preview to other collections
-
Add
livePreviewconfig to your collection (like Pages):// collections/MyCollection.ts{admin: {livePreview: {url: ({ data, req }) => {// Return preview URL based on your routingreturn `https://mysite.com/preview?id=${data.id}`}}}} -
Ensure your preview page includes
<LivePreviewListener> -
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-fieldpath construction - Custom richText: Lexical richText blocks require additional setup (not yet implemented)