@focus-reactive/payload-plugin-ab

中文文档
2.10.1发布于 2026/10/06MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2217

有效载荷 CMS 的 A/B 测试插件

中文使用文档
文档目录22 节
  1. AI 整合提示
  2. 如何运作
  3. 安装
  4. 快速启动
  5. 步骤1——注册插件
  6. 交通百分比
  7. 第2步——接通中间软件
  8. 配置参考
  9. 插件选项
  10. 收藏AB 配置
  11. 存储加速
  12. 存储适配器
  13. 全球绘图器
  14. 缩写
  15. 中间软件
  16. 创建 Solve 重写
  17. Cookie 系统
  18. 分析一体化
  19. 使用实验跟踪器
  20. 多用途支助
  21. 本地化支助
  22. 参考文献

@ 聚焦- reactive/ payload- plugin- ab

A/B 测试插件 有效载荷CMS v3 + 下一个.js. 直接在有效载荷管理员内部创建页面变体,分配流量百分数,并设置边缘的路由用户——没有额外的收藏,没有手动接线.

变体与原页的同名相活. 该插件注入了管理员UI,悬钩,以及一个显示Next.js中件读取到路由流量的变体而无需数据库回转.


AI 整合提示

复制并粘贴此提示到您的 AI 助手( Cursor, Claude等) 中, 将插件整合到已有的 Biveload + Next.js 项目中 .

I want to add A/B testing to my Payload CMS v3 + Next.js project using @focus-reactive/payload-plugin-ab.

## How it works

Variants are regular documents in the same collection as the original page, distinguished by an
injected `_abVariantOf` relationship field. The plugin auto-injects:
- A Variants panel (sidebar) on original pages — lists variants, traffic % inputs, "+ Add new" button
- A "Variant of" badge (sidebar) on variant pages — links back to the original
- Hooks that maintain a variant manifest (a path-keyed JSON map) in a storage adapter
- A `POST /api/_ab/duplicate` endpoint that clones a page with slug `original--{hash}`

The manifest is read by Next.js middleware at the edge to rewrite requests to the correct variant.

## Installation

pnpm add @focus-reactive/payload-plugin-ab

## Step 1 — Register the plugin in payload.config.ts

import { abTestingPlugin } from '@focus-reactive/payload-plugin-ab'
import { payloadGlobalAdapter } from '@focus-reactive/payload-plugin-ab/adapters/payload-global'

const abAdapter = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '',
})

// Inside buildConfig({ plugins: [...] })
abTestingPlugin({
  storage: abAdapter,
  collections: {
    // Key = slug of any collection you want to A/B test
    pages: {
      generatePath: ({ doc, locale }) => {
        const slug = doc.slug as string | undefined
        if (!slug) return null
        return locale ? `/${locale}/${slug}` : `/${slug}`
      },
      // generateVariantData is optional — auto-generates:
      // { bucket: variantSlug, rewritePath: generatePath(variantDoc), passPercentage }
    },
  },
})

## Step 2 — Add Next.js middleware

import { createResolveAbRewrite } from '@focus-reactive/payload-plugin-ab/middleware'
import { payloadGlobalAdapter } from '@focus-reactive/payload-plugin-ab/adapters/payload-global'

const storage = payloadGlobalAdapter({ serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? '' })

const resolveAbRewrite = createResolveAbRewrite({
  storage,
  getBucket: (v) => v.bucket,
  getRewritePath: (v) => v.rewritePath,
  getPassPercentage: (v) => v.passPercentage,
})

export async function middleware(request) {
  const { pathname } = request.nextUrl
  const result = await resolveAbRewrite(request, pathname, pathname, pathname)
  return result ?? NextResponse.next()
}

export const config = { matcher: ['/((?!_next|api|favicon.ico).*)'] }

## Important notes

- generatePath must return a non-null path for variant slugs (e.g. "about--4ji9") since it
  is also used to compute the variant's rewrite path when generateVariantData is omitted.
