@zachariaz/strapi-plugin-content-variants 0.1.0

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.
Files changed (43) hide show
  1. package/README.md +600 -0
  2. package/dist/_chunks/Segments-BREqC60L.js +330 -0
  3. package/dist/_chunks/Segments-BgxnvvtR.mjs +330 -0
  4. package/dist/_chunks/en-Bnfrhhim.js +62 -0
  5. package/dist/_chunks/en-e_966kWj.mjs +62 -0
  6. package/dist/_chunks/index-DVoZM8JU.js +1036 -0
  7. package/dist/_chunks/index-Dj2sexmk.mjs +1020 -0
  8. package/dist/admin/index.js +3 -0
  9. package/dist/admin/index.mjs +4 -0
  10. package/dist/admin/src/components/Initializer.d.ts +5 -0
  11. package/dist/admin/src/components/SegmentPickerAction.d.ts +7 -0
  12. package/dist/admin/src/components/VariantInfoAction.d.ts +8 -0
  13. package/dist/admin/src/components/VariantPanel.d.ts +13 -0
  14. package/dist/admin/src/components/VariantPickerAction.d.ts +20 -0
  15. package/dist/admin/src/contentManagerHooks/editView.d.ts +6 -0
  16. package/dist/admin/src/contentManagerHooks/listView.d.ts +22 -0
  17. package/dist/admin/src/hooks/useSegments.d.ts +17 -0
  18. package/dist/admin/src/hooks/useVariantFamily.d.ts +19 -0
  19. package/dist/admin/src/hooks/useVariantLinks.d.ts +44 -0
  20. package/dist/admin/src/index.d.ts +11 -0
  21. package/dist/admin/src/pages/Settings/Segments.d.ts +2 -0
  22. package/dist/admin/src/pluginId.d.ts +2 -0
  23. package/dist/admin/src/utils/batchLinkFetcher.d.ts +11 -0
  24. package/dist/admin/src/utils/variants.d.ts +13 -0
  25. package/dist/server/index.js +895 -0
  26. package/dist/server/index.mjs +896 -0
  27. package/dist/server/src/bootstrap.d.ts +17 -0
  28. package/dist/server/src/config/index.d.ts +5 -0
  29. package/dist/server/src/content-types/index.d.ts +121 -0
  30. package/dist/server/src/controllers/index.d.ts +24 -0
  31. package/dist/server/src/controllers/segment.d.ts +11 -0
  32. package/dist/server/src/controllers/variant-link.d.ts +18 -0
  33. package/dist/server/src/destroy.d.ts +5 -0
  34. package/dist/server/src/index.d.ts +244 -0
  35. package/dist/server/src/register.d.ts +5 -0
  36. package/dist/server/src/routes/admin.d.ts +12 -0
  37. package/dist/server/src/routes/content-api.d.ts +19 -0
  38. package/dist/server/src/routes/index.d.ts +25 -0
  39. package/dist/server/src/services/index.d.ts +62 -0
  40. package/dist/server/src/services/segment.d.ts +14 -0
  41. package/dist/server/src/services/variant-link.d.ts +60 -0
  42. package/dist/server/src/services/variant-resolver.d.ts +23 -0
  43. package/package.json +104 -0
