Raffiniert-Media/payload-live-preview-inspector

GitHub: Raffiniert-Media/payload-live-preview-inspector

为 Payload CMS Live Preview 提供 Storyblok 风格的点击组件即滚动并定位到后台对应编辑字段的前端体验增强插件。

Stars: 0 | Forks: 0

# payload-live-preview-inspector 这是一个 [Payload CMS](https://payloadcms.com) 插件,它为 Payload 内置的 [Live Preview](https://payloadcms.com/docs/live-preview/overview) 带来了类似 Storyblok 风格的点击滚动功能:在 Live Preview iframe 中悬停某个组件即可将其高亮,点击它会将管理后台编辑表单平滑滚动到(并短暂闪烁)对应的字段——在此过程中会切换选项卡、展开折叠的手风琴组件,并聚焦该字段。 此插件**不会**自行设置 Live Preview。它是在已正常工作的 `admin.livePreview` 配置基础之上添加点击滚动行为的。 ![在 Live Preview 中悬停并点击组件会滚动、展开并闪烁管理后台编辑表单中的对应字段](https://raw.githubusercontent.com/Raffiniert-Media/payload-live-preview-inspector/main/docs/images/example.gif) ## 工作原理 `LivePreviewInspectorClient`(在你的前端中)会高亮鼠标指针下的被标记元素,并在点击时将其字段路径发送到管理后台。`LivePreviewInspectorListener`(由插件自动注册)会根据实时的表单状态解析该路径并显示该字段:它会切换到正确的选项卡,展开折叠的 Array/Blocks 行,滚动页面,然后闪烁并聚焦该字段。定位是基于坐标点的,并会选择最小的被标记元素,因此即使是在覆盖整个卡片的链接下方,文本依然可以被点击。 元素通过三个层级获取其路径属性——显式标记始终具有最高优先级,每一层仅填充上一层未覆盖的内容(详情见[标记](#tagging-three-layers)): 1. **`pathOf()`(显式)** —— 由你自己标记元素;精确无误,适用于任何对象。 2. **Stega(自动,可选启用)** —— `inspectable(data, { stega: true })` 将每个字符串字段的路径作为不可见字符编码到其值中;客户端从渲染的 DOM 中解码它们。 3. **值匹配(自动,零配置)** —— 标记任何整体文本与某一个字段当前值完全相等的元素。 对于自动标记的叶子节点,客户端还会**推断区块容器**,因此点击 block 的空白边缘会跳转到整行。在 iframe 之外,`LivePreviewInspectorClient` 完全是空操作。 ## 安装说明 ``` pnpm add @raffiniert-media-ag/payload-live-preview-inspector ``` 要求 `react`/`react-dom` 版本为 19;`payload` 和 `@payloadcms/ui` 是可选的对等依赖,仅在管理后台需要。该包有四个入口点,这样划分是为了防止沉重的代码泄露到你的前端 bundle 中: | 子路径 | 包含内容 | 导入位置 | | ----------- | ---------------------------------------------------------------------- | ------------------------------------------------ | | `.` | `payloadLivePreviewInspector()` 插件 | `payload.config.ts` | | `/client` | `LivePreviewInspectorClient`(一个 `'use client'` 组件) | 仅在挂载该组件的文件中 | | `/path` | `inspectable`, `pathOf`, `stegaClean`, 常量 —— 纯函数,无组件 | 其他任何地方:页面、blocks、Server Components | | `/listener` | 管理后台监听器(导入 `@payloadcms/ui`) | 无处导入 —— 插件会自动为你连接 | 打包工具将 `'use client'` 的聚合导出视为不可分割的整体:在任何客户端组件中从 `/client` 导入辅助函数,会将整个检查器拖入每个访问者的 bundle 中。请改为从 `/path` 导入它们。 ## 设置 ### 1. 管理后台(Payload 配置) 列出需要启用点击滚动的 collections/globals(每一个都必须已经启用 Live Preview): ``` import { payloadLivePreviewInspector } from '@raffiniert-media-ag/payload-live-preview-inspector' export default buildConfig({ admin: { livePreview: { collections: ['posts'], url: ({ data }) => `https://your-frontend.example.com/preview/posts/${data.id}`, }, }, plugins: [ payloadLivePreviewInspector({ collections: { posts: true }, globals: { siteSettings: true }, }), ], }) ``` 可选的覆盖配置(显示为默认值):`flashColor: '#3fb950'`,`flashDurationMs: 1200`,`scrollOffset: 100`,`accordionAnimationMs: 350`,`tabSwitchWaitMs: 1500`(等待新挂载字段的最大时间——在选项卡搜索期间针对每个候选选项卡,以及在滚动到尚未渲染的字段之后;如果非常重的选项卡被跳过,请调大此值)。 ### 2. 前端 在渲染于 Live Preview iframe 内部的任何页面根部附近,挂载一次 `LivePreviewInspectorClient`: ``` import { LivePreviewInspectorClient } from '@raffiniert-media-ag/payload-live-preview-inspector/client' export default function PreviewPage() { return ( <> {/* ...your page... */} ) } ``` 然后标记元素。推荐的方式是 `inspectable()` + `pathOf()`:包装你的文档数据一次,每个嵌套节点就会知道自己的字段路径——array/blocks 行通过它们稳定的 `id` 进行寻址,因此这种映射在重新排序后依然存在。纯 JavaScript,无需 hooks——也适用于 Server Components: ``` import { inspectable, pathOf } from '@raffiniert-media-ag/payload-live-preview-inspector/path' const page = inspectable(data)