- Variant documents are hidden from the collection list view automatically.
- The slug field (and tenantField if configured) is read-only on variant documents.
- Percentage validation: the sum of all variant passPercentage values for one page cannot exceed 100.
  A Payload ValidationError is thrown on save if it would.
- For Vercel deployments, swap payloadGlobalAdapter for vercelEdgeAdapter for sub-millisecond
  manifest reads from Vercel Edge Config.
- If the project uses Payload localization, locale-aware manifest entries are written automatically
  — no extra config needed.

如何运作

变体存在 在与原始页面相同的收藏中数字 :

pages collection
├── original   slug: "about"       _abVariantOf: null
├── variant A  slug: "about--4ji9" _abVariantOf: <original id>
└── variant B  slug: "about--x2k1" _abVariantOf: <original id>

在每次保存或删除时,会重算 变量显示 ——由 URL 路径键入的平版 JSON 对象——并写入存储适配器. Next.js 中间软件在边缘读取运算表,以便在任何渲染发生之前重写请求.

Payload Admin  →  afterChange / afterDelete hooks  →  Storage Adapter
                                                              ↓
                                         { "/about": [variantA, variantB] }
                                                              ↓
                                              Next.js Middleware (edge)
                                                              ↓
                                           Route user to original or variant

安装

pnpm add @focus-reactive/payload-plugin-ab

对于Vercel Edge适配器,也安装:

pnpm add @vercel/edge-config

同伴依赖性 : payload ^3.0.0。 。 。 . next ^14 || ^15 财务报告和审定财务报表 react ^18 || ^19 是可选的,只要求中件和分析.


快速启动

步骤1——注册插件

// payload.config.ts
import { buildConfig } from "payload";
import { abTestingPlugin } from "@focus-reactive/payload-plugin-ab";
import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const abAdapter = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? "",
});

export default buildConfig({
  plugins: [
    abTestingPlugin({
      storage: abAdapter,
      collections: {
        pages: {
          generatePath: ({ doc, locale }) => {
            const slug = doc.slug as string | undefined;
            if (!slug) return null;
            return locale ? `/${locale}/${slug}` : `/${slug}`;
          },
        },
      },
    }),
  ],
});

插件注入每个配置的收藏 :

  • 备选面板 (原页边栏) — 列出输入% 的变体, 添加, 删除
  • "变相" 只读关系( 变体页上的边栏)
  • _abVariantOf 财务报告和审定财务报表 _abPassPercentage 字段( 隐藏在编辑器中)
  • admin.baseListFilter 从列表视图中隐藏变量文档
  • beforeChange 编号 : afterChange 编号 : afterDelete 钩子

交通百分比

每个变体都有自己的 _abPassPercentage,与变体文档一起版本: 版本草稿中包含您正在编辑的值, 已出版的版本中包含该值 显示服务。 原始页面没有存储副本.

备选案文小组的编辑百分比 _abPendingPercentages 虚拟 字段,没有数据库列,随原保存发送 :

  • 保存草稿 每个变体草案的新百分比。 现场直播 流量继续使用之前发布的分数.
  • 发布 原文中将其应用于已公布的变体。 只有百分比为 发布——一个变体的未发布内容保持未发布,并在发布时进行直播 那变体本身.
  • 从未出版的变体,无论百分比如何,都不使用流量.

可变百分比必须总计为99%或更低,使原来的百分比至少为1%.

第2步——接通中间软件

// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { createResolveAbRewrite } from "@focus-reactive/payload-plugin-ab/middleware";
import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const storage = payloadGlobalAdapter({
  serverURL: process.env.NEXT_PUBLIC_SERVER_URL ?? "",
});

const resolveAbRewrite = createResolveAbRewrite({
  storage,
  getBucket: (v) => v.bucket,
  getRewritePath: (v) => v.rewritePath,
  getPassPercentage: (v) => v.passPercentage,
});

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  const result = await resolveAbRewrite(request, pathname, pathname, pathname);
  return result ?? NextResponse.next();
}

