scorpio-99/payload-better-preview

GitHub: scorpio-99/payload-better-preview

为 Payload CMS 提供增强版实时预览体验,支持悬停高亮、区块标识以及后台编辑器与前端预览之间的双向滚动同步。

Stars: 9 | Forks: 1

# payload-better-preview [![npm](https://img.shields.io/npm/v/payload-better-preview?logo=npm&color=ce421b)](https://www.npmjs.com/package/payload-better-preview) [![downloads](https://img.shields.io/npm/dt/payload-better-preview?logo=npm&color=ce421b)](https://www.npmjs.com/package/payload-better-preview) [![issues](https://img.shields.io/github/issues/scorpio-99/payload-better-preview?logo=github&color=blue)](https://github.com/scorpio-99/payload-better-preview/issues) [![stars](https://img.shields.io/github/stars/scorpio-99/payload-better-preview?logo=github)](https://github.com/scorpio-99/payload-better-preview) 为 [Payload CMS](https://payloadcms.com) 提供更好的实时预览 — 悬停高亮并显示 block 标识、双向管理/预览同步,以及平滑过渡效果。 ## 功能 ### 悬停高亮 蓝色覆盖层会标记光标所在的 block,并带有标签徽章显示 block 类型、索引和名称。嵌套的 block 将获得带有面包屑标签的虚线父级覆盖层。 ![悬停高亮](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/d76ce711c4093427.gif) ### Admin → 预览同步 在 admin 编辑器中点击一个 block 行,预览界面会滚动到该 block 并通过闪烁效果将其高亮。 ![Admin 到预览同步](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/9ca26e4982093428.gif) ### 预览 → Admin 同步 在预览界面中点击一个 block,admin 编辑器会滚动到对应的 block 行,如果其处于折叠状态则将其展开(包括嵌套 block 的所有祖先行),并将其高亮。 ![预览到 Admin 同步](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/05608e8dfe093429.gif) ### 其他 - **仅限草稿** — 对已发布页面零影响 - **滚动/调整大小跟踪** — 覆盖层平滑跟随 block 位置 - **无限嵌套** — 双向同步适用于任意 block 嵌套深度 ## 安装 ``` pnpm add payload-better-preview # 或者 npm install payload-better-preview ``` ### 1. 注册插件 ``` // payload.config.ts import { betterPreview } from 'payload-better-preview' export default buildConfig({ plugins: [ betterPreview({ accentColor: '#3b82f6', // optional scrollAlign: 'start', // optional scrollOffset: 128, // optional }), ], admin: { livePreview: { collections: ['pages'], url({ data }) { return `/${data.id}` }, }, }, }) ``` ### 2. 为 block 包装器添加数据属性 每个渲染的 block 必须具有三个数据属性: | 属性 | 描述 | |---|---| | `data-block` | Block 类型 slug,例如 `"hero"`、`"text"` | | `data-block-index` | blocks 字段中从 0 开始的索引 | | `data-block-field` | 字段路径 — 见下文 | | `data-block-name` | 可选的显示名称,展示在覆盖层标签中 | `data-block-field` 的值必须与 Payload 字段名匹配,以便插件能在 admin 和预览界面之间映射 block: ``` export function BlockRenderer({ block, index, field }) { return (
{/* block content */}
) } export function RenderBlocks({ blocks, field }) { return blocks.map((block, index) => ( )) } ``` ``` // pass the exact Payload field name ``` ### 3. 在你的页面中渲染 `` ``` import { BetterPreview } from 'payload-better-preview/client' export default async function Page() { const { isEnabled: draft } = await draftMode() return ( <> {draft && } {draft && } {/* ... rest of page */} ) } ``` ## 嵌套 block 对于包含其他 block 的 block,通过用连字符连接父字段、父索引和子字段名称来构建 `field` prop。这与 Payload 在 admin 中生成的 ID 模式相匹配,并支持在任意深度进行同步: ``` export function NestedBlockRenderer({ block, index, field }) { return (
) } ``` ## RichText (Lexical) 块 Payload 的 Lexical 编辑器支持直接嵌入在 richText 字段中的 block。该插件也为这些 block 提供了完整的双向同步。 ### 1. 注册 Lexical feature 将 `BetterPreviewLexicalFeature` 添加到你的 Lexical 编辑器配置中。这会注入一个客户端插件,为编辑器中的每个 block 标记 `data-block-id` 属性,以便 admin 能够定位它们: ``` // payload.config.ts import { betterPreview, BetterPreviewLexicalFeature } from 'payload-better-preview' import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical' export default buildConfig({ plugins: [betterPreview()], editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [...] }), BetterPreviewLexicalFeature(), ], }), }) ``` ### 2. 在 JSX converters 中添加数据属性 将 `data-block-id`(block 来自 `node.fields.id` 的唯一 ID)添加到每个 block 包装器中。插件使用此 ID 在 admin 和预览界面之间进行同步: ``` // jsxConverters.tsx import { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react' export const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ ...defaultConverters, blocks: { hero: ({ node }) => (
{/* block content */}
), }, }) ``` ### 工作原理 - **`data-block-id`** 在 admin 和预览界面中唯一标识每个 block - **Admin → 预览**:在 admin 中点击 Lexical block 会发送带有 block ID 的 `scroll-to-richtext-block` 消息,预览界面会找到 `[data-block-id]` 并滚动到该位置 - **预览 → Admin**:点击带有 `data-block-id`(但没有 `data-block-field`)的 block 会发送 `focus-richtext-block` 消息,admin 会找到 `[data-block-id]`,根据需要展开折叠项,并滚动到该位置 ### 禁用特定元素上的同步 为任意元素添加 `data-preview-ignore` 可防止对其的点击触发滚动同步。双向均适用 —— 在预览界面(前端渲染的 block)和 admin(Lexical 编辑器内的自定义 block 组件)中均有效。 适用于 block 组件内的交互式元素,如按钮、链接或自定义控件: ``` // jsxConverters.tsx — preview side blocks: { cta: ({ node }) => (