{page.title}

{page.layout?.map((block) => (

{block.heading}

))} ``` `pathOf(node)` 寻址节点本身(例如整个 block);`pathOf(node, 'fieldName')` 寻址其上的某个字段。你不必手动标记所有内容——以下两个自动层会填补大部分空白。 ## 标记:三个层级 显式的 `pathOf()` 属性永远不会被自动层覆盖;自动标记的元素带有 `data-payload-live-preview-auto="stega" | "match" | "container"`,因此你可以在 devtools 中区分它们。 ### 1. `pathOf()` —— 显式,最精确 适用于任何情况(图片、数字、整个 blocks、没有文本的元素),且不依赖任何启发式逻辑。在自动层无法覆盖的地方使用它。 ### 2. Stega —— 针对文本内容的自动标记 向 `inspectable()` 传入 `stega: true`,从 proxy 读取的每个文本字符串都会携带其字段路径作为不可见字符——无论它最终出现在 DOM 的哪个位置,无论它跨越了多少个组件或服务器/客户端边界,扫描器都会标记包含它的元素。`alt`、`title`、`aria-label` 和 `placeholder` 属性也会被扫描。 **双词安全规则:** 只有包含**至少两个以空格分隔的单词**的字符串才会被编码。单个 token(如 `'default'`、`'topRight'`)是代码用作对象键和比较目标的——编码其中一个会悄无声息地破坏像 `styles[block.variant]` 这样的查找。Select/radio/enum 的值几乎从不包含空格,因此它们在构造上是安全的。另一方面:单个单词的显示文本不会被 stega 标记(值匹配或 `pathOf()` 会覆盖它)。另外被跳过的情况还有:`id`/`blockType`/`blockName`/`slug`,形如 URL/日期/数字/uuid 的值,以及数组中的字符串。例外情况:`alt`、`ariaLabel`、`placeholder` 总是会被编码——它们仅出现在值匹配无法触及的属性值中,且代码从不比较它们。 在需要时进行微调和获取原始值: ``` const page = inspectable(data, { stega: { encodeKeys: ['buttonLabel'], // always encode - fields you KNOW are display text skipKeys: ['cssClasses'], // never encode filter: ({ defaultEncode, key, path, value }) => defaultEncode, // final say per string }, }) import { stegaClean } from '@raffiniert-media-ag/payload-live-preview-inspector/path' stegaClean(page.title) // raw string - use before ===, new Date(), APIs; also deep-cleans objects ``` 权衡:编码后的字符串包含额外的字符——对字面量使用 `===` 会失败,并且 `slice()` 可能会破坏标记(此时它会直接丢失,绝不会出错)。所有这些仅存在于预览模式中(参见[生产环境](#production--performance))。 ### 3. 值匹配 —— 零配置 默认开启——客户端会向管理后台请求文档当前的字符串值,并标记任何整体文本刚好等于其中某一个字段值的元素。富文本字段会贡献其各自的文本片段,并映射回其编辑器。刻意采取保守策略:被多个字段共享的值(例如复制到 SEO 标题中的 hero 标题)永远不会被匹配,小于 3 个字符的值会被忽略,并且只有整个元素完全匹配才算数。在开发环境中,预览控制台会记录每个被跳过的模糊值及其冲突的字段路径。设置 `valueMatching={false}` 可将其关闭。 ### 容器推断 路径共享同一个 Array/Blocks 行前缀的元素,会投票选出它们最接近的共同祖先作为该行的容器。如果该行已被标记,则会跳过此步骤,并且永远不会应用于包含另一行元素的祖先。对于渲染很少文本的 blocks,`pathOf(block)` 依然更可靠。 ## 链接拦截 默认情况下,客户端会阻止 iframe 内的每个 `` 进行导航——包括客户端路由链接(Next.js 的 `` 等),这些链接会在其自身的处理器运行之前在捕获阶段被拦截。传入 `disableLinks={false}` 可恢复导航。中键点击和 Cmd/Ctrl-点击不受此限制。 ## Server/client 组件边界 代理的路径元数据在序列化后无法存活——将包装后的节点从 Server Component 传递到 Client Component 会导致 `pathOf()` 在另一侧返回空值。按优先级排序如下: 1. **Stega** —— 路径存在于字符串值本身内部,可以跨越任何边界。 2. **`serializable: true`** —— 将每个节点的路径作为 `__payloadLivePreviewPath` 属性嵌入,该属性可以通过 JSON 传递(在 `Object.keys()` 中可见;数组节点无法携带它,其对象子节点可以携带)。 3. **将 `pathOf()` 的结果作为 props 传递** —— 它们是纯粹的可序列化对象。 ## 生产环境 / 性能 这里的任何内容都不会触及真实访客:在 iframe 之外,客户端不会附加任何监听器,也不会扫描任何内容(其唯一的成本只是几 kB 的 bundle),proxy 的开销微乎其微,并且路径属性/stega 字符仅在启用时存在。运行此插件的生产网站在 Lighthouse 中获得了 **100/100 的移动端性能** 得分: ![Lighthouse 移动端报告:100 性能,100 无障碍,100 最佳实践,100 SEO](https://static.pigsec.cn/wp-content/uploads/repos/cas/fe/fe67a79fb31c8e03d406c15d1b785011e42031089f8f1a42e5702fd4f8850adc.png) 最干净的设置是专用的预览路由(就像这个仓库的 `dev/app/(frontend)/preview/...`)——公共路由永远不会导入这里的任何内容。如果你在公共渲染和预览渲染之间共享组件,`enabled` 是所有输出层的终极关闭开关: ``` import { draftMode } from 'next/headers' const { isEnabled } = await draftMode() const page = inspectable(data, { enabled: isEnabled, stega: true }) // enabled: false → no path attributes, no stega characters, no markers. ``` ## API 参考 从 `.`(Payload 配置)导入: - `payloadLivePreviewInspector({ collections?, globals?, disabled?, flashColor?, flashDurationMs?, scrollOffset?, accordionAnimationMs?, tabSwitchWaitMs? })` —— 参见[设置](#1-admin-payload-config)。 从 `/path` 导入(纯辅助函数,在任何地方使用都很安全;同样也从 `/client` 重新导出): - `inspectable(data, options?)` —— 路径跟踪 proxy。选项包括:`enabled`、`stega: true | { encodeKeys?, skipKeys?, filter? }`、`serializable`。 - `pathOf(node, subPath?)` —— 包装节点的路径属性。 - `stegaClean(value)` —— 从字符串或整个对象树中剥离 stega 字符。 - `LIVE_PREVIEW_PATH_ATTRIBUTE`、`LIVE_PREVIEW_AUTO_ATTRIBUTE`、`SERIALIZED_PATH_KEY`、`LIVE_PREVIEW_HOVER_CLASS_NAME` —— 原始属性/类名,例如用于手动标记或重新设置样式。 从 `/client` 导入(仅在你挂载它的地方导入): - `LivePreviewInspectorClient({ disableLinks?, hoverColor?, stega?, targetOrigin?, valueMatching? })` —— 均为可选。`targetOrigin` 将 `postMessage` 目标固定为你的管理后台源;如果省略,则会自动检测(回退到 `'*'`——其 payload 只是一个字段路径字符串)。 从 `/listener` 导入:`LivePreviewInspectorListener` —— 管理后台端;插件会为你自动注册它。 ## 已知限制 - 仅在关系的编辑抽屉内渲染的字段无法触及——点击会静默无操作。对于在预览渲染后被删除的行也是如此。 - 在另一个选项卡中查找字段会点击表单的各个选项卡(如果什么都没找到,则会恢复原样)。在切换选项卡或滚动后挂载速度慢于 `tabSwitchWaitMs` 的字段可能会导致显示结果停留在最近的父级上——对于非常庞大的表单,请调大此选项。 - 多语言环境设置或抽屉复制的字段可能会带有后缀的 DOM id;在那里的 `field-` 查找偶尔可能会漏掉。 - Stega 仅能覆盖作为文本(或 `alt`/`title`/`aria-label`/`placeholder`)渲染且包含两个或更多单词的值;重塑字符串值的操作(如 `slice()`、正则表达式)会破坏标记——此时该元素会变为未标记状态,而绝不会发生错误标记。复制的预览文本会带有不可见字符(仅限预览环境)。 - 值匹配要求整个元素与唯一一个字段值完全相等——按照设计,格式化的日期、截断的摘要和重复的值都不会匹配。 - 容器推断会在交错标记和没有渲染可标记叶子节点的 blocks 上失效;在这种情况下,请使用 `pathOf(block)`。 ## 本地开发 `dev/` 文件夹是一个完整的 Payload 应用(使用 SQLite,无需外部依赖),用于开发此插件: ``` pnpm install pnpm dev # http://localhost:3000/admin - dev@payloadcms.com / test pnpm test:int # vitest unit tests pnpm test:e2e # playwright - full hover/click/scroll/flash flow ```
标签:CMS插件, Live Preview, Payload CMS, Syscall, Web开发, 可视化编辑, 用户体验, 自动化攻击