export const config = {
  matcher: ["/((?!_next|api|favicon.ico).*)"],
};

配置参考

插件选项

interface AbTestingPluginConfig<TVariantData extends object> {
  storage: StorageAdapter<TVariantData>;
  collections: Record<string, CollectionABConfig<TVariantData>>;
  enabled?: boolean; // default: true
  debug?: boolean; // show manifest Global in Payload admin. default: false
}

收藏AB 配置

interface CollectionABConfig<TVariantData = DefaultVariantData> {
  /**
   * Slug field name used to generate variant slugs: `{original}--{hash}`.
   * Default: 'slug'
   */
  slugField?: string;

  /**
   * Dot-notation path to the tenant field.
   * Scopes percentage validation per tenant; locks the field read-only on variants.
   */
  tenantField?: string;

  /**
   * Returns the URL path used as the manifest key.
   * Return null to skip this document.
   * Called once per locale when localization is enabled.
   *
   * Must also return a valid path for variant slugs (e.g. 'about--4ji9')
   * since it is used to compute rewritePath when generateVariantData is omitted.
   */
  generatePath: (args: { doc: Record<string, unknown>; locale: string | undefined }) => string | null;

  /**
   * Builds the data stored per variant in the manifest.
   * When omitted, auto-generates: { bucket: variantSlug, rewritePath, passPercentage }
   */
  generateVariantData?: (args: {
    doc: Record<string, unknown>; // parent document
    variantDoc: Record<string, unknown>; // variant document
    locale: string | undefined;
  }) => TVariantData;
}

// Default shape when generateVariantData is omitted:
type DefaultVariantData = {
  bucket: string; // variant's slug, e.g. 'about--4ji9'
  rewritePath: string; // result of generatePath(variantDoc, locale)
  passPercentage: number; // _abPassPercentage field value
};

存储加速

interface StorageAdapter<TVariantData extends object> {
  write(path: string, variants: TVariantData[], payload: Payload): Promise<void>;
  read(path: string): Promise<TVariantData[] | null>; // must be edge-compatible
  clear(path: string, payload: Payload): Promise<void>;
  createGlobal?(debug: boolean): GlobalConfig; // optional, used by payloadGlobalAdapter
}

执行此接口以使用任何后端( Redis, Upstash, Vercel KV等) .


存储适配器

全球绘图器

将清单存储在有效载荷 Global 中作为 JSON 字段。 中间软件通过有效载荷 REST API获取.

import { payloadGlobalAdapter } from "@focus-reactive/payload-plugin-ab/adapters/payload-global";

const storage = payloadGlobalAdapter({
  serverURL: "https://cms.example.com", // required for middleware reads
  globalSlug: "_abManifest", // default
  apiRoute: "/api", // default
});

Global默认隐藏于管理员. 设定 debug: true 在插件选项中以显示 系统 组合.

缩写

将列表存储在 Vercel Edge Config 中,用于分毫秒读取边缘.

import { vercelEdgeAdapter } from "@focus-reactive/payload-plugin-ab/adapters/vercel-edge";

const storage = vercelEdgeAdapter({
  configID: process.env.EDGE_CONFIG_ID!,
  configURL: process.env.EDGE_CONFIG!,
  vercelRestAPIAccessToken: process.env.VERCEL_REST_API_ACCESS_TOKEN!,
  teamID: process.env.VERCEL_TEAM_ID, // optional
  manifestKey: "ab-testing", // default
});

必需的 vars : EDGE_CONFIG, (中文(简体) ), EDGE_CONFIG_ID, (中文(简体) ), VERCEL_REST_API_ACCESS_TOKEN。 。 。 . 在 Vercel 仪表板中创建边框配置商店并安装 @vercel/edge-config。 。 。 .


中间软件

创建 Solve 重写

import { createResolveAbRewrite } from "@focus-reactive/payload-plugin-ab/middleware";

