scorpio-99/payload-better-preview
GitHub: scorpio-99/payload-better-preview
为 Payload CMS 提供增强版实时预览体验,支持悬停高亮、区块标识以及后台编辑器与前端预览之间的双向滚动同步。
Stars: 9 | Forks: 1
# payload-better-preview
[](https://www.npmjs.com/package/payload-better-preview)
[](https://www.npmjs.com/package/payload-better-preview)
[](https://github.com/scorpio-99/payload-better-preview/issues)
[](https://github.com/scorpio-99/payload-better-preview)
为 [Payload CMS](https://payloadcms.com) 提供更好的实时预览 — 悬停高亮并显示 block 标识、双向管理/预览同步,以及平滑过渡效果。
## 功能
### 悬停高亮
蓝色覆盖层会标记光标所在的 block,并带有标签徽章显示 block 类型、索引和名称。嵌套的 block 将获得带有面包屑标签的虚线父级覆盖层。

### Admin → 预览同步
在 admin 编辑器中点击一个 block 行,预览界面会滚动到该 block 并通过闪烁效果将其高亮。

### 预览 → Admin 同步
在预览界面中点击一个 block,admin 编辑器会滚动到对应的 block 行,如果其处于折叠状态则将其展开(包括嵌套 block 的所有祖先行),并将其高亮。

### 其他
- **仅限草稿** — 对已发布页面零影响
- **滚动/调整大小跟踪** — 覆盖层平滑跟随 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 (
))
}
```
```
// 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 }) => (
),
},
```
```
// Custom block component — admin side (Lexical editor)
export function CtaBlockComponent({ formData }) {
return (
)
}
```
任何源自 `[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):
{/* block content */}
)
}
export function RenderBlocks({ blocks, field }) {
return blocks.map((block, index) => (
{/* 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 */}{formData.title}
{/* clicks here will NOT send scroll-to-preview message */}
```
## 插件选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
| `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, 暗色界面, 用户体验, 管理后台, 自动化攻击