@bespokecode/payloadcms-zen-editor
v0.1.1
Published
A visual block editor for Payload CMS — edit native Payload blocks in a fullscreen, Elementor-style UI with live preview.
Maintainers
Readme
@bespokecode/payloadcms-zen-editor
A visual, fullscreen block editor for Payload CMS. Lets editors open any document in a side-by-side "Zen" view: a live preview iframe on the right and a drag-sortable block list with autosave and publish controls on the left. Native Payload blocks — no lock-in, no custom data format.
Features
- Fullscreen editing UI accessible from the standard Payload document toolbar
- Live-preview iframe with desktop / tablet / mobile breakpoints
- Drag-and-drop block reordering
- Add / duplicate / delete blocks via a block picker drawer
- Autosave on every change; explicit Publish button
- Click-to-select blocks in the preview (requires
ZenEditablewrapper — see below) - Zero host-project dependencies — ships its own
@dnd-kit/*andlucide-react
Requirements
- Payload CMS 3.x
- Next.js 15 or 16
- React 19
Installation
pnpm add @bespokecode/payloadcms-zen-editorSetup
1. Payload config
Add the plugin to your payload.config.ts and tell it which collections and fields to manage:
// payload.config.ts
import { zenEditorPlugin } from '@bespokecode/payloadcms-zen-editor'
import { buildConfig } from 'payload'
export default buildConfig({
// ...
plugins: [
zenEditorPlugin({
collections: {
// key = collection slug, value = per-collection options
pages: { blocksField: 'layout' },
posts: { blocksField: 'content' },
},
}),
],
})The plugin injects an "Open Zen Editor" button into the document toolbar and registers a /zen view route for each enabled collection. No changes are made to your collection schema.
Live preview URL (required for the preview pane)
The Zen editor resolves the preview URL from the collection's admin.livePreview.url setting. Configure it the same way you would for Payload's built-in live preview:
// collections/Pages.ts
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
livePreview: {
url: ({ data, req }) =>
`${process.env.NEXT_PUBLIC_SERVER_URL}/pages/${data.slug}`,
},
},
// ...
}If livePreview.url is not set the preview pane will be blank, but editing still works.
2. Frontend wiring
Two pieces are needed in your Next.js frontend.
ZenEditorBridge — root layout (once)
Mount this component once in your root layout. In normal browsing it renders nothing. When the page is loaded inside the Zen editor iframe it activates the postMessage bridge.
// app/layout.tsx
import { ZenEditorBridge } from '@bespokecode/payloadcms-zen-editor/client'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<ZenEditorBridge />
</body>
</html>
)
}ZenEditable — block renderer
Wrap each block in your block renderer with ZenEditable. Pass the block's position index and its blockType. This enables click-to-select in the preview pane and shows hover/selection overlays.
// components/RenderBlocks.tsx
import { ZenEditable } from '@bespokecode/payloadcms-zen-editor/client'
export const RenderBlocks: React.FC<{ blocks: Block[] }> = ({ blocks }) => (
<>
{blocks.map((block, index) => {
const Block = blockComponents[block.blockType]
if (!Block) return null
return (
<ZenEditable key={block.id ?? index} index={index} blockType={block.blockType}>
<Block {...block} />
</ZenEditable>
)
})}
</>
)API
zenEditorPlugin(config) — server
| Option | Type | Description |
|---|---|---|
| collections | Record<string, { blocksField: string }> | Map of collection slugs to their config. blocksField is the name of the blocks array field (e.g. 'layout'). |
<ZenEditable> — client
| Prop | Type | Description |
|---|---|---|
| index | number | Zero-based position of this block in the blocks array. |
| blockType | string | The block type string (matches block.blockType). |
| children | ReactNode | Your rendered block markup. |
<ZenEditorBridge /> — client
No props. Mount once in the root layout.
useZenMode() — client hook
Returns { mode: 'inactive' | 'active' }. Can be used to conditionally hide UI elements that should not appear inside the editor preview.
import { useZenMode } from '@bespokecode/payloadcms-zen-editor/client'
export function Navigation() {
const { mode } = useZenMode()
if (mode === 'active') return null
return <nav>...</nav>
}Workspace / monorepo usage
When consumed as a pnpm workspace package the top-level exports in package.json point directly to the TypeScript source (./src/...). Your Next.js bundler compiles it on-the-fly — no build step needed during development.
At publish time pnpm applies the publishConfig.exports overrides, which point to the compiled dist/ output. This dual-state setup means workspace consumers always get the latest source while npm consumers get a pre-compiled package.
Note:
publishConfig.exportsoverride is supported by pnpm 9+ and npm 7+. If you are using an older package manager you may need to swap theexportsfield manually before publishing.
License
MIT — see LICENSE.