const resolveAbRewrite = createResolveAbRewrite({
  storage, // same adapter as the plugin
  getBucket: (v) => v.bucket, // extract bucket key
  getRewritePath: (v) => v.rewritePath, // extract internal rewrite URL
  getPassPercentage: (v) => v.passPercentage, // enables weighted routing
});

// In middleware:
const result = await resolveAbRewrite(
  request,
  pathname, // visiblePathname — used as the per-path cookie key
  pathname, // manifestKey — key to look up in the manifest
  pathname, // originalRewritePath — where to send 'original' bucket users
);

三个路径参数允许您解开用户所看到的,显示的密钥,以及内部重写目标——对本地语前缀有用:

const internalPath = `/${locale}${pathname}`;

await resolveAbRewrite(request, pathname, internalPath, internalPath);

跑道: 何时 getPassPercentage 提供,每个变体都得到其流量的百分比;其余为原始。 插件在保存上验证总和从未超过100% 。 何时 getPassPercentage 被省略,所有变体和原共享的概率相等.

粘性任务 : Bucket任务被存储在一种活于浏览器会话的pro-path cookie中,因此返回的用户总是看到相同的变体. 如果桶饼干不见了,中间的软件会从实验饼干中恢复分配;再取出一个其变体已不存在的桶.

饼干默认名称终身目的
弹夹payload_ab_bucket_{path}90天时间此用户所在的桶
访客身份证ab_visitor_id365天(1天)分析的持续访问者识别符
实验exp_{path}90天时间Bucket 名称可读客户端用分析钩

取代名称和寿命( A)visitorIdMaxAge, (中文(简体) ), expCookieMaxAge, (中文(简体) ), bucketCookieMaxAge(以秒计) cookies 配置为 createResolveAbRewrite数字 :

import type { AbCookieConfig } from "@focus-reactive/payload-plugin-ab/middleware";

// Define once, share between middleware and analytics
export const abCookies: AbCookieConfig = {
  visitorIdCookieName: "my_visitor_id",
  getExpCookieName: (key) => `ab_exp_${key}`,
};

使用 resolveAbCookieNames(config, experimentId) 在服务器组件中以纯字符串方式获取 cookie 名称以传递给客户端组件 :

import { resolveAbCookieNames } from "@focus-reactive/payload-plugin-ab/middleware";

const { variantCookieName, visitorCookieName } = resolveAbCookieNames(abCookies, "/en/about");

分析一体化