package/README.md ADDED
@@ -0,0 +1,600 @@
1
+ # @solteq/strapi-plugin-content-variants
2
+
3
+ Strapi v5 plugin for segment-based content personalization. Define audience segments, mark fields as variant-aware, and serve different content to different user groups -- all within Strapi's existing component system.
4
+
5
+ ## Current State
6
+
7
+ ### Working -- Verified in Browser
8
+
9
+ #### Phase 1: Segment Model -- `plugin::content-variants.segment`
10
+
11
+ A collection type storing audience segment definitions.
12
+
13
+ - **Fields**: `name` (string, unique, required), `slug` (string, unique, required, auto-generated from name), `description` (text, optional), `externalId` (string, optional -- for future CDP integration)
14
+ - **No draft/publish** -- segments are always active
15
+ - **Server**: CRUD service using `strapi.documents()`, controller, admin-only routes at `GET|POST /content-variants/segments`, `GET|PUT|DELETE /content-variants/segments/:id`
16
+ - **Content API**: Read-only `GET /api/content-variants/segments` for frontends
17
+
18
+ **Files**: `server/src/content-types/segment/schema.json`, `server/src/services/segment.ts`, `server/src/controllers/segment.ts`, `server/src/routes/admin.ts`, `server/src/routes/content-api.ts`
19
+
20
+ #### Phase 1: Segments Settings Page
21
+
22
+ Admin page under **Settings > Global Settings > Content Variants** (`/admin/settings/content-variants`).
23
+
24
+ - Table of all segments with Name, Slug, External ID, Description columns
25
+ - Edit and Delete action buttons per row
26
+ - "Add Segment" opens an inline form with auto-slug generation from name
27
+ - Delete shows a confirm dialog
28
+
29
+ **Files**: `admin/src/pages/Settings/Segments.tsx`, `admin/src/hooks/useSegments.ts`
30
+
31
+ #### Phase 2: CTB Content-Type-Level Toggle
32
+
33
+ "Enable content variants" checkbox in the **Content-Type Builder** when editing any content type's Advanced Settings.
34
+
35
+ - Stores `pluginOptions['content-variants'].enabled: true` in the content type schema
36
+ - Appears alongside "Draft & publish" and "Internationalization" checkboxes
37
+
38
+ **Location**: CTB > click content type > Edit > Advanced Settings tab
39
+
40
+ #### Phase 2: CTB Per-Field Variant Checkbox
41
+
42
+ "Enable variants for this field" checkbox in Advanced Settings of **string, text, richtext, media, and blocks** fields within components.
43
+
44
+ - Stores `pluginOptions['content-variants'].variant: true` on the field schema
45
+ - Only appears for fields belonging to **components** (`forTarget === 'component'`), since variants live inside dynamic zone components
46
+
47
+ **Location**: CTB > select component (e.g., Hero) > Edit field > Advanced Settings tab
48
+
49
+ #### Phase 3: Edit View Sparkle Indicators
50
+
51
+ Sparkle icon badge on variant-enabled fields in the Content Manager edit view, so editors can see at a glance which fields have per-segment values.
52
+
53
+ - Registered via `registerHook('Admin/CM/pages/EditView/mutate-edit-view-layout')`
54
+ - Adds a tooltip: "This field has per-segment variants"
55
+ - Only active when the content type has `pluginOptions['content-variants'].enabled: true`
56
+ - **Verified working** -- sparkle icons appear next to Title, Description, Image fields
57
+
58
+ **Files**: `admin/src/contentManagerHooks/editView.tsx`
59
+
60
+ ---
61
+
62
+ #### Phase 3: Segment Picker Header Action
63
+
64
+ Dropdown in the Content Manager edit view header (next to locale picker) for switching between "Default" and segment-specific variant views.
65
+
66
+ - Shows "Default" option plus all defined segments
67
+ - Segments with existing variants show a checkmark; segments without show "(no variant)"
68
+ - Selecting a segment swaps variant-enabled field values in the form using `setValues()`
69
+ - Stores active segment in URL query params: `?plugins[content-variants][segment]=slug`
70
+ - Preserves default values when switching and writes back edits to the correct variant slot
71
+ - **Depends on**: components having a `variants[]` repeatable component field
72
+
73
+ **Files**: `admin/src/components/SegmentPickerAction.tsx`
74
+
75
+ #### Phase 3: Variant Management Side Panel
76
+
77
+ "VARIANTS" panel in the edit view right sidebar (alongside ENTRY and PREVIEW).
78
+
79
+ - Lists all dynamic zone components that support variants
80
+ - Shows each variant with its segment assignments and priority
81
+ - "Add variant" button creates a new empty variant in the component's `variants[]` array
82
+ - Per-variant: assign/remove segments with priority number input
83
+ - Delete variant action
84
+
85
+ **Files**: `admin/src/components/VariantPanel.tsx`
86
+
87
+ #### Phase 3: List View Variant Count Column
88
+
89
+ "Variants" column in the Content Manager list view showing variant count per document.
90
+
91
+ - Registered via `registerHook('Admin/CM/pages/ListView/inject-column-in-table')`
92
+ - Shows a badge with the count, or "--" if no variants
93
+ - Only injected for content types with variants enabled
94
+
95
+ **Files**: `admin/src/contentManagerHooks/listView.tsx`
96
+
97
+ #### Phase 3: Variant Utilities
98
+
99
+ Core utility functions for admin-side variant detection and value manipulation.
100
+
101
+ - `isVariantEnabledContentType()`, `isVariantField()`, `getVariantFieldNames()`
102
+ - `hasVariantsField()`, `getDynamicZoneFields()`, `getVariantComponentUIDs()`
103
+ - `findVariantForSegment()`, `extractFieldValues()`, `buildSwappedFormValues()`
104
+ - `writeBackToVariant()`, `countVariants()`, `getSegmentSlugsWithVariants()`
105
+
106
+ **Files**: `admin/src/utils/variants.ts`
107
+
108
+ #### Phase 4: Variant Resolver Service
109
+
110
+ Server-side service that resolves variant fields given a segment slug.
111
+
112
+ - Walks all dynamic zone components in a document
113
+ - Finds the variant matching the segment with highest priority (lowest number)
114
+ - Merges variant field values over the defaults
115
+ - Strips the `variants[]` array from resolved output
116
+
117
+ **Files**: `server/src/services/variant-resolver.ts`
118
+
119
+ #### Phase 4: Document Service Middleware
120
+
121
+ Intercepts `findMany`/`findOne` Document Service operations for REST API segment filtering.
122
+
123
+ - When a `segment` parameter is present in the request, resolves variants server-side
124
+ - Returns flat, resolved content (variant fields merged, variants array removed)
125
+ - Handles both single documents and paginated results
126
+
127
+ **Files**: `server/src/bootstrap.ts`
128
+
129
+ ---
130
+
131
+ ## Variant Data Model
132
+
133
+ Variants are stored as Strapi components embedded inside the main component:
134
+
135
+ ```
136
+ Hero (component in dynamic zone)
137
+ ├── title (default value -- fallback, variant:true)
138
+ ├── description (default value -- fallback, variant:true)
139
+ ├── image (default media -- fallback, variant:true)
140
+ └── variants[] (repeatable component: zone.hero-variant)
141
+ ├── title (variant-specific override)
142
+ ├── description (variant-specific override)
143
+ ├── image (variant-specific media)
144
+ └── segmentAssignments[] (repeatable: shared.segment-assignment)
145
+ ├── segment (relation → plugin::content-variants.segment)
146
+ └── priority (integer, lower = higher priority)
147
+ ```
148
+
149
+ The plugin does **not** auto-generate these components. Developers create them following the naming convention, or use the "Scaffold Demo Content" feature (not yet built).
150
+
151
+ ### Required shared components (created in host project)
152
+
153
+ **shared.segment-assignment** (`src/components/shared/segment-assignment.json`):
154
+ ```json
155
+ {
156
+ "collectionName": "components_shared_segment_assignments",
157
+ "info": { "displayName": "Segment Assignment" },
158
+ "attributes": {
159
+ "segment": {
160
+ "type": "relation",
161
+ "relation": "oneToOne",
162
+ "target": "plugin::content-variants.segment"
163
+ },
164
+ "priority": {
165
+ "type": "integer",
166
+ "default": 0,
167
+ "min": 0
168
+ }
169
+ }
170
+ }
171
+ ```
172
+
173
+ **zone.hero-variant** (`src/components/zone/hero-variant.json`) -- mirrors variant-enabled fields + segmentAssignments:
174
+ ```json
175
+ {
176
+ "collectionName": "components_zone_hero_variants",
177
+ "info": { "displayName": "Hero Variant" },
178
+ "attributes": {
179
+ "Title": { "type": "string" },
180
+ "Description": { "type": "blocks" },
181
+ "Image": { "type": "media", "multiple": false },
182
+ "segmentAssignments": {
183
+ "type": "component",
184
+ "repeatable": true,
185
+ "component": "shared.segment-assignment"
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ Then add `variants` field to the parent Hero component:
192
+ ```json
193
+ "variants": {
194
+ "type": "component",
195
+ "repeatable": true,
196
+ "component": "zone.hero-variant"
197
+ }
198
+ ```
199
+
200
+ ## Plugin Architecture
201
+
202
+ ```
203
+ strapi-plugin-content-variants/
204
+ ├── package.json
205
+ ├── admin/
206
+ │ ├── custom.d.ts
207
+ │ ├── tsconfig.json
208
+ │ ├── tsconfig.build.json
209
+ │ └── src/
210
+ │ ├── index.tsx # register + bootstrap (CTB, CM, hooks)
211
+ │ ├── pluginId.ts
212
+ │ ├── components/
213
+ │ │ ├── Initializer.tsx
214
+ │ │ ├── SegmentPickerAction.tsx # Header dropdown for segment selection
215
+ │ │ └── VariantPanel.tsx # Side panel for variant management
216
+ │ ├── contentManagerHooks/
217
+ │ │ ├── editView.tsx # Variant field indicators (sparkle badge)
218
+ │ │ └── listView.tsx # Variant count column
219
+ │ ├── hooks/
220
+ │ │ └── useSegments.ts # Fetch/manage segments via admin API
221
+ │ ├── pages/
222
+ │ │ └── Settings/
223
+ │ │ └── Segments.tsx # Settings page: segment CRUD table + form
224
+ │ ├── translations/
225
+ │ │ └── en.json
226
+ │ └── utils/
227
+ │ └── variants.ts # Variant detection and value manipulation
228
+ └── server/
229
+ ├── tsconfig.json
230
+ ├── tsconfig.build.json
231
+ └── src/
232
+ ├── index.ts # Exports all server modules
233
+ ├── register.ts # Plugin register lifecycle
234
+ ├── bootstrap.ts # Document Service middleware registration
235
+ ├── destroy.ts # Plugin destroy lifecycle
236
+ ├── config/
237
+ │ └── index.ts # Plugin config defaults
238
+ ├── content-types/
239
+ │ ├── index.ts # Exports { segment }
240
+ │ └── segment/
241
+ │ └── schema.json # Segment model definition
242
+ ├── controllers/
243
+ │ ├── index.ts # Exports { segment }
244
+ │ └── segment.ts # Segment CRUD controller
245
+ ├── routes/
246
+ │ ├── index.ts # Exports { admin, 'content-api' }
247
+ │ ├── admin.ts # Admin-only CRUD routes for /segments
248
+ │ └── content-api.ts # Public read-only /segments route
249
+ └── services/
250
+ ├── index.ts # Exports { segment, 'variant-resolver' }
251
+ ├── segment.ts # Segment CRUD service with auto-slug
252
+ └── variant-resolver.ts # Resolve variants by segment
253
+ ```
254
+
255
+ ## Installation
256
+
257
+ Install the plugin in your Strapi v5 project:
258
+
259
+ ```bash
260
+ npm install @zachariaz/strapi-plugin-content-variants
261
+ ```
262
+
263
+ Enable it in `config/plugins.ts` (or `.js`):
264
+
265
+ ```typescript
266
+ export default {
267
+ 'content-variants': {
268
+ enabled: true,
269
+ },
270
+ };
271
+ ```
272
+
273
+ Restart Strapi. The plugin registers its admin pages, Content-Type Builder extensions, and Content Manager hooks on boot.
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ # Build plugin
279
+ npm run build # or: npx @strapi/pack-up build
280
+
281
+ # Watch mode
282
+ npm run watch
283
+ ```
284
+
285
+ ## REST API Usage
286
+
287
+ The plugin intercepts standard Strapi Content API calls via Document Service middleware. No special endpoints needed — just add query parameters to your existing API calls.
288
+
289
+ ### Authentication
290
+
291
+ Storefronts and frontends authenticate with a **Strapi API Token** (Bearer token). Create one at **Settings > API Tokens** (`/admin/settings/api-tokens`):
292
+
293
+ - **Token type**: Read-only
294
+ - **Permissions**: Enable `find` and `findOne` for each content type the storefront needs (e.g., `hero-banner`, `campaign`) plus `find` for `content-variants` plugin (segments endpoint)
295
+
296
+ All Content API calls require the `Authorization` header:
297
+
298
+ ```
299
+ Authorization: Bearer <your-api-token>
300
+ ```
301
+
302
+ ### Query Parameters
303
+
304
+ | Parameter | Description |
305
+ |-----------|-------------|
306
+ | `segment` | Segment slug. Resolves variant fields server-side, returns flat merged content. |
307
+ | `includeVariants` | Set to `true` to return base content + `_variants[]` array with all variant data. |
308
+ | `locale` | Standard Strapi i18n locale (e.g., `en`, `fi`). |
309
+ | `status` | `draft` or `published`. Default: `published`. Use `draft` to see unpublished content. |
310
+ | `populate` | Standard Strapi populate (e.g., `*`, `heroBanner`, `image`). |
311
+
312
+ ### Important: Draft vs Published
313
+
314
+ By default, the Content API returns **published** content only. Variant resolution only works if both the base document and the variant documents have been published. If variant documents are draft-only, the published base content won't be resolved.
315
+
316
+ Use `status=draft` during development to test with unpublished content. In production, make sure to publish both base and variant documents.
317
+
318
+ ### API Test Calls (Postman / curl)
319
+
320
+ Below are example calls using the test data. Replace `localhost:1337` with your Strapi host.
321
+
322
+ All calls require the `Authorization: Bearer <token>` header. In curl:
323
+
324
+ ```bash
325
+ curl -H 'Authorization: Bearer <your-api-token>' 'http://localhost:1337/api/...'
326
+ ```
327
+
328
+ In Postman, set the Authorization type to "Bearer Token" and paste the token value.
329
+
330
+ ---
331
+
332
+ #### 1. List hero banners — base content (no segment)
333
+
334
+ Returns default field values without variant resolution.
335
+
336
+ ```
337
+ GET http://localhost:1337/api/hero-banners?locale=en&status=draft&populate=*
338
+ Authorization: Bearer <token>
339
+ ```
340
+
341
+ Response (trimmed to first entry):
342
+ ```json
343
+ {
344
+ "data": [
345
+ {
346
+ "id": 1,
347
+ "documentId": "c0y1hk1245eats3bret72241",
348
+ "title": "Herobanner ",
349
+ "subtitle": "hero subtitle",
350
+ "ctaLabel": "Click me",
351
+ "ctaUrl": "#",
352
+ "locale": "en",
353
+ "image": null
354
+ }
355
+ ],
356
+ "meta": { "pagination": { "page": 1, "pageSize": 25, "pageCount": 1, "total": 5 } }
357
+ }
358
+ ```
359
+
360
+ ---
361
+
362
+ #### 2. List hero banners — resolved for a segment
363
+
364
+ The `segment` parameter triggers server-side variant resolution. Variant-marked fields are replaced with segment-specific values.
365
+
366
+ ```
367
+ GET http://localhost:1337/api/hero-banners?locale=en&status=draft&segment=new-members&populate=*
368
+ Authorization: Bearer <token>
369
+ ```
370
+
371
+ Response — note `title` changed from `"Herobanner "` to `"Herobanner for new members"`:
372
+ ```json
373
+ {
374
+ "data": [
375
+ {
376
+ "id": 1,
377
+ "documentId": "c0y1hk1245eats3bret72241",
378
+ "title": "Herobanner for new members",
379
+ "subtitle": "hero subtitle",
380
+ "ctaLabel": "Click me",
381
+ "ctaUrl": "#",
382
+ "locale": "en",
383
+ "image": null
384
+ }
385
+ ]
386
+ }
387
+ ```
388
+
389
+ ---
390
+
391
+ #### 3. Single hero banner — resolved for a different segment
392
+
393
+ ```
394
+ GET http://localhost:1337/api/hero-banners/c0y1hk1245eats3bret72241?locale=en&status=draft&segment=club-members&populate=*
395
+ Authorization: Bearer <token>
396
+ ```
397
+
398
+ Response — `title` resolved for club-members segment:
399
+ ```json
400
+ {
401
+ "data": {
402
+ "id": 1,
403
+ "documentId": "c0y1hk1245eats3bret72241",
404
+ "title": "Herobanner for club",
405
+ "subtitle": "hero subtitle",
406
+ "ctaLabel": "Click me",
407
+ "ctaUrl": "#",
408
+ "locale": "en",
409
+ "image": null
410
+ }
411
+ }
412
+ ```
413
+
414
+ ---
415
+
416
+ #### 4. Single hero banner — enriched mode with all variants
417
+
418
+ The `includeVariants=true` parameter returns base content plus a `_variants[]` array listing every variant with its segment assignments and overridden field values.
419
+
420
+ ```
421
+ GET http://localhost:1337/api/hero-banners/c0y1hk1245eats3bret72241?locale=en&status=draft&includeVariants=true&populate=*
422
+ Authorization: Bearer <token>
423
+ ```
424
+
425
+ Response:
426
+ ```json
427
+ {
428
+ "data": {
429
+ "id": 1,
430
+ "documentId": "c0y1hk1245eats3bret72241",
431
+ "title": "Herobanner ",
432
+ "subtitle": "hero subtitle",
433
+ "ctaLabel": "Click me",
434
+ "ctaUrl": "#",
435
+ "locale": "en",
436
+ "_variants": [
437
+ {
438
+ "documentId": "j55u6yrwiukbdbz50tsir7g1",
439
+ "segments": [
440
+ { "name": "New members", "slug": "new-members" }
441
+ ],
442
+ "fields": {
443
+ "title": "Herobanner for new members",
444
+ "subtitle": "hero subtitle",
445
+ "ctaLabel": "Click me"
446
+ }
447
+ },
448
+ {
449
+ "documentId": "q65aizvvuneke6lpnxpglsoj",
450
+ "segments": [
451
+ { "name": "Club Members", "slug": "club-members" }
452
+ ],
453
+ "fields": {
454
+ "title": "Herobanner for club",
455
+ "subtitle": "hero subtitle",
456
+ "ctaLabel": "Click me"
457
+ }
458
+ },
459
+ {
460
+ "documentId": "mkjtwxoyc509uedvsbh6uq5b",
461
+ "segments": [
462
+ { "name": "Superbuyers", "slug": "superbyers" }
463
+ ],
464
+ "fields": {
465
+ "title": "Herobanner superbyers",
466
+ "subtitle": "hero subtitle",
467
+ "ctaLabel": "Click me"
468
+ }
469
+ }
470
+ ]
471
+ }
472
+ }
473
+ ```
474
+
475
+ ---
476
+
477
+ #### 5. Campaigns with relation — no segment
478
+
479
+ Campaigns have a `heroBanner` relation. Without `segment`, the related hero banner returns base (default) field values.
480
+
481
+ ```
482
+ GET http://localhost:1337/api/campaigns?locale=en&status=draft&populate=heroBanner
483
+ Authorization: Bearer <token>
484
+ ```
485
+
486
+ Response:
487
+ ```json
488
+ {
489
+ "data": [
490
+ {
491
+ "id": 1,
492
+ "documentId": "nwqo1liifvnuz6iv5eb2riu1",
493
+ "title": "Summer campaign",
494
+ "slug": "summercampaign",
495
+ "description": "Description",
496
+ "locale": "en",
497
+ "heroBanner": {
498
+ "id": 1,
499
+ "documentId": "c0y1hk1245eats3bret72241",
500
+ "title": "Herobanner ",
501
+ "subtitle": "hero subtitle",
502
+ "ctaLabel": "Click me",
503
+ "ctaUrl": "#",
504
+ "locale": "en"
505
+ }
506
+ }
507
+ ]
508
+ }
509
+ ```
510
+
511
+ ---
512
+
513
+ #### 6. Campaigns with relation — segment resolution propagates through relations
514
+
515
+ When `segment` is set, variant resolution propagates into populated relations. The hero banner's variant-marked fields are resolved for the segment.
516
+
517
+ ```
518
+ GET http://localhost:1337/api/campaigns?locale=en&status=draft&segment=new-members&populate=heroBanner
519
+ Authorization: Bearer <token>
520
+ ```
521
+
522
+ Response — the `heroBanner.title` is now resolved for `new-members`:
523
+ ```json
524
+ {
525
+ "data": [
526
+ {
527
+ "id": 1,
528
+ "documentId": "nwqo1liifvnuz6iv5eb2riu1",
529
+ "title": "Summer campaign",
530
+ "slug": "summercampaign",
531
+ "description": "Description",
532
+ "locale": "en",
533
+ "heroBanner": {
534
+ "id": 1,
535
+ "documentId": "c0y1hk1245eats3bret72241",
536
+ "title": "Herobanner for new members",
537
+ "subtitle": "hero subtitle",
538
+ "ctaLabel": "Click me",
539
+ "ctaUrl": "#",
540
+ "locale": "en"
541
+ }
542
+ }
543
+ ]
544
+ }
545
+ ```
546
+
547
+ ---
548
+
549
+ #### 7. List available segments
550
+
551
+ Returns all defined segments. The API token must have `find` permission for the `content-variants` plugin.
552
+
553
+ ```
554
+ GET http://localhost:1337/api/content-variants/segments
555
+ Authorization: Bearer <token>
556
+ ```
557
+
558
+ Response:
559
+ ```json
560
+ [
561
+ {
562
+ "id": 2,
563
+ "documentId": "nlhcms3seykmet4bniz2z794",
564
+ "name": "New members",
565
+ "slug": "new-members",
566
+ "description": null,
567
+ "externalId": null
568
+ },
569
+ {
570
+ "id": 1,
571
+ "documentId": "qf7hz4xyuo2rlkr74hnfa20c",
572
+ "name": "Club Members",
573
+ "slug": "club-members",
574
+ "description": null,
575
+ "externalId": null
576
+ },
577
+ {
578
+ "id": 3,
579
+ "documentId": "iww0h4gt6z2wka0o92hg25co",
580
+ "name": "Superbuyers",
581
+ "slug": "superbyers",
582
+ "description": null,
583
+ "externalId": null
584
+ }
585
+ ]
586
+ ```
587
+
588
+ Use the `slug` values from this response as the `segment` query parameter in content API calls.
589
+
590
+ ---
591
+
592
+ ### How Variant Resolution Works
593
+
594
+ 1. **Without `segment`**: Returns the base document with default field values. No variant data is included unless `includeVariants=true` is set.
595
+
596
+ 2. **With `segment=slug`**: The Document Service middleware intercepts the response. For each variant-enabled content type, it finds the variant link matching the segment slug, fetches the variant document, and merges its variant-marked fields over the base document's fields. The response looks identical to a normal Strapi response — the frontend doesn't need to know about variants.
597
+
598
+ 3. **With `includeVariants=true`**: Returns the base document as-is, plus a `_variants[]` array. Each entry contains the variant's `documentId`, `segments` (name + slug), and `fields` (the overridden field values). Useful for client-side resolution or preview UIs.
599
+
600
+ 4. **Relations**: Segment resolution propagates through `populate`. If a Campaign populates its `heroBanner` relation and `segment=new-members` is set, the heroBanner's fields are resolved for that segment too.