Click goals
Let editors name click goals and attach them to buttons and links, in Payload blocks or Puck components.
A click goal counts clicks on elements marked with its short name. Editors create click goals themselves, in Tests → Goals ("A click on the page") or inline with + New click goal in the picker, then choose them as a test's goal. No metric source is needed.
A click goal's key is click:<short-name>. Short names use 1–64 lowercase letters, numbers, underscores or hyphens, starting with a letter or number ([a-z0-9][a-z0-9_-]{0,63}).
Payload blocks
Add xpGoalField() to your button or link block's fields, and render the saved value on the actual interactive element:
import type { Block } from 'payload'
import { xpGoalField } from '@delmaredigital/payload-experiments'
export const Button: Block = {
slug: 'button',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'href', type: 'text', required: true },
xpGoalField(),
],
}export function ButtonRenderer({ label, href, xpGoal }: { label: string; href: string; xpGoal?: string | null }) {
return <a href={href} data-xp-goal={xpGoal ?? undefined}>{label}</a>
}xpGoalField() adds a text field named xpGoal. Managers see a picker with inline creation; editors without goal-management access see a plain text input. Each table that uses the field, and its version table, gains one nullable text column, so run payload migrate:create and payload generate:importmap.
Publishing content with an undefined goal name is refused by the field's validation (for managers), as are draft saves when the collection validates drafts (versions.drafts.validate). A malformed name is refused for everyone.
Puck components
Use the editor field in your editor config and the plain field in your render config:
import { xpGoalPuckEditorField } from '@delmaredigital/payload-experiments/puck/editor'
// In your Button component's fields:
fields: {
label: { type: 'text' },
href: { type: 'text' },
xpGoal: xpGoalPuckEditorField(), // pass { routes } for custom Payload routes
}import { xpGoalPuckField } from '@delmaredigital/payload-experiments/puck'
fields: {
label: { type: 'text' },
href: { type: 'text' },
xpGoal: xpGoalPuckField(),
}Render data-xp-goal={xpGoal} on the element. The Puck prop adds no database column. Publishing a Puck page whose components name a goal that does not exist is refused for managers, with the component's location.
In code
Code can set the attribute directly, for example data-xp-goal="join". Define a click goal with that short name first. Goals placed only in code, or inside related documents, are invisible to the fairness check below and are not listed in the picker's "On this test's page" group.
Counting clicks
Clicks are counted by the client provider. Mount an ExperimentsProvider around the content; click recording is armed when exposure recording is not suppressed and at least one of the visitor's assignments is exposable. This does not require that test to have a click goal. It records native clicks on elements with a valid data-xp-goal value. Nested providers share one document listener, and the deepest armed provider supplies the route.
- Enter on links and buttons, and Space on buttons, count through the browser's native click. A focusable
divneedsrole="button"and your own keyboard handler. - Beacons use
sendBeacon, with akeepalivefetch fallback, so navigating away does not lose the click. - Clicks and exposure beacons share
events.rateLimit; size your limiter for both. - A newly defined click goal is accepted within 30 seconds.
Fairness
A click goal can be a test's main goal or a guardrail. Main and guardrail click goals must appear in every tested version, or outside all tested content, so each version has the same chance to collect them. Start refuses a test that breaks this (goal-in-some-versions), and each results run checks published content again. Supporting goals that appear only in some versions warn instead.
A click shows interest, not the result you want. Results for a test with a click main goal suggest adding the real outcome, such as a signup or a purchase, as another goal.