{node.fields.title}

{/* clicks here will NOT trigger scroll sync */}
), }, ``` ``` // Custom block component — admin side (Lexical editor) export function CtaBlockComponent({ formData }) { return (

{formData.title}

{/* clicks here will NOT send scroll-to-preview message */}
) } ``` 任何源自 `[data-preview-ignore]` 元素内部的点击都会被插件完全忽略 —— 该交互的悬停高亮和滚动同步都将被跳过。 ### richText 内部的嵌套 block 对于嵌套在 richText block 内部的 block(例如,一个拥有自己 `blocks` 字段的 `nestedBlock`),其同步方式与常规嵌套 block 相同。插件会自动检测父级 richText block 并限定搜索范围,以避免出现重复 ID。 为嵌套 block 添加 `data-block-field` 和 `data-block-index`(但不要添加到直接的 richText block 中,因为它们没有字段路径): ``` // For the outer richText block (nestedBlock):
// For inner blocks (BlockRenderer receives richTextBlockId from parent):
``` ## 插件选项 | 选项 | 类型 | 默认值 | 描述 | |---|---|---|---| | `disabled` | `boolean` | `false` | 完全禁用该插件 | | `accentColor` | `string` | `'#3b82f6'` | admin 中覆盖层和闪烁效果的高亮颜色 | | `scrollAlign` | `'start' \| 'center' \| 'end'` | `'start'` | admin 中滚动时的 block 对齐方式 | | `scrollOffset` | `number` | `128` | admin 中滚动时的顶部偏移量(以 px 为单位),用于补偿固定的 admin 顶部栏 | ## `` props | Prop | 类型 | 默认值 | 描述 | |---|---|---|---| | `accentColor` | `string` | `'#3b82f6'` | 预览界面中覆盖层和闪烁效果的高亮颜色 | | `scrollAlign` | `'start' \| 'center' \| 'end'` | `'start'` | 预览界面中滚动时的 block 对齐方式 | | `scrollOffset` | `number` | `0` | 预览界面中滚动时的顶部偏移量(以 px 为单位),适用于固定头部 | | `showToggle` | `boolean` | `true` | 显示内置的切换按钮 | | `toggleComponent` | `React.ComponentType` | — | 使用自定义组件替换内置的切换按钮 | ### 预览界面中的滚动偏移 预览在内部使用 `window.scrollTo` 以避免将滚动传播到父级 admin frame。因此,`scroll-margin-top` 不起作用。请改用 `scrollOffset`: ``` ``` ### 自定义切换组件 ``` import type { ToggleProps } from 'payload-better-preview/client' function MyToggle({ enabled, onToggle }: ToggleProps) { return ( ) } ``` ## 工作原理 **Block 字段(标准):** - **Admin → 预览**:在 admin 中点击一个 block 行会向预览 iframe 发送带有 `{ field, index }` 的 `scroll-to-block` 消息。预览界面会找到 `[data-block-field][data-block-index]` 并滚动到该位置。 - **预览 → Admin**:点击带有 `data-block-field` 的 block 会发送 `focus-block` 消息。Admin 会展开所有祖先行并滚动到目标行。 **RichText (Lexical) block:** - **Admin → 预览**:点击 Lexical block 会发送带有 `{ blockId }` 的 `scroll-to-richtext-block` 消息。预览界面会找到 `[data-block-id]` 并滚动到该位置。 - **预览 → Admin**:点击带有 `data-block-id`(但没有 `data-block-field`)的 block 会发送 `focus-richtext-block` 消息。Admin 会找到 `[data-block-id]`,根据需要展开折叠项,并滚动到该位置。 - **richText 内部的嵌套 block**:点击会发送带有额外 `richTextBlockId` 的 `focus-block` 消息。Admin 会打开父级 Lexical block,然后在其内部限定行搜索范围,从而避免存在多个相同 block 时出现重复 ID 问题。 所有通信均通过 `window.postMessage` 进行。没有网络请求,没有共享状态。 `` 是一个渲染为 `null`(无 React DOM 输出)的 `'use client'` 组件。它将 3 个绝对定位的 DOM 元素注入到 `document.body` 中: 1. **覆盖层** — 主 block 高亮(蓝色实线边框) 2. **父级覆盖层** — 用于嵌套 block(虚线边框,细微显示) 3. **标签** — 带有 block 类型和索引的信息徽章 所有交互均通过 `document` 上的事件委托来处理,因此它能够在实时预览重新渲染带来的 DOM 更新中继续存活。 ## 贡献者
标签:CMS, DNS解析, Live Preview, npm包, Payload CMS, React, Syscalls, TypeScript, UI交互, 内容管理系统, 双向同步, 块编辑器, 安全插件, 实时预览, 嵌套支持, 平滑过渡, 开源项目, 悬浮高亮, 所见即所得, 无头CMS, 暗色界面, 用户体验, 管理后台, 自动化攻击