@sanity/personalization-plugin 2.5.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +570 -144
- package/dist/growthbook/index.d.ts +12 -15
- package/dist/growthbook/index.d.ts.map +1 -0
- package/dist/growthbook/index.js +104 -68
- package/dist/growthbook/index.js.map +1 -1
- package/dist/index.d.ts +212 -252
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +398 -301
- package/dist/index.js.map +1 -1
- package/dist/launchDarkly/index.d.ts +9 -12
- package/dist/launchDarkly/index.d.ts.map +1 -0
- package/dist/launchDarkly/index.js +74 -46
- package/dist/launchDarkly/index.js.map +1 -1
- package/package.json +37 -79
- package/dist/growthbook/index.d.mts +0 -15
- package/dist/growthbook/index.mjs +0 -124
- package/dist/growthbook/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -267
- package/dist/index.mjs +0 -472
- package/dist/index.mjs.map +0 -1
- package/dist/launchDarkly/index.d.mts +0 -12
- package/dist/launchDarkly/index.mjs +0 -107
- package/dist/launchDarkly/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/components/Array.tsx +0 -68
- package/src/components/ExperimentContext.tsx +0 -65
- package/src/components/ExperimentField.tsx +0 -138
- package/src/components/ExperimentInput.tsx +0 -75
- package/src/components/ExperimentItem.tsx +0 -10
- package/src/components/Select.tsx +0 -43
- package/src/components/VariantInput.tsx +0 -19
- package/src/components/VariantPreview.tsx +0 -75
- package/src/fieldExperiments.tsx +0 -266
- package/src/growthbook/Components/GrowthbookContext.tsx +0 -38
- package/src/growthbook/Components/Secrets.tsx +0 -47
- package/src/growthbook/index.ts +0 -54
- package/src/growthbook/types.ts +0 -15
- package/src/growthbook/utils.ts +0 -94
- package/src/index.ts +0 -3
- package/src/launchDarkly/components/LaunchDarklyContext.tsx +0 -36
- package/src/launchDarkly/components/Secrets.tsx +0 -46
- package/src/launchDarkly/index.ts +0 -52
- package/src/launchDarkly/types.ts +0 -193
- package/src/launchDarkly/utils.ts +0 -54
- package/src/types.ts +0 -245
- package/src/utils/flattenSchemaType.ts +0 -47
- package/v2-incompatible.js +0 -11
package/README.md
CHANGED
|
@@ -1,32 +1,59 @@
|
|
|
1
1
|
# @sanity/personalization-plugin
|
|
2
2
|
|
|
3
|
-
## Previously
|
|
3
|
+
## Previously known as @sanity/personalisation-plugin
|
|
4
4
|
|
|
5
|
-
This plugin allows users to add a/b/n testing experiments to individual fields.
|
|
5
|
+
This plugin allows users to add a/b/n testing experiments to individual fields and page-level experiments.
|
|
6
|
+
|
|
7
|
+
> **Full Demo**
|
|
8
|
+
>
|
|
9
|
+
> 🚀 For a full working example of this plugin implemented with Next.js, see the [personalization-plugin-example](https://github.com/demo-repositories/personalization-plugin-example) repository.
|
|
10
|
+
>
|
|
11
|
+
> 🎬 Watch the [video walkthrough](https://www.loom.com/share/3e1314575b23434eb0aa35ccad9b9592) to see how the plugin works in a Next.js project.
|
|
6
12
|
|
|
7
13
|

|
|
8
14
|
|
|
9
|
-
For this plugin you need to
|
|
15
|
+
For this plugin you need to define the experiments you are running and the variations those experiments have. Each experiment needs to have an id, a label, and an array of variants that have an id and a label. You can either pass an array of experiments in the plugin config, or you can use and async function to retrieve the experiments and variants from an external service like growthbook, Amplitude, LaunchDarkly... You could even store the experiments in your sanity dataset.
|
|
10
16
|
|
|
11
17
|
Once configured you can query the values using the ids of the experiment and variant
|
|
12
18
|
|
|
13
|
-
- [@sanity/personalization-plugin](
|
|
19
|
+
- [@sanity/personalization-plugin](#sanitypersonalization-plugin)
|
|
20
|
+
- [Previously known as @sanity/personalisation-plugin](#previously-known-as-sanitypersonalisation-plugin)
|
|
14
21
|
- [Installation](#installation)
|
|
22
|
+
- [When to Use This Plugin](#when-to-use-this-plugin)
|
|
15
23
|
- [Usage](#usage)
|
|
16
24
|
- [Loading Experiments](#loading-experiments)
|
|
25
|
+
- [Option 1: Static Array](#option-1-static-array)
|
|
26
|
+
- [Option 2: Fetch from External Service](#option-2-fetch-from-external-service)
|
|
27
|
+
- [Option 3: Store in Sanity Dataset](#option-3-store-in-sanity-dataset)
|
|
17
28
|
- [Using complex field configurations](#using-complex-field-configurations)
|
|
29
|
+
- [Page-Level Experiments](#page-level-experiments)
|
|
30
|
+
- [Step 1: Configure the Plugin with a Reference Field](#step-1-configure-the-plugin-with-a-reference-field)
|
|
31
|
+
- [Step 2: Create a Route Experiment Document Type](#step-2-create-a-route-experiment-document-type)
|
|
32
|
+
- [Step 3: Query the Correct Page](#step-3-query-the-correct-page)
|
|
33
|
+
- [Step 4: Implement Proxy for Routing](#step-4-implement-proxy-for-routing)
|
|
18
34
|
- [Validation of individual array items](#validation-of-individual-array-items)
|
|
19
35
|
- [Shape of stored data](#shape-of-stored-data)
|
|
20
36
|
- [Querying data](#querying-data)
|
|
21
|
-
- [
|
|
37
|
+
- [Variant Assignment](#variant-assignment)
|
|
38
|
+
- [Variant ID Consistency](#variant-id-consistency)
|
|
39
|
+
- [Cookie-Based Assignment](#cookie-based-assignment)
|
|
40
|
+
- [Reading Variants in Page Components](#reading-variants-in-page-components)
|
|
41
|
+
- [Third-Party Integration](#third-party-integration)
|
|
42
|
+
- [Split testing (URL-based)](#split-testing-url-based)
|
|
43
|
+
- [Studio Setup](#studio-setup)
|
|
44
|
+
- [Frontend usage](#frontend-usage)
|
|
22
45
|
- [Using experiment fields in an array](#using-experiment-fields-in-an-array)
|
|
46
|
+
- [Overwriting the experiment and variant field names](#overwriting-the-experiment-and-variant-field-names)
|
|
47
|
+
- [Example: Audience Segmentation](#example-audience-segmentation)
|
|
48
|
+
- [Stored Data Structure](#stored-data-structure)
|
|
49
|
+
- [Querying with Custom Field Names](#querying-with-custom-field-names)
|
|
23
50
|
- [License](#license)
|
|
24
51
|
- [Develop \& test](#develop--test)
|
|
25
52
|
- [Release new version](#release-new-version)
|
|
26
|
-
- [License](#license-1)
|
|
27
53
|
|
|
28
|
-
For Specific information about the Growthbook FieldLevel export see its [readme](/growthbook.md)
|
|
29
|
-
|
|
54
|
+
> For Specific information about the Growthbook FieldLevel export see its [readme](/growthbook.md)
|
|
55
|
+
>
|
|
56
|
+
> For Specific information about the LaunchDarkly FieldLevel export see its [readme](/launchdarkly.md)
|
|
30
57
|
|
|
31
58
|
## Installation
|
|
32
59
|
|
|
@@ -34,6 +61,27 @@ For Specific information about the LaunchDarkly FieldLevel export see its [readm
|
|
|
34
61
|
npm install @sanity/personalization-plugin
|
|
35
62
|
```
|
|
36
63
|
|
|
64
|
+
## When to Use This Plugin
|
|
65
|
+
|
|
66
|
+
This plugin supports two types of A/B testing:
|
|
67
|
+
|
|
68
|
+
| Type | Use Case | Example |
|
|
69
|
+
| --------------- | ---------------------------------------------- | ------------------------------------------- |
|
|
70
|
+
| **Field-Level** | Test different content values on the same page | Different headlines, CTAs, or descriptions |
|
|
71
|
+
| **Page-Level** | Test entirely different page layouts | Different homepage designs or landing pages |
|
|
72
|
+
|
|
73
|
+
**Choose Field-Level when:**
|
|
74
|
+
|
|
75
|
+
- You want to test a single element (headline, button text, image)
|
|
76
|
+
- The page structure stays the same
|
|
77
|
+
- You need fine-grained control over individual content pieces
|
|
78
|
+
|
|
79
|
+
**Choose Page-Level when:**
|
|
80
|
+
|
|
81
|
+
- You want to test completely different page designs
|
|
82
|
+
- Multiple elements change together as part of a cohesive variant
|
|
83
|
+
- You're running landing page optimization tests
|
|
84
|
+
|
|
37
85
|
## Usage
|
|
38
86
|
|
|
39
87
|
Add it as a plugin in `sanity.config.ts` (or .js):
|
|
@@ -42,31 +90,38 @@ Add it as a plugin in `sanity.config.ts` (or .js):
|
|
|
42
90
|
import {defineConfig} from 'sanity'
|
|
43
91
|
import {fieldLevelExperiments} from '@sanity/personalization-plugin'
|
|
44
92
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
93
|
+
// Example: Testing different homepage headlines
|
|
94
|
+
const headlineExperiment = {
|
|
95
|
+
id: 'homepage-headline',
|
|
96
|
+
label: 'Homepage Headline Test',
|
|
48
97
|
variants: [
|
|
49
98
|
{
|
|
50
|
-
id: '
|
|
51
|
-
label: '
|
|
99
|
+
id: 'control',
|
|
100
|
+
label: 'Control',
|
|
52
101
|
},
|
|
53
102
|
{
|
|
54
|
-
id: '
|
|
55
|
-
label: '
|
|
103
|
+
id: 'emotional',
|
|
104
|
+
label: 'Emotional Appeal',
|
|
56
105
|
},
|
|
57
106
|
],
|
|
58
107
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
108
|
+
|
|
109
|
+
// Example: Testing different signup button text
|
|
110
|
+
const ctaExperiment = {
|
|
111
|
+
id: 'signup-cta',
|
|
112
|
+
label: 'Signup CTA Test',
|
|
62
113
|
variants: [
|
|
63
114
|
{
|
|
64
|
-
id: '
|
|
65
|
-
label: '
|
|
115
|
+
id: 'control',
|
|
116
|
+
label: 'Control',
|
|
66
117
|
},
|
|
67
118
|
{
|
|
68
|
-
id: '
|
|
69
|
-
label: '
|
|
119
|
+
id: 'urgent',
|
|
120
|
+
label: 'Urgency Messaging',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'benefit',
|
|
124
|
+
label: 'Benefit Focused',
|
|
70
125
|
},
|
|
71
126
|
],
|
|
72
127
|
}
|
|
@@ -77,22 +132,22 @@ export default defineConfig({
|
|
|
77
132
|
//...
|
|
78
133
|
fieldLevelExperiments({
|
|
79
134
|
fields: ['string'],
|
|
80
|
-
|
|
81
|
-
experiments: [experiment1, experiment2],
|
|
135
|
+
experiments: [headlineExperiment, ctaExperiment],
|
|
82
136
|
}),
|
|
83
137
|
],
|
|
84
138
|
})
|
|
85
139
|
```
|
|
86
140
|
|
|
87
|
-
This will register two new fields to the schema
|
|
141
|
+
This will register two new fields to the schema based on the setting passed into `fields:`:
|
|
88
142
|
|
|
89
|
-
- `experimentString`
|
|
90
|
-
- `variantString`
|
|
143
|
+
- `experimentString` - An object field with a `string` field called `default`, a `string` field called `experimentId`, and an array field called `variants`
|
|
144
|
+
- `variantString` - An object field with a `string` field called `value`, a string field called `variantId`, and a `string` field called `experimentId`
|
|
91
145
|
|
|
92
146
|
Use the experiment field in your schema like this:
|
|
93
147
|
|
|
94
148
|
```ts
|
|
95
|
-
//
|
|
149
|
+
// Example: blog post with A/B testable title
|
|
150
|
+
// In post.ts
|
|
96
151
|
|
|
97
152
|
fields: [
|
|
98
153
|
defineField({
|
|
@@ -102,84 +157,94 @@ fields: [
|
|
|
102
157
|
]
|
|
103
158
|
```
|
|
104
159
|
|
|
160
|
+
When editors open a document with this field, they can:
|
|
161
|
+
|
|
162
|
+
1. Enter a **default value** (shown to users not in an experiment)
|
|
163
|
+
2. Click the <img src="./beaker.svg" alt="beaker icon" width="28"> **beaker icon** ("Add experiment") to assign an experiment
|
|
164
|
+
3. Enter **variant-specific values** for each variant in the experiment
|
|
165
|
+
|
|
166
|
+

|
|
167
|
+
|
|
168
|
+
> 💡 **Tip:** Look for the <img src="./beaker.svg" alt="beaker icon" width="28"> beaker icon in the field toolbar — clicking it opens the experiment picker where you can assign an experiment and enter variant-specific values.
|
|
169
|
+
|
|
105
170
|
## Loading Experiments
|
|
106
171
|
|
|
107
|
-
Experiments must be an array of objects with an id
|
|
172
|
+
Experiments must be an array of objects with an `id`, `label`, and an array of `variants` (each with `id` and `label`).
|
|
173
|
+
|
|
174
|
+
**Important:** The variant `id` values must match what your frontend uses to assign users to variants (typically via cookies).
|
|
175
|
+
|
|
176
|
+
### Option 1: Static Array
|
|
177
|
+
|
|
178
|
+
Define experiments directly in your config:
|
|
108
179
|
|
|
109
180
|
```ts
|
|
110
181
|
experiments: [
|
|
111
182
|
{
|
|
112
|
-
id: '
|
|
113
|
-
label: '
|
|
183
|
+
id: 'homepage-headline',
|
|
184
|
+
label: 'Homepage Headline Test',
|
|
114
185
|
variants: [
|
|
115
|
-
{
|
|
116
|
-
|
|
117
|
-
label: 'first var',
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: '123-b',
|
|
121
|
-
label: 'second var',
|
|
122
|
-
},
|
|
186
|
+
{id: 'control', label: 'Control'},
|
|
187
|
+
{id: 'emotional', label: 'Emotional Appeal'},
|
|
123
188
|
],
|
|
124
189
|
},
|
|
125
190
|
{
|
|
126
|
-
id: '
|
|
127
|
-
label: '
|
|
191
|
+
id: 'signup-cta',
|
|
192
|
+
label: 'Signup CTA Test',
|
|
128
193
|
variants: [
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
id: '456-b',
|
|
135
|
-
label: 'b second var',
|
|
136
|
-
},
|
|
194
|
+
{id: 'control', label: 'Control'},
|
|
195
|
+
{id: 'urgent', label: 'Urgency Messaging'},
|
|
196
|
+
{id: 'benefit', label: 'Benefit Focused'},
|
|
137
197
|
],
|
|
138
198
|
},
|
|
139
199
|
]
|
|
140
200
|
```
|
|
141
201
|
|
|
142
|
-
|
|
202
|
+
### Option 2: Fetch from External Service
|
|
203
|
+
|
|
204
|
+
Use an async function to load experiments from services like GrowthBook, Amplitude, or LaunchDarkly:
|
|
143
205
|
|
|
144
206
|
```ts
|
|
145
207
|
experiments: async () => {
|
|
146
|
-
const response = await fetch('https://
|
|
147
|
-
const {externalExperiments} = await response.json()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
})
|
|
158
|
-
return {
|
|
159
|
-
id: experimentId,
|
|
160
|
-
label: experimentLabel,
|
|
161
|
-
variants,
|
|
162
|
-
}
|
|
163
|
-
})
|
|
164
|
-
return experiments
|
|
208
|
+
const response = await fetch('https://api.growthbook.io/experiments')
|
|
209
|
+
const {experiments: externalExperiments} = await response.json()
|
|
210
|
+
|
|
211
|
+
return externalExperiments?.map((experiment) => ({
|
|
212
|
+
id: experiment.id,
|
|
213
|
+
label: experiment.name,
|
|
214
|
+
variants: experiment.variations?.map((variant) => ({
|
|
215
|
+
id: variant.variationId,
|
|
216
|
+
label: variant.name,
|
|
217
|
+
})),
|
|
218
|
+
}))
|
|
165
219
|
}
|
|
166
220
|
```
|
|
167
221
|
|
|
168
|
-
|
|
222
|
+
### Option 3: Store in Sanity Dataset
|
|
223
|
+
|
|
224
|
+
The async function receives a configured Sanity Client, allowing you to store experiments as documents:
|
|
169
225
|
|
|
170
226
|
```ts
|
|
171
227
|
experiments: async (client) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
228
|
+
// Fetch experiment documents from your dataset
|
|
229
|
+
const experiments = await client.fetch(`
|
|
230
|
+
*[_type == 'experiment']{
|
|
231
|
+
id,
|
|
232
|
+
label,
|
|
233
|
+
variants[]{id, label}
|
|
234
|
+
}
|
|
235
|
+
`)
|
|
236
|
+
return experiments
|
|
237
|
+
}
|
|
175
238
|
```
|
|
176
239
|
|
|
240
|
+
This approach lets content editors create and manage experiments directly in Sanity Studio without code changes.
|
|
241
|
+
|
|
177
242
|
## Using complex field configurations
|
|
178
243
|
|
|
179
244
|
For more control over the value field, you can pass a schema definition into the fields array.
|
|
180
245
|
|
|
181
246
|
```ts
|
|
182
|
-
import {defineConfig} from 'sanity'
|
|
247
|
+
import {defineConfig, defineField} from 'sanity'
|
|
183
248
|
import {fieldLevelExperiments} from '@sanity/personalization-plugin'
|
|
184
249
|
|
|
185
250
|
export default defineConfig({
|
|
@@ -195,18 +260,171 @@ export default defineConfig({
|
|
|
195
260
|
hidden: ({document}) => !document?.title,
|
|
196
261
|
}),
|
|
197
262
|
],
|
|
198
|
-
experiments: [
|
|
263
|
+
experiments: [headlineExperiment, ctaExperiment],
|
|
264
|
+
}),
|
|
265
|
+
],
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
This would also create two new fields in your schema:
|
|
270
|
+
|
|
271
|
+
- `experimentFeaturedProduct` - An object field with a `reference` field called `default`, a `string` field called `experimentId`, and an array field called `variants`
|
|
272
|
+
- `variantFeaturedProduct` - An object field with a `reference` field called `value`, a string field called `variantId`, and a `string` field called `experimentId`
|
|
273
|
+
|
|
274
|
+
Note that the `name` key in the field definition is used to name the generated field type, while the actual field inside is always called `value`.
|
|
275
|
+
|
|
276
|
+
## Page-Level Experiments
|
|
277
|
+
|
|
278
|
+
You can use this plugin to A/B test entire pages by experimenting on reference fields. This is useful when you want to show completely different page content to different user segments.
|
|
279
|
+
|
|
280
|
+

|
|
281
|
+
|
|
282
|
+
> 💡 **Tip:** Just like field-level experiments, click the <img src="./beaker.svg" alt="beaker icon" width="28"> beaker icon on the reference field to assign an experiment and configure variant-specific pages.
|
|
283
|
+
|
|
284
|
+
### Step 1: Configure the Plugin with a Reference Field
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import {defineConfig, defineField} from 'sanity'
|
|
288
|
+
import {fieldLevelExperiments} from '@sanity/personalization-plugin'
|
|
289
|
+
|
|
290
|
+
const homepageExperiment = {
|
|
291
|
+
id: 'homepage-redesign',
|
|
292
|
+
label: 'Homepage Redesign Test',
|
|
293
|
+
variants: [
|
|
294
|
+
{id: 'control', label: 'Control (Current Design)'},
|
|
295
|
+
{id: 'variant-a', label: 'Variant A (New Design)'},
|
|
296
|
+
],
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export default defineConfig({
|
|
300
|
+
//...
|
|
301
|
+
plugins: [
|
|
302
|
+
fieldLevelExperiments({
|
|
303
|
+
fields: [
|
|
304
|
+
'string',
|
|
305
|
+
// Add a reference field for page-level experiments
|
|
306
|
+
defineField({
|
|
307
|
+
name: 'page',
|
|
308
|
+
type: 'reference',
|
|
309
|
+
to: [{type: 'page'}, {type: 'homePage'}],
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
experiments: [homepageExperiment],
|
|
313
|
+
}),
|
|
314
|
+
],
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Step 2: Create a Route Experiment Document Type
|
|
319
|
+
|
|
320
|
+
Create a document type to store which pages should be shown for each route:
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import {defineType, defineField} from 'sanity'
|
|
324
|
+
|
|
325
|
+
export const routeExperiment = defineType({
|
|
326
|
+
name: 'routeExperiment',
|
|
327
|
+
title: 'Route Experiment',
|
|
328
|
+
type: 'document',
|
|
329
|
+
fields: [
|
|
330
|
+
defineField({
|
|
331
|
+
name: 'name',
|
|
332
|
+
title: 'Experiment Name',
|
|
333
|
+
type: 'string',
|
|
334
|
+
validation: (Rule) => Rule.required(),
|
|
335
|
+
}),
|
|
336
|
+
defineField({
|
|
337
|
+
name: 'targetRoute',
|
|
338
|
+
title: 'Target Route',
|
|
339
|
+
type: 'string',
|
|
340
|
+
description: 'The URL path this experiment applies to (e.g., "/" for homepage)',
|
|
341
|
+
validation: (Rule) => Rule.required(),
|
|
342
|
+
}),
|
|
343
|
+
defineField({
|
|
344
|
+
name: 'isActive',
|
|
345
|
+
title: 'Active',
|
|
346
|
+
type: 'boolean',
|
|
347
|
+
initialValue: false,
|
|
348
|
+
}),
|
|
349
|
+
defineField({
|
|
350
|
+
name: 'page',
|
|
351
|
+
title: 'Page',
|
|
352
|
+
type: 'experimentPage', // Auto-generated by the plugin
|
|
353
|
+
description: 'Select default page and variant pages',
|
|
199
354
|
}),
|
|
200
355
|
],
|
|
201
356
|
})
|
|
202
357
|
```
|
|
203
358
|
|
|
204
|
-
|
|
359
|
+
### Step 3: Query the Correct Page
|
|
360
|
+
|
|
361
|
+
Use GROQ to resolve the correct page based on experiment and variant:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const ROUTE_EXPERIMENT_QUERY = `
|
|
365
|
+
*[_type == "routeExperiment" && targetRoute == $path && isActive == true][0]{
|
|
366
|
+
"page": coalesce(
|
|
367
|
+
page.variants[experimentId == $experimentId && variantId == $variantId][0].value,
|
|
368
|
+
page.default
|
|
369
|
+
)->{
|
|
370
|
+
_id,
|
|
371
|
+
_type,
|
|
372
|
+
title,
|
|
373
|
+
slug,
|
|
374
|
+
// ... other page fields
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
`
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
> The `slug` field is required for the slug-based path rewrite (Option B in Step 4).
|
|
381
|
+
|
|
382
|
+
### Step 4: Implement Proxy for Routing
|
|
383
|
+
|
|
384
|
+
In your frontend (e.g., Next.js proxy), determine which page to serve. Two approaches are supported:
|
|
385
|
+
|
|
386
|
+
**Option A: Same URL (pageId query param)** — Keeps the visible URL stable. Best for A/B tests where users always see the same path.
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// proxy.ts
|
|
390
|
+
import {NextResponse} from 'next/server'
|
|
391
|
+
import type {NextRequest} from 'next/server'
|
|
392
|
+
|
|
393
|
+
export async function proxy(request: NextRequest) {
|
|
394
|
+
const pathname = request.nextUrl.pathname
|
|
395
|
+
|
|
396
|
+
// Get user's assigned variant from cookie
|
|
397
|
+
const variantId = request.cookies.get('ab-variant')?.value || 'control'
|
|
398
|
+
|
|
399
|
+
// Fetch the experiment configuration
|
|
400
|
+
const data = await client.fetch(ROUTE_EXPERIMENT_QUERY, {
|
|
401
|
+
path: pathname,
|
|
402
|
+
experimentId: 'homepage-redesign',
|
|
403
|
+
variantId: variantId,
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
if (data?.page) {
|
|
407
|
+
// Rewrite to the selected page (same URL, pass pageId)
|
|
408
|
+
const url = request.nextUrl.clone()
|
|
409
|
+
url.searchParams.set('pageId', data.page._id)
|
|
410
|
+
return NextResponse.rewrite(url)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return NextResponse.next()
|
|
414
|
+
}
|
|
415
|
+
```
|
|
205
416
|
|
|
206
|
-
-
|
|
207
|
-
- `variantFeaturedProduct` an object field with a `reference` field called `value`, a string field called `variandId`, a `string` field called `experimentId`.
|
|
417
|
+
**Option B: Slug-based path rewrite** — Rewrites the URL to the variant page's slug. Use when your app routes by slug (e.g. `app/[[...slug]]/page.tsx`) and you want the URL to reflect the variant.
|
|
208
418
|
|
|
209
|
-
|
|
419
|
+
```ts
|
|
420
|
+
if (data?.page?.slug?.current) {
|
|
421
|
+
// Rewrite to the variant's slug path
|
|
422
|
+
const url = request.nextUrl.clone()
|
|
423
|
+
url.pathname = `/${data.page.slug.current}`
|
|
424
|
+
return NextResponse.rewrite(url)
|
|
425
|
+
}
|
|
426
|
+
// If slug is missing, the request continues without rewriting
|
|
427
|
+
```
|
|
210
428
|
|
|
211
429
|
## Validation of individual array items
|
|
212
430
|
|
|
@@ -238,51 +456,171 @@ defineField({
|
|
|
238
456
|
|
|
239
457
|
## Shape of stored data
|
|
240
458
|
|
|
241
|
-
The custom input contains buttons which will add new array items with the experiment and variant already populated. Data returned from this
|
|
459
|
+
The custom input contains buttons which will add new array items with the experiment and variant already populated. Data returned from this field will look like this:
|
|
242
460
|
|
|
243
461
|
```json
|
|
244
462
|
"title": {
|
|
245
|
-
"default": "
|
|
246
|
-
"experimentId": "
|
|
463
|
+
"default": "Welcome to Our Platform",
|
|
464
|
+
"experimentId": "homepage-headline",
|
|
247
465
|
"variants": [
|
|
248
466
|
{
|
|
249
|
-
"experimentId": "
|
|
250
|
-
"
|
|
251
|
-
"
|
|
467
|
+
"experimentId": "homepage-headline",
|
|
468
|
+
"variantId": "control",
|
|
469
|
+
"value": "Welcome to Our Platform"
|
|
252
470
|
},
|
|
253
471
|
{
|
|
254
|
-
"experimentId": "
|
|
255
|
-
"variantId": "
|
|
256
|
-
"value": "
|
|
472
|
+
"experimentId": "homepage-headline",
|
|
473
|
+
"variantId": "emotional",
|
|
474
|
+
"value": "Transform Your Life Today"
|
|
257
475
|
}
|
|
258
476
|
]
|
|
259
477
|
}
|
|
260
478
|
```
|
|
261
479
|
|
|
480
|
+
In this example:
|
|
481
|
+
|
|
482
|
+
- `default` is shown to users not in an experiment
|
|
483
|
+
- `control` variant shows "Welcome to Our Platform"
|
|
484
|
+
- `emotional` variant shows "Transform Your Life Today"
|
|
485
|
+
|
|
262
486
|
## Querying data
|
|
263
487
|
|
|
264
|
-
|
|
488
|
+
Use GROQ's `coalesce` function to query for a specific variant with a fallback to the default value:
|
|
265
489
|
|
|
266
490
|
```ts
|
|
491
|
+
// Fetch blog posts with experiment-aware title
|
|
267
492
|
*[_type == "post"] {
|
|
268
|
-
"title":coalesce(
|
|
493
|
+
"title": coalesce(
|
|
494
|
+
title.variants[experimentId == $experimentId && variantId == $variantId][0].value,
|
|
495
|
+
title.default
|
|
496
|
+
),
|
|
497
|
+
// ... other fields
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
On your frontend, pass the experiment and variant IDs as query parameters:
|
|
502
|
+
|
|
503
|
+
```ts
|
|
504
|
+
const posts = await client.fetch(query, {
|
|
505
|
+
experimentId: 'homepage-headline',
|
|
506
|
+
variantId: userVariant, // e.g., 'control' or 'emotional'
|
|
507
|
+
})
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
This pattern ensures:
|
|
511
|
+
|
|
512
|
+
1. Users in the experiment see their assigned variant's content
|
|
513
|
+
2. Users not in an experiment see the default value
|
|
514
|
+
3. The query works even if no variants are defined (falls back to default)
|
|
515
|
+
|
|
516
|
+
## Variant Assignment
|
|
517
|
+
|
|
518
|
+
For experiments to work, your frontend must assign users to variants and pass the correct variant ID when querying content.
|
|
519
|
+
|
|
520
|
+
### Variant ID Consistency
|
|
521
|
+
|
|
522
|
+
**Important:** The variant IDs in your plugin configuration must match exactly what your frontend uses.
|
|
523
|
+
|
|
524
|
+
```ts
|
|
525
|
+
// Studio config - these IDs must match your frontend
|
|
526
|
+
const experiment = {
|
|
527
|
+
id: 'homepage-headline',
|
|
528
|
+
variants: [
|
|
529
|
+
{id: 'control', label: 'Control'}, // ID: 'control'
|
|
530
|
+
{id: 'variant-a', label: 'Variant A'}, // ID: 'variant-a'
|
|
531
|
+
],
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Cookie-Based Assignment
|
|
536
|
+
|
|
537
|
+
The most common approach is to assign variants via cookies on first visit. Using MurmurHash with a userId gives better distribution and deterministic assignment (the same user always gets the same variant):
|
|
538
|
+
|
|
539
|
+
```ts
|
|
540
|
+
// In Next.js proxy (proxy.ts)
|
|
541
|
+
import {NextResponse} from 'next/server'
|
|
542
|
+
import type {NextRequest} from 'next/server'
|
|
543
|
+
import {v4} from 'uuid'
|
|
544
|
+
import MurmurHash3 from 'imurmurhash'
|
|
545
|
+
|
|
546
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
547
|
+
|
|
548
|
+
export function proxy(request: NextRequest) {
|
|
549
|
+
const response = NextResponse.next()
|
|
550
|
+
|
|
551
|
+
// Check if user already has a variant
|
|
552
|
+
let variant = request.cookies.get('ab-variant')?.value
|
|
553
|
+
|
|
554
|
+
if (!variant) {
|
|
555
|
+
// Use logged-in user ID if available, else persisted or new anonymous ID
|
|
556
|
+
const userId =
|
|
557
|
+
getUserIdFromSession(request) ?? // Implement: return session?.user?.id etc.
|
|
558
|
+
request.cookies.get('ab-user-id')?.value ??
|
|
559
|
+
v4()
|
|
560
|
+
|
|
561
|
+
// Deterministic variant from hash (same userId → same variant)
|
|
562
|
+
variant = MurmurHash3(userId).result() % 2 ? 'control' : 'variant-a'
|
|
563
|
+
|
|
564
|
+
response.cookies.set('ab-variant', variant, {maxAge: COOKIE_MAX_AGE, path: '/'})
|
|
565
|
+
// Persist anonymous ID when we created a new one (stable until user logs in)
|
|
566
|
+
if (!getUserIdFromSession(request) && !request.cookies.get('ab-user-id')?.value) {
|
|
567
|
+
response.cookies.set('ab-user-id', userId, {maxAge: COOKIE_MAX_AGE, path: '/'})
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return response
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
> **Tip:** Install with `npm install uuid imurmurhash`. When a user logs in, update the `ab-user-id` cookie to their real user ID so variant assignment stays consistent across sessions.
|
|
576
|
+
>
|
|
577
|
+
> > **Auth integration:** Implement `getUserIdFromSession(request)` to return the logged-in user's ID (e.g. `getServerSession()?.user?.id` with NextAuth). If your app has no auth, leave it as a stub that returns `undefined` so anonymous users get a UUID-based assignment.
|
|
578
|
+
|
|
579
|
+
### Reading Variants in Page Components
|
|
580
|
+
|
|
581
|
+
In your page components, read the variant from cookies:
|
|
582
|
+
|
|
583
|
+
```ts
|
|
584
|
+
import {cookies} from 'next/headers'
|
|
585
|
+
|
|
586
|
+
async function getVariant(): Promise<string> {
|
|
587
|
+
const cookieStore = await cookies()
|
|
588
|
+
const abCookie = cookieStore.get('ab-variant')?.value
|
|
589
|
+
return abCookie || 'control'
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export default async function Page() {
|
|
593
|
+
const variant = await getVariant()
|
|
594
|
+
|
|
595
|
+
const data = await client.fetch(query, {
|
|
596
|
+
experimentId: 'homepage-headline',
|
|
597
|
+
variantId: variant,
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// Render with experiment-aware content
|
|
269
601
|
}
|
|
270
602
|
```
|
|
271
603
|
|
|
272
|
-
|
|
604
|
+
### Third-Party Integration
|
|
605
|
+
|
|
606
|
+
For advanced use cases, you can integrate with experimentation platforms like GrowthBook, LaunchDarkly, or Amplitude. These platforms handle variant assignment and provide analytics. See the [GrowthBook](/growthbook.md) and [LaunchDarkly](/launchdarkly.md) integration guides for details.
|
|
273
607
|
|
|
274
|
-
Split testing
|
|
608
|
+
## Split testing (URL-based)
|
|
609
|
+
|
|
610
|
+
Split testing involves routing users at one URL to different pages. Use this when you want to test completely different page layouts, not just individual fields.
|
|
275
611
|
|
|
276
612
|
### Studio Setup
|
|
277
613
|
|
|
278
|
-
|
|
614
|
+
First, define a custom path type for URL validation:
|
|
279
615
|
|
|
280
616
|
```ts
|
|
617
|
+
import {defineType} from 'sanity'
|
|
618
|
+
|
|
281
619
|
export const path = defineType({
|
|
282
620
|
name: 'path',
|
|
283
621
|
type: 'string',
|
|
284
622
|
validation: (Rule) =>
|
|
285
|
-
Rule.required().custom(async (value: string | undefined
|
|
623
|
+
Rule.required().custom(async (value: string | undefined) => {
|
|
286
624
|
if (!value) return true
|
|
287
625
|
if (!value.startsWith('/')) return 'Must start with "/"'
|
|
288
626
|
return true
|
|
@@ -290,28 +628,41 @@ export const path = defineType({
|
|
|
290
628
|
})
|
|
291
629
|
```
|
|
292
630
|
|
|
293
|
-
|
|
631
|
+
Add the path type to your schema and plugin config:
|
|
294
632
|
|
|
295
633
|
```ts
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
634
|
+
fieldLevelExperiments({
|
|
635
|
+
fields: ['path', 'string'], // Include 'path' for URL experiments
|
|
636
|
+
experiments: [
|
|
637
|
+
{
|
|
638
|
+
id: 'landing-page-test',
|
|
639
|
+
label: 'Landing Page A/B Test',
|
|
640
|
+
variants: [
|
|
641
|
+
{ id: 'control', label: 'Control' },
|
|
642
|
+
{ id: 'variant-a', label: 'Variant A' },
|
|
643
|
+
],
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
}),
|
|
300
647
|
```
|
|
301
648
|
|
|
302
|
-
|
|
649
|
+
Create a document type to store routing experiments:
|
|
303
650
|
|
|
304
651
|
```ts
|
|
652
|
+
import {defineType, defineField} from 'sanity'
|
|
653
|
+
|
|
305
654
|
export const routing = defineType({
|
|
306
655
|
name: 'routing',
|
|
307
656
|
type: 'document',
|
|
308
|
-
title: '
|
|
657
|
+
title: 'URL Split Tests',
|
|
309
658
|
fields: [
|
|
310
|
-
{
|
|
659
|
+
defineField({
|
|
311
660
|
name: 'pathExperiment',
|
|
661
|
+
title: 'URL Path Experiment',
|
|
312
662
|
type: 'experimentPath',
|
|
313
663
|
initialValue: {active: true},
|
|
314
|
-
|
|
664
|
+
description: 'Set the default URL and variant URLs for this test',
|
|
665
|
+
}),
|
|
315
666
|
],
|
|
316
667
|
preview: {
|
|
317
668
|
select: {
|
|
@@ -320,7 +671,8 @@ export const routing = defineType({
|
|
|
320
671
|
},
|
|
321
672
|
prepare({path, experiment}) {
|
|
322
673
|
return {
|
|
323
|
-
title: `${path}
|
|
674
|
+
title: `${path}`,
|
|
675
|
+
subtitle: `Experiment: ${experiment || 'None'}`,
|
|
324
676
|
}
|
|
325
677
|
},
|
|
326
678
|
},
|
|
@@ -329,36 +681,61 @@ export const routing = defineType({
|
|
|
329
681
|
|
|
330
682
|
### Frontend usage
|
|
331
683
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
In Next.js Middleware it could be something like
|
|
684
|
+
Use a proxy to intercept requests and route users to the appropriate page based on their variant assignment:
|
|
335
685
|
|
|
336
686
|
```ts
|
|
337
|
-
|
|
687
|
+
// proxy.ts
|
|
688
|
+
import {NextResponse} from 'next/server'
|
|
689
|
+
import type {NextRequest} from 'next/server'
|
|
690
|
+
import {v4} from 'uuid'
|
|
691
|
+
import MurmurHash3 from 'imurmurhash'
|
|
692
|
+
import {client} from './lib/sanity'
|
|
693
|
+
|
|
694
|
+
const ROUTING_QUERY = `*[
|
|
338
695
|
_type == "routing" &&
|
|
339
696
|
pathExperiment.default == $path
|
|
340
697
|
][0]{
|
|
341
|
-
"route": coalesce(
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
698
|
+
"route": coalesce(
|
|
699
|
+
pathExperiment.variants[experimentId == $experimentId && variantId == $variantId][0].value,
|
|
700
|
+
pathExperiment.default
|
|
701
|
+
)
|
|
702
|
+
}`
|
|
703
|
+
|
|
704
|
+
const COOKIE_MAX_AGE = 60 * 60 * 24 * 30 // 30 days
|
|
705
|
+
|
|
706
|
+
export async function proxy(request: NextRequest) {
|
|
707
|
+
const pathname = request.nextUrl.pathname
|
|
708
|
+
|
|
709
|
+
// Get user's variant from cookie (set on first visit)
|
|
710
|
+
let variantId = request.cookies.get('ab-variant')?.value
|
|
711
|
+
|
|
712
|
+
const response = NextResponse.next()
|
|
713
|
+
|
|
714
|
+
if (!variantId) {
|
|
715
|
+
const userId =
|
|
716
|
+
getUserIdFromSession(request) ?? // Implement: return session?.user?.id etc.
|
|
717
|
+
request.cookies.get('ab-user-id')?.value ??
|
|
718
|
+
v4()
|
|
719
|
+
variantId = MurmurHash3(userId).result() % 2 ? 'control' : 'variant-a'
|
|
720
|
+
if (!getUserIdFromSession(request) && !request.cookies.get('ab-user-id')?.value) {
|
|
721
|
+
response.cookies.set('ab-user-id', userId, {maxAge: COOKIE_MAX_AGE, path: '/'})
|
|
722
|
+
}
|
|
354
723
|
}
|
|
724
|
+
response.cookies.set('ab-variant', variantId, {maxAge: COOKIE_MAX_AGE, path: '/'})
|
|
725
|
+
|
|
726
|
+
// Query for URL routing experiments
|
|
727
|
+
const data = await client.fetch(ROUTING_QUERY, {
|
|
728
|
+
path: pathname,
|
|
729
|
+
experimentId: 'landing-page-test',
|
|
730
|
+
variantId: variantId,
|
|
731
|
+
})
|
|
355
732
|
|
|
356
|
-
|
|
357
|
-
const data = await client.fetch(ROUTING_QUERY, queryParams)
|
|
358
|
-
if (data?.route) {
|
|
733
|
+
if (data?.route && data.route !== pathname) {
|
|
359
734
|
const url = request.nextUrl.clone()
|
|
360
735
|
url.pathname = data.route
|
|
361
736
|
const rewrite = NextResponse.rewrite(url)
|
|
737
|
+
// Preserve the cookie on the rewrite response
|
|
738
|
+
rewrite.cookies.set('ab-variant', variantId, {maxAge: COOKIE_MAX_AGE, path: '/'})
|
|
362
739
|
return rewrite
|
|
363
740
|
}
|
|
364
741
|
|
|
@@ -366,11 +743,12 @@ export async function middleware(request: NextRequest) {
|
|
|
366
743
|
}
|
|
367
744
|
|
|
368
745
|
export const config = {
|
|
369
|
-
//only run the middleware on pages
|
|
370
746
|
matcher: ['/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)'],
|
|
371
747
|
}
|
|
372
748
|
```
|
|
373
749
|
|
|
750
|
+
**Tip:** For better performance, consider querying all routing experiments at build time and caching them, rather than fetching on every request.
|
|
751
|
+
|
|
374
752
|
## Using experiment fields in an array
|
|
375
753
|
|
|
376
754
|
You may want to add experiment fields as a type with in an array in oder to do this you would need to set an initial value for the experiment to active the schema would be something like:
|
|
@@ -409,19 +787,46 @@ You can then use a groq filter to return the base version of you array member so
|
|
|
409
787
|
|
|
410
788
|
## Overwriting the experiment and variant field names
|
|
411
789
|
|
|
412
|
-
If your use case
|
|
790
|
+
If your use case doesn't match the "experiment/variant" terminology, you can rename these fields. This is useful for:
|
|
791
|
+
|
|
792
|
+
- **Audience-based personalization**: Show different content to different user segments (e.g., "enterprise customers" vs "small business")
|
|
793
|
+
- **Locale-based content**: Display region-specific messaging
|
|
794
|
+
- **Feature flags**: Toggle content based on feature availability
|
|
795
|
+
|
|
796
|
+
### Example: Audience Segmentation
|
|
413
797
|
|
|
414
798
|
```ts
|
|
415
799
|
import {defineConfig} from 'sanity'
|
|
416
800
|
import {fieldLevelExperiments} from '@sanity/personalization-plugin'
|
|
417
801
|
|
|
802
|
+
// Define your audiences and segments
|
|
803
|
+
const audiences = [
|
|
804
|
+
{
|
|
805
|
+
id: 'customer-type',
|
|
806
|
+
label: 'Customer Type',
|
|
807
|
+
variants: [
|
|
808
|
+
{id: 'enterprise', label: 'Enterprise'},
|
|
809
|
+
{id: 'small-business', label: 'Small Business'},
|
|
810
|
+
{id: 'individual', label: 'Individual'},
|
|
811
|
+
],
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
id: 'subscription-tier',
|
|
815
|
+
label: 'Subscription Tier',
|
|
816
|
+
variants: [
|
|
817
|
+
{id: 'free', label: 'Free Tier'},
|
|
818
|
+
{id: 'pro', label: 'Pro Tier'},
|
|
819
|
+
{id: 'enterprise', label: 'Enterprise Tier'},
|
|
820
|
+
],
|
|
821
|
+
},
|
|
822
|
+
]
|
|
823
|
+
|
|
418
824
|
export default defineConfig({
|
|
419
825
|
//...
|
|
420
826
|
plugins: [
|
|
421
|
-
//...
|
|
422
827
|
fieldLevelExperiments({
|
|
423
828
|
fields: ['string'],
|
|
424
|
-
experiments:
|
|
829
|
+
experiments: audiences,
|
|
425
830
|
experimentNameOverride: 'audience',
|
|
426
831
|
variantNameOverride: 'segment',
|
|
427
832
|
}),
|
|
@@ -429,40 +834,61 @@ export default defineConfig({
|
|
|
429
834
|
})
|
|
430
835
|
```
|
|
431
836
|
|
|
432
|
-
This
|
|
837
|
+
This creates two new fields in your schema:
|
|
433
838
|
|
|
434
|
-
- `audienceString`
|
|
435
|
-
- `segmentString`
|
|
839
|
+
- `audienceString` - An object field with a `string` field called `default`, a `string` field called `audienceId`, and an array field called `segments`
|
|
840
|
+
- `segmentString` - An object field with a `string` field called `value`, a string field called `segmentId`, and a `string` field called `audienceId`
|
|
436
841
|
|
|
437
|
-
|
|
842
|
+
### Stored Data Structure
|
|
843
|
+
|
|
844
|
+
The data will be stored with your custom field names:
|
|
438
845
|
|
|
439
846
|
```json
|
|
440
|
-
"
|
|
441
|
-
"default": "
|
|
442
|
-
"audienceId": "
|
|
847
|
+
"headline": {
|
|
848
|
+
"default": "Welcome to Our Platform",
|
|
849
|
+
"audienceId": "customer-type",
|
|
443
850
|
"segments": [
|
|
444
851
|
{
|
|
445
|
-
"audienceId": "
|
|
446
|
-
"
|
|
447
|
-
"
|
|
852
|
+
"audienceId": "customer-type",
|
|
853
|
+
"segmentId": "enterprise",
|
|
854
|
+
"value": "Enterprise-Grade Solutions for Your Team"
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
"audienceId": "customer-type",
|
|
858
|
+
"segmentId": "small-business",
|
|
859
|
+
"value": "Grow Your Business with Powerful Tools"
|
|
448
860
|
},
|
|
449
861
|
{
|
|
450
|
-
"audienceId": "
|
|
451
|
-
"segmentId": "
|
|
452
|
-
"value": "
|
|
862
|
+
"audienceId": "customer-type",
|
|
863
|
+
"segmentId": "individual",
|
|
864
|
+
"value": "Your Personal Productivity Hub"
|
|
453
865
|
}
|
|
454
866
|
]
|
|
455
867
|
}
|
|
456
868
|
```
|
|
457
869
|
|
|
458
|
-
|
|
870
|
+
### Querying with Custom Field Names
|
|
871
|
+
|
|
872
|
+
Update your GROQ queries to use the renamed fields:
|
|
459
873
|
|
|
460
874
|
```ts
|
|
461
|
-
*[_type == "
|
|
462
|
-
"
|
|
875
|
+
*[_type == "landingPage"] {
|
|
876
|
+
"headline": coalesce(
|
|
877
|
+
headline.segments[audienceId == $audience && segmentId == $segment][0].value,
|
|
878
|
+
headline.default
|
|
879
|
+
),
|
|
463
880
|
}
|
|
464
881
|
```
|
|
465
882
|
|
|
883
|
+
On your frontend, pass the audience and segment:
|
|
884
|
+
|
|
885
|
+
```ts
|
|
886
|
+
const page = await client.fetch(query, {
|
|
887
|
+
audience: 'customer-type',
|
|
888
|
+
segment: userSegment, // e.g., 'enterprise', 'small-business', or 'individual'
|
|
889
|
+
})
|
|
890
|
+
```
|
|
891
|
+
|
|
466
892
|
## License
|
|
467
893
|
|
|
468
894
|
[MIT](LICENSE) © Jon Burbridge
|