挂载 <ExperimentTracker experimentId={manifestKey} /> 每个变式 页 次 它在以后的每一个事件上 都标出三个GA4的自定义尺寸:

  • fr_ab_experiment ——实验id(明显密钥等. /en/about(中文(简体) )
  • fr_ab_variant - 指定桶(original 或变种涕丸)
  • fr_ab_visitor_id - 那个 ab_visitor_id 饼干价值

在您的 GA4 属性中注册这三个维度 。 这个 @focus-reactive/payload-plugin-analytics A/B 标签读取以计算 接触、转化率、升力、意义和SRM。 转换是 分析插件已存在 lead_action 实验页上的事件——那里 没有单独的AB转换事件.

插件自动创建 ab-experiments 收藏(隐藏,除非 debug: true(中文(简体) ) 记录每个实验 startedAt 在第一个变体发布.

使用实验跟踪器

// app/[locale]/[slug]/page.tsx  (Server Component)
import { ExperimentTracker } from "@focus-reactive/payload-plugin-ab/analytics/client";
import { resolveAbCookieNames } from "@focus-reactive/payload-plugin-ab/middleware";

export default async function Page({ params }) {
  const experimentId = `/${params.locale}/${params.slug}`;
  const { variantCookieName, visitorCookieName } = resolveAbCookieNames(undefined, experimentId);

  return (
    <>
      {/* page content */}
      <ExperimentTracker
        experimentId={experimentId}
        variantCookieName={variantCookieName}
        visitorCookieName={visitorCookieName}
      />
    </>
  );
}

ExperimentTracker 是客户组件。 道具 :

Prop类型需求说明
experimentIdstring对突出密钥( URL 路径, 例如 ). /en/about(中文(简体) )
variantCookieNamestring没有覆盖变体桶的饼干名称( 使用) resolveAbCookieNames(中文(简体) )
visitorCookieNamestring没有覆盖访问者身份的 cookie 名称( 使用) resolveAbCookieNames(中文(简体) )

多用途支助

abTestingPlugin({
  storage,
  collections: {
    pages: {
      tenantField: 'tenant',  // dot-notation path to the tenant field
      generatePath: ...,
    },
  },
})

设定时,百分数验证为每个租户范围,租户田被锁定在变体页上只读.


本地化支助

不需要额外的配置 。 何时 payload.config.localization 设置时,插件将每个地方的每个保存上显示每个地方,并每个地方写一个单独的显示条目. locale 实值 undefined 未配置本地化时.

generatePath: ({ doc, locale }) => {
  const slug = doc.slug as string | undefined;
  if (!slug) return null;
  return locale ? `/${locale}/${slug}` : `/${slug}`; // '/en/about', '/fr/about'
};

参考文献

导入路径出口
@focus-reactive/payload-plugin-ababTestingPlugin, (中文(简体) ), AbTestingPluginConfig, (中文(简体) ), CollectionABConfig, (中文(简体) ), StorageAdapter, (中文(简体) ), AbCookieConfig, (中文(简体) ), resolveAbCookieNames, (中文(简体) ), ResolvedAbCookieNames, (中文(简体) ), AB_EXPERIMENTS_SLUG, (中文(简体) ), AB_DIMENSION_PARAMS
@focus-reactive/payload-plugin-ab/adapters/payload-globalpayloadGlobalAdapter
@focus-reactive/payload-plugin-ab/adapters/vercel-edgevercelEdgeAdapter
@focus-reactive/payload-plugin-ab/middlewarecreateResolveAbRewrite, (中文(简体) ), resolveAbCookieNames, (中文(简体) ), AbCookieConfig, (中文(简体) ), ResolveAbRewriteConfig
@focus-reactive/payload-plugin-ab/analyticsExperimentTracker, (中文(简体) ), ExperimentTrackerProps, (中文(简体) ), AbCookieConfig, (中文(简体) ), resolveAbCookieNames, (中文(简体) ), ResolvedAbCookieNames
@focus-reactive/payload-plugin-ab/analytics/clientExperimentTracker, (中文(简体) ), ExperimentTrackerProps, (中文(简体) ), AbCookieConfig, (中文(简体) ), resolveAbCookieNames, (中文(简体) ), ResolvedAbCookieNames

版本与发布记录

@focus-reactive/payload-plugin-ab 共发布 24 个版本,首次发布于 2026/03/02;过去 12 个月发布了 24 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.10.1latest2026/10/061 个月前
2.10.02026/08/123 个月前
2.9.02026/06/194 个月前
2.8.02026/06/184 个月前
2.7.02026/06/055 个月前
2.6.02026/05/255 个月前

关于 @focus-reactive/payload-plugin-ab 的常见问题

@focus-reactive/payload-plugin-ab 是什么?

有效载荷 CMS 的 A/B 测试插件

如何安装 @focus-reactive/payload-plugin-ab?

在项目目录运行 npm install @focus-reactive/payload-plugin-ab,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:payload(^3.0.0)、@payloadcms/ui(^3.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@focus-reactive/payload-plugin-ab 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@focus-reactive/payload-plugin-ab 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@focus-reactive/payload-plugin-ab 可以商用吗?

@focus-reactive/payload-plugin-ab 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@focus-reactive/payload-plugin-ab 的下载量有多少?

根据 npm 官方下载统计,@focus-reactive/payload-plugin-ab 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,217 次,日均约 74 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@focus-reactive/payload-plugin-ab 还在维护吗?

最新版本 2.10.1 发布于 2026/10/06(1 个月前);至今共发布 24 个版本,其中过去 12 个月发布了 24 个;首次发布于 2026/03/02。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。