feat(personalization): add field-level personalization - #1226
Conversation
Port sanity-io/sanity-plugin-personalization#38 Add a `fieldLevelPersonalization` plugin export for segment-based field personalization, with segments sourced from a static array, an external API, or Sanity documents. Reorganize experiment components into `experiment/` and `personalization/` folders, rename the shared array item component to `ArrayItem`, and introduce document field groups so experiment/personalization fields render cleanly inside arrays and modals. The exported `FieldPluginConfig` type is renamed to `ExperimentFieldPluginConfig`. Also wires a field-level personalization example into the test studio.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
🦋 Changeset detectedLatest commit: 2fc0185 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
Adds field-level personalization to @sanity/personalization-plugin (ported from sanity-io/sanity-plugin-personalization#38), enabling segment-based per-field overrides alongside the existing field-level experiments functionality.
Changes:
- Introduces new
fieldLevelPersonalizationplugin export with segment types, inputs, and previews. - Reorganizes experiment and personalization UI components into
src/components/experiment/*andsrc/components/personalization/*, with sharedArrayItem. - Updates exported/public types (notably renaming
FieldPluginConfig→ExperimentFieldPluginConfig) and wires the new feature into the test-studio example + package exports snapshot.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/@sanity/personalization-plugin/src/utils/clearChildGroups.ts | Adds helper to clear nested field group state for cleaner rendering in arrays/modals. |
| plugins/@sanity/personalization-plugin/src/types.ts | Renames experiment config type and adds new personalization config/context/input prop types. |
| plugins/@sanity/personalization-plugin/src/index.ts | Exports fieldLevelPersonalization from the package entrypoint. |
| plugins/@sanity/personalization-plugin/src/index.test.ts | Updates package-exports snapshot to include fieldLevelPersonalization. |
| plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx | Implements the new field-level personalization plugin (schema + form provider wiring). |
| plugins/@sanity/personalization-plugin/src/fieldExperiments.tsx | Refactors to use reorganized experiment components and adds field groups support. |
| plugins/@sanity/personalization-plugin/src/components/Select.tsx | Updates SelectOption import to the new experiment component location. |
| plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx | Adds segment item preview rendering (including reference/image handling). |
| plugins/@sanity/personalization-plugin/src/components/personalization/SegmentInput.tsx | Adds “Copy default” functionality for segment values. |
| plugins/@sanity/personalization-plugin/src/components/personalization/index.ts | Adds personalization components barrel exports. |
| plugins/@sanity/personalization-plugin/src/components/personalization/Field.tsx | Adds field action to toggle/remove personalization and clears nested groups. |
| plugins/@sanity/personalization-plugin/src/components/personalization/Context.tsx | Adds personalization provider/context that resolves segments (sync/async). |
| plugins/@sanity/personalization-plugin/src/components/personalization/Array.tsx | Adds segment “Add …” buttons to append segment entries into the array. |
| plugins/@sanity/personalization-plugin/src/components/experiment/VariantPreview.tsx | Updates imports after experiment component reorganization. |
| plugins/@sanity/personalization-plugin/src/components/experiment/VariantInput.tsx | Adds “Copy default” functionality for variant values. |
| plugins/@sanity/personalization-plugin/src/components/experiment/Input.tsx | Renames experiment input component and updates imports after reorg. |
| plugins/@sanity/personalization-plugin/src/components/experiment/index.ts | Adds experiment components barrel exports. |
| plugins/@sanity/personalization-plugin/src/components/experiment/Field.tsx | Renames/updates field component and clears nested groups before rendering. |
| plugins/@sanity/personalization-plugin/src/components/experiment/Context.tsx | Updates imports/types for the renamed experiment config type. |
| plugins/@sanity/personalization-plugin/src/components/experiment/Array.tsx | Updates imports after experiment component reorganization. |
| plugins/@sanity/personalization-plugin/src/components/ArrayItem.tsx | Renames shared array item component (ExperimentItem → ArrayItem). |
| dev/test-studio/src/personalization/index.tsx | Updates test studio example to include fieldLevelPersonalization with sample segments. |
| .changeset/field-level-personalization.md | Adds a minor changeset with author credit and summary of the new export + type rename. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…el-personalization-620c Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Align the new personalization Field with the @sanity/icons v4 convention already applied on main for the experiment Field. Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
✅ E2E Tests🟢 24 passed • view full report • view run Studio: https://plugins-e2e-test-studio-9btrfm1o2.sanity.dev Datasets: |
- Guard nested children before clearing field groups - Fix preview title operator precedence - Use configured apiVersion in SegmentPreview/VariantPreview - Avoid "undefined" segment titles; null-safe preview path traversal - Keep a deprecated FieldPluginConfig type alias for compatibility
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (7)
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:19
useClientis hard-coded to apiVersion2025-01-01, which ignores the plugin's configurableapiVersion(available inPersonalizationContext). This makes the preview behavior diverge from the rest of the plugin and from user expectations when they setapiVersion.
const {segments, apiVersion} = usePersonalizationContext()
const client = useClient({apiVersion})
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:46
- The
reduce((acc, index) => acc[index], doc)will throw if any intermediate path segment is missing (becauseaccbecomesundefinedand the next iteration readsacc[index]). Use optional chaining in the reducer so missing paths resolve toundefinedinstead of crashing the preview.
Object.keys(previewFields).forEach((key) => {
const valueKey = referenceType?.preview?.select?.[key]
selectFields[key] =
valueKey && doc
? valueKey.split('.').reduce((acc: any, index) => {
plugins/@sanity/personalization-plugin/src/utils/clearChildGroups.ts:24
children.props.childrenis spread without being validated. Ifchildren.props.childrenisundefined(or not an element-like object),...children.props.childrenwill throw at runtime. Add a guard for the nested child before spreading and reuse anestedChildvariable when building the updated props tree.
!('props' in innerChild)
) {
return props
}
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:29
setTitle(${selectedSegment?.label})will set the literal string "undefined" when no segment is selected. Assign the label directly so the state remainsundefinedin that case.
setTitle(selectedSegment?.label)
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:246
PersonalizationProvideris called like a regular function. Since it uses React hooks, this violates the Rules of Hooks and can break if thehasPersonalizationcondition ever changes between renders. Render it as a component instead.
personalizationFieldPluginConfig: {
plugins/@sanity/personalization-plugin/src/types.ts:24
- The exported
FieldPluginConfigtype was renamed/removed. Removing an exported type is a breaking change for TypeScript consumers; adding a deprecated alias keeps existing users compiling while still introducing the clearerExperimentFieldPluginConfigname (and better matches theminorbump in the changeset).
export type ExperimentFieldPluginConfig = {
fields: (string | FieldDefinition)[]
experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
apiVersion?: string
experimentNameOverride?: string
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:214
- Plugin
namecontains a misspelling (personalistaion). This can make plugin identification/debugging inconsistent (and differs from the package name).
This issue also appears on line 246 of the same file.
personalizationNameOverride,
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e4ba4de. Configure here.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (3)
plugins/@sanity/personalization-plugin/src/components/experiment/Input.tsx:14
- Importing
ExperimentTypefrom../..couples this component to the package barrel (src/index.ts) and risks circular type dependencies as exports evolve. Import the type directly fromsrc/typesinstead.
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:220 - Plugin name string contains a typo ("personalistaion"), which makes the plugin identifier harder to search for and inconsistent with the package name.
name: 'sanity-personalistaion-plugin-field-level-personalization',
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:210
segmentId/segmentArrayNameare always derived fromsegmentNameOverride, so any explicitsegmentId/segmentArrayNamepassed in the plugin config is ignored. If these fields are part of the public config type, they should be respected when provided (and only derived as a default).
const segmentArrayName = `${segmentNameOverride}s`
const segmentId = `${segmentNameOverride}Id`
…el-personalization-620c Resolve conflicts with #1208 review fixes: keep ArrayItem rename while moving activation into useEffect, keep explicit preview title typing, and use optional-chaining path traversal. Also fix the personalization plugin name spelling to match main.
Disambiguate suspend-react cache keys so experiments and personalization async resolvers do not collide when both plugins are active. Render ExperimentProvider and PersonalizationProvider as components so hooks run under a proper React boundary.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (5)
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:252
PersonalizationProvideruses hooks, but it’s being invoked as a plain function (PersonalizationProvider(providerProps)). This makes its hooks run inside the parentinputcomponent and only whenhasPersonalizationis true, which can violate the Rules of Hooks and break if the condition ever changes. Render it as a component instead.
return <PersonalizationProvider {...providerProps} />
plugins/@sanity/personalization-plugin/src/components/experiment/VariantPreview.tsx:40
VariantPreviewdoesn’t clearmedia/subtitlewhenvaluechanges between types (e.g. reference → string/image). This can leave stale preview UI from the previous value. Clear state up front in the effect before branching onvalue.
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:32SegmentPreviewdoesn’t clearmedia/subtitlewhenvaluechanges between types (e.g. reference → string/image). This can leave stale preview UI from the previous value. Clear state up front in the effect before branching onvalue.
setTitle(selectedSegment?.label)
if (typeof value === 'string') {
return setSubtitle(value)
}
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:60
- The effect dependencies don’t include
segment. If the selected segment changes but the label stays the same (orselectedSegmentresolves to another item with the same label), the effect won’t rerun and the preview can become stale.
}, [value, client, selectedSegment?.label, props.schemaType])
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentInput.tsx:9
useFormValuecan returnundefinedwhen the default field is empty/unset. Passingundefinedintoset(...)produces an invalid JSON patch (and can crash when patches are serialized). Guard this case andunset(['value'])(or no-op) instead.
const handleClick = () => {
props.onChange(set(defaultValue, ['value']))
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 24 changed files in this pull request and generated no new comments.
Suppressed comments (6)
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:38
- This reference preview path can throw when the
valuefield isn’t actually a reference type (or the schema shape differs):referenceFieldcan beundefined, butreferenceField.to.find(...)is called unconditionally.
const type = props.schemaType as ObjectSchemaType
const valueField = type.fields.find((field) => field.name === 'value') as ObjectSchemaType
const referenceField = valueField?.type as ReferenceSchemaType
const referenceType = referenceField.to.find((field) => field.type?.name === doc?._type)
plugins/@sanity/personalization-plugin/src/components/experiment/VariantPreview.tsx:37
VariantPreviewstoresmedia/subtitlein state, but doesn’t clear them whenvaluechanges between types (e.g. previously an image, now a string). This can leave stalemediaorsubtitlefrom a previous value showing in the preview.
This issue also appears on line 53 of the same file.
plugins/@sanity/personalization-plugin/src/components/experiment/VariantPreview.tsx:55
- This branch can still throw before the null-safe
reduce:referenceFieldcan beundefineddepending on the schema shape, butreferenceField.to.find(...)is called unconditionally. This would crash the document form preview for non-referencevaluefields.
plugins/@sanity/personalization-plugin/src/fieldPersonalization.tsx:210 fieldLevelPersonalizationcomputessegmentArrayName/segmentIdfromsegmentNameOverride, butPersonalizationFieldPluginConfigalso exposessegmentArrayNameandsegmentIdoverrides. As written, user-provided values are silently ignored, which is surprising for API consumers and makes those config fields ineffective.
const segmentArrayName = `${segmentNameOverride}s`
const segmentId = `${segmentNameOverride}Id`
plugins/@sanity/personalization-plugin/src/fieldExperiments.tsx:250
ExperimentFieldPluginConfigexposesexperimentId/variantId/variantArrayNameoverrides, butfieldLevelExperimentsalways derives these from the*NameOverridevalues. This makes the override fields ineffective and prevents consumers from aligning with existing schema field names.
const pluginConfig = {...CONFIG_DEFAULT, ...config}
const {fields, experimentNameOverride, variantNameOverride} = pluginConfig
plugins/@sanity/personalization-plugin/src/components/personalization/SegmentPreview.tsx:32
SegmentPreviewkeepsmedia/subtitlein state, but doesn’t clear them whenvaluechanges between types (e.g. image → string, reference → image). This can leave stalemediaorsubtitlefrom a previous value showing in the preview.
This issue also appears on line 35 of the same file.
const getSubtitle = async () => {
setTitle(selectedSegment?.label)
if (typeof value === 'string') {
return setSubtitle(value)
}
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @sanity/sanity-plugin-async-list@3.0.0 ### Major Changes - [#1087](#1087) [`f4e7fcc`](f4e7fcc) Thanks [@stipsan](https://github.com/stipsan)! - Refactor the input into a real React component and fix the `async-list-undefined` namespace/id - **Breaking:** `AsyncList` is now a regular React component that takes a single `props` argument (the standard Sanity input props plus an `options` field), instead of being called as `AsyncList(props, options)`. For the `components.input` slot, use the new `createAsyncListInput(options)` factory: `input: createAsyncListInput({loader})`. This makes the input safe under the Rules of Hooks and lets the React Compiler optimize it. - **Breaking:** the secrets namespace and DOM `id` are no longer derived as `async-list-${schemaType}` (which became the literal `async-list-undefined` for component usage). The DOM `id` now uses Sanity's stable per-field id, and the secrets namespace falls back to `async-list` (instead of `async-list-undefined`) when no `schemaType`/`secrets.namespace` is available. When using the component with `secrets`, set an explicit `secrets.namespace`; a dev warning is logged if it is missing. - Fix: the debounced search handler is stable and is cancelled on unmount, so it no longer drops queued calls or updates state on an unmounted tree. - Fix: the value-change handler no longer depends on the whole `props` object, avoiding unnecessary `Autocomplete` re-renders. - Fix: loader results are validated to ensure each option's `value` is a string. ### Patch Changes - Updated dependencies [[`c61bb44`](c61bb44)]: - @sanity/studio-secrets@4.0.15 ## @sanity/personalization-plugin@3.1.0 ### Minor Changes - [#1226](#1226) [`1143bd6`](1143bd6) Thanks [@jjburbridge](https://github.com/jjburbridge)! - Add `fieldLevelPersonalization` export for segment-based field personalization. Segments can be defined statically, fetched from an external API, or stored in your Sanity dataset. `FieldPluginConfig` is renamed to `ExperimentFieldPluginConfig` (a deprecated type alias is kept for compatibility). ### Patch Changes - [#1208](#1208) [`51317b2`](51317b2) Thanks [@stipsan](https://github.com/stipsan)! - Address review feedback from the migration into the monorepo: - Fix LaunchDarkly experiment pagination so `offset` advances between pages, preventing duplicate results and a possible infinite loop - Correct misspelled plugin names (`personalistaion` → `personalization`) and stop the LaunchDarkly entry point from identifying itself as the GrowthBook plugin - Fix an operator-precedence bug in the experiment field preview that could set the title to the entire field object - Walk reference preview paths with optional chaining to avoid throwing on missing intermediate fields - Move the experiment item activation patch out of render and into an effect - Remove unused `Select` props - Fix the GrowthBook (`project` config key) and LaunchDarkly (`fieldLevelExperiments` usage) docs and drop the stale standalone-repo Studio version / tooling references - Updated dependencies [[`c61bb44`](c61bb44)]: - @sanity/studio-secrets@4.0.15 ## sanity-plugin-cloudinary@2.1.0 ### Minor Changes - [#1233](#1233) [`36f3fe2`](36f3fe2) Thanks [@ChrisLaRocque](https://github.com/ChrisLaRocque)! - Add performant preview images Asset previews and diffs now build an optimized, scaled-down preview URL with `@cloudinary/url-gen` (a 400px-wide transformation) when a cloud name is configured, instead of loading the full-size original. This keeps the Studio fast when previewing large Cloudinary assets, and falls back to the stored asset URL when no cloud name is available. - [#1227](#1227) [`3b02635`](3b02635) Thanks [@pgurley](https://github.com/pgurley), [@atlvis](https://github.com/atlvis)! - Add `cloudinaryReferencePlugin` for storing Cloudinary assets as reusable document references - New `cloudinaryReferencePlugin` registers the schema types needed to reference Cloudinary assets as documents - New `cloudinaryAssetDocument` type stores a Cloudinary asset as a standalone document - New `cloudinaryAssetReference` type references those asset documents, with a custom input for selecting and managing assets through the Cloudinary Media Library - `openMediaSelector` now supports a `showHandler` callback and a `folder` option, so the select button can show a loading state and scope the library to a folder - Fixed the internal name of `cloudinaryAssetSourcePlugin` (`cloudinart-asset-source` → `cloudinary-asset-source`) ### Patch Changes - [#1236](#1236) [`7ec89b6`](7ec89b6) Thanks [@stipsan](https://github.com/stipsan)! - - Fix the `cloudinaryAssetSourcePlugin` name, which was mistakenly registered as `cloudinart-asset-source` - Wait for the Cloudinary Media Library script to finish loading before opening it, avoiding runtime errors when several inputs mount at once - Fix a user-facing typo in the asset source loading message ("Media Libary" → "Media Library") - Remove an invalid `src`-less `<track>` element from the video preview - Correct the README usage examples (`defineConfg` → `defineConfig`) and drop stale standalone-repo "Develop & test" / "Release new version" instructions - Updated dependencies [[`c61bb44`](c61bb44)]: - @sanity/studio-secrets@4.0.15 ## sanity-plugin-media@6.1.0 ### Minor Changes - [#1119](#1119) [`ab39674`](ab39674) Thanks [@lud-hu](https://github.com/lud-hu)! - Add an "Edit Media" asset source so media details (alt text, title, tags, etc.) can be edited directly from an image or file field on a document, without opening the full Media tool. - [#1117](#1117) [`6f4316d`](6f4316d) Thanks [@nickeforsberg](https://github.com/nickeforsberg)! - Add an `excludeTags` option that hides assets referencing the listed `media.tag` slugs (`name.current` values). Excluded assets are omitted from the Media browser grid and asset-picker queries, and the matching tags are hidden from the tag sidebar and tag search facet. The asset edit dialog still lists all tags so you can assign or remove them on an open asset. - [#1121](#1121) [`30fa7e2`](30fa7e2) Thanks [@bobbygeo](https://github.com/bobbygeo), [@pedrobonamin](https://github.com/pedrobonamin)! - Add folder management to the media browser - Organise assets into nested folders, backed by a new `media.folder` document type (a `name` plus a weak `parent` reference) and a single weak `opt.media.folder` reference on each asset (mirroring how tags work). Renaming a folder is a one-field document write regardless of how many assets it contains. - Browse folders in a dedicated sidebar tree, with breadcrumb navigation, create / rename / delete flows, a bulk "Move to folder" dialog, and per-asset folder controls in the asset details dialog. The default "All assets" view lists every asset; opening a folder filters the list to that folder. - Deleting a folder removes only the folder document — its assets stay in the library with their folder assignment cleared, and nested folders move up one level. - Support multi-select insert when the media plugin is used as an asset source for array (multiple) fields. - [#1115](#1115) [`e7ec6e6`](e7ec6e6) Thanks [@am0wa](https://github.com/am0wa)! - Extract `image` (`sanity.imageExifTags`) metadata on asset upload and include it in asset queries. The asset edit dialog now pre-fills the Description field from the image's `ImageDescription` EXIF tag as a fallback for image assets. - [#1118](#1118) [`186e29c`](186e29c) Thanks [@Polleke007](https://github.com/Polleke007), [@joepvandepol](https://github.com/joepvandepol)! - Add the ability to replace a selected asset with another one and update all of its references across documents When a single asset is selected, a new **Replace** action opens an overview where you can pick a replacement asset. Every document that references the original asset (including deeply nested image fields) is re-pointed to the chosen asset. Ported from sanity-io/sanity-plugin-media#236. ### Patch Changes - [#1108](#1108) [`1dc3a7b`](1dc3a7b) Thanks [@oxygensmith](https://github.com/oxygensmith)! - Add `crossOrigin="anonymous"` to the image thumbnail component so thumbnails load in Firefox. Firefox's Opaque Response Blocking would otherwise block Sanity CDN asset responses (which send `Vary: Origin`) when requested without an `Origin` header. ## @sanity/plugin-kit@10.0.3 ### Patch Changes - [#1698](#1698) [`d3dedd3`](d3dedd3) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency oxlint to ^1.76.0 - [#1779](#1779) [`5555795`](5555795) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency oxfmt to ^0.61.0 - [#1799](#1799) [`93858fc`](93858fc) Thanks [@stipsan](https://github.com/stipsan)! - Prefer function components via `react/prefer-function-component` (with `allowErrorBoundary`) instead of banning `Component`/`PureComponent` imports ## @sanity/cross-dataset-duplicator@2.0.13 ### Patch Changes - [#1228](#1228) [`71d77d5`](71d77d5) Thanks [@rohanvachheta](https://github.com/rohanvachheta)! - Handle reference errors during duplication by fetching missing referenced documents (including transitive refs, respecting `filter`), re-uploading any recovered assets, and retrying the transaction, with a one-by-one commit fallback - Updated dependencies [[`c61bb44`](c61bb44)]: - @sanity/studio-secrets@4.0.15 ## @sanity/embeddings-index-ui@4.0.13 ### Patch Changes - [#1792](#1792) [`c61bb44`](c61bb44) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency react-rx to ^4.2.5 ## @sanity/form-toolkit@3.0.13 ### Patch Changes - [#1234](#1234) [`1dd36c8`](1dd36c8) Thanks [@stipsan](https://github.com/stipsan)! - Address post-migration review feedback: - `FormRenderer` now renders interactive (uncontrolled) inputs when no `getFieldState` is provided, so the documented native HTML form usage works out of the box - Uncontrolled text and textarea fields honor `options.defaultValue` (controlled fields keep form-library state only, so UI and submit values stay in sync) - `FormRenderer` falls back to `field.name` for the React key when a field has no `_key` - The shared HubSpot/Mailchimp request handler defaults to the Next.js-compatible handler instead of throwing when no framework environment variable is detected, and short-circuits CORS preflight (`OPTIONS`) requests - Corrected the `formSchema`, `formiumInput`, and `mailchimpInput` usage examples (import paths and required options), README typos, and example/dev-workflow references - Updated dependencies [[`f4e7fcc`](f4e7fcc)]: - @sanity/sanity-plugin-async-list@3.0.0 ## @sanity/language-filter@5.0.14 ### Patch Changes - [#1792](#1792) [`c61bb44`](c61bb44) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency react-rx to ^4.2.5 ## @sanity/studio-secrets@4.0.15 ### Patch Changes - [#1792](#1792) [`c61bb44`](c61bb44) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency react-rx to ^4.2.5 ## sanity-plugin-hotspot-array@5.0.12 ### Patch Changes - [#1789](#1789) [`9628df4`](9628df4) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency motion to ^12.43.0 ## sanity-plugin-iframe-pane@5.0.30 ### Patch Changes - [#1789](#1789) [`9628df4`](9628df4) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency motion to ^12.43.0 ## sanity-plugin-internationalized-array@5.1.25 ### Patch Changes - [#1765](#1765) [`97937c4`](97937c4) Thanks [@pedrobonamin](https://github.com/pedrobonamin)! - Add a stable `data-testid` on the document-level add-translations panel for e2e coverage ## sanity-plugin-mux-input@5.0.8 ### Patch Changes - [#1792](#1792) [`c61bb44`](c61bb44) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency react-rx to ^4.2.5 ## sanity-plugin-studio-smartling@5.0.14 ### Patch Changes - [#1216](#1216) [`d4dd45b`](d4dd45b) Thanks [@stipsan](https://github.com/stipsan)! - Harden the Smartling adapter and fix docs: - Send the secret credentials JSON verbatim during authentication instead of double-encoding it, which could break auth with proxies that forward the request body as-is - Throw a clear error (surfacing Smartling's message) when authentication does not return an access token, instead of a cryptic `TypeError` - Avoid throwing when no existing job is found or when a translation/progress response is missing its expected payload - Guard the progress calculation against a zero total word count so empty documents no longer report `NaN`/`Infinity` - Fix the `additionalDeserializers` option name in the advanced configuration docs (was `additonalDeserializers`) and update the README development/release instructions for the monorepo ## sanity-plugin-workflow@3.0.39 ### Patch Changes - [#1789](#1789) [`9628df4`](9628df4) Thanks [@squiggler-app](https://github.com/apps/squiggler-app)! - fix(deps): update dependency motion to ^12.43.0 ## @sanity/document-internationalization@6.2.28 ## @sanity/sfcc@1.0.25 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > <sup>[Cursor Bugbot](https://cursor.com/bugbot) is generating a summary for commit 9246b02. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>

Ports sanity-io/sanity-plugin-personalization#38 into the monorepo.
Original author: @jjburbridge (credited via the
author:directive in the changeset and as the git commit author).Walkthrough
Field-level personalization on the Tagline field: activate via the people icon, add a segment, and "Copy default" to seed the value.
field_level_personalization_demo.mp4
Segments section with Add Enterprise / Small Business / Individual buttons
Copy default populates the segment value
What this adds
fieldLevelPersonalizationplugin export — segment-based field personalization (the personalization counterpart tofieldLevelExperiments). Segments can be provided as a static array, fetched from an external API, or stored as Sanity documents (async resolver receives the client).src/components/experiment/and the new personalization components live insrc/components/personalization/, each with a barrelindex.ts. The shared array item component is renamedExperimentItem→ArrayItem.default/experiments(orpersonalization) /all-fieldsgroups, and aclearChildrenGroupshelper clears nested group state so fields render cleanly inside arrays and modals.FieldPluginConfigis renamed toExperimentFieldPluginConfig, with a deprecatedFieldPluginConfigtype alias kept for compatibility. NewPersonalizationFieldPluginConfig,PersonalizationContextProps, andPersonalizationArrayInputPropstypes are added.fieldLevelPersonalizationwith sample segments (Enterprise / Small Business / Individual).Not ported: the upstream
CHANGELOG.md/package.jsonversion bumps from semantic-release (the monorepo uses changesets — aminorchangeset is included instead).Review feedback addressed
apiVersionin previews"undefined"segment titles; null-safe preview path traversalTesting
pnpm formatpnpm lint(type-aware oxlint) — passespnpm --filter @sanity/personalization-plugin build— passesTo show artifacts inline, enable in settings.