# CMS Integration Patterns Integrating experimentation with your CMS enables content teams to run tests without developer intervention. ## Architecture Options ### 1. CMS-Managed Variants Store experiment variants as content in the CMS. **Pros:** Content team autonomy, version controlled **Cons:** More complex queries, potential publish coordination ```typescript // Experiment document defineType({ name: 'experiment', type: 'document', fields: [ defineField({ name: 'name', type: 'string' }), defineField({ name: 'status', type: 'string', options: { list: ['draft', 'running', 'paused', 'concluded'] }}), defineField({ name: 'variants', type: 'array', of: [{ type: 'object', fields: [ defineField({ name: 'name', type: 'string' }), defineField({ name: 'weight', type: 'number' }), defineField({ name: 'content', type: 'reference', to: [{ type: 'page' }] }), ] }] }), defineField({ name: 'startDate', type: 'datetime' }), defineField({ name: 'endDate', type: 'datetime' }), ] }) ``` ### 2. Field-Level Variants Store variants as fields on the content document. **Pros:** Simpler queries, content stays together **Cons:** Less flexible, schema complexity ```typescript defineType({ name: 'landingPage', fields: [ defineField({ name: 'headline', type: 'string' }), defineField({ name: 'headlineVariantB', type: 'string', description: 'A/B test variant (leave empty if not testing)' }), defineField({ name: 'activeExperiment', type: 'string' }), ] }) ``` ### 3. External Experimentation Platform Use dedicated tools (Optimizely, LaunchDarkly, VWO) with CMS content. **Pros:** Robust analytics, proven platforms **Cons:** Additional cost, integration complexity ```typescript // CMS stores experiment IDs, platform handles assignment defineField({ name: 'experimentId', type: 'string', description: 'Optimizely experiment ID' }) ``` ## Implementation Pattern (CMS-Managed) ### 1. Experiment Schema ```typescript defineType({ name: 'experiment', type: 'document', fields: [ defineField({ name: 'name', type: 'string', validation: r => r.required() }), defineField({ name: 'hypothesis', type: 'text' }), defineField({ name: 'status', type: 'string', options: { list: ['draft', 'running', 'concluded'] }, initialValue: 'draft' }), defineField({ name: 'variants', type: 'array', of: [{ type: 'object', name: 'variant', fields: [ defineField({ name: 'id', type: 'string' }), defineField({ name: 'name', type: 'string' }), defineField({ name: 'weight', type: 'number', initialValue: 50 }), ] }], validation: r => r.min(2).error('Need at least 2 variants') }), defineField({ name: 'targetPage', type: 'reference', to: [{ type: 'page' }] }), defineField({ name: 'targetField', type: 'string' }), ] }) ``` ### 2. Variant Content ```typescript // On the page being tested defineField({ name: 'experimentVariants', type: 'array', of: [{ type: 'object', fields: [ defineField({ name: 'experimentId', type: 'reference', to: [{ type: 'experiment' }] }), defineField({ name: 'variantId', type: 'string' }), defineField({ name: 'headline', type: 'string' }), // Other variant-specific fields ] }] }) ``` ### 3. Frontend Assignment ```typescript // Middleware or server-side. Returns the assigned variant id, or null when the // experiment has no variants to assign. function assignVariant(experimentId: string, variants: Variant[]): string | null { if (variants.length === 0) return null // Reuse an existing assignment, but only if it's still a valid variant. // After variants are renamed or removed, drop the stale cookie and reassign, // otherwise users stay bucketed to IDs that no longer exist. const cookieKey = `exp_${experimentId}` const existing = getCookie(cookieKey) if (existing && variants.some(v => v.id === existing)) return existing // Random assignment based on weights. Normalize against the total weight so // splits work even when weights don't sum to 100 (otherwise draws above the // sum skew to the fallback). const totalWeight = variants.reduce((sum, v) => sum + v.weight, 0) if (totalWeight <= 0) { const fallback = variants[0] setCookie(cookieKey, fallback.id, { maxAge: 30 * 24 * 60 * 60 }) return fallback.id } const rand = Math.random() * totalWeight let cumulative = 0 for (const variant of variants) { cumulative += variant.weight if (rand < cumulative) { setCookie(cookieKey, variant.id, { maxAge: 30 * 24 * 60 * 60 }) return variant.id } } // Fall back to the last variant to absorb any floating-point remainder, and // persist it like any other assignment so the visitor stays bucketed. const fallback = variants[variants.length - 1] setCookie(cookieKey, fallback.id, { maxAge: 30 * 24 * 60 * 60 }) return fallback.id } ``` ### 4. Query with Variant Resolve **one** experiment's assignment at a time, passing both its `experimentId` and the `variantId` returned by `assignVariant`. Match on both: variant IDs like `control` repeat across experiments, so filtering on `variantId` alone could pick a different running experiment's row. For a page running several experiments, resolve each one separately and merge the results. ```groq *[_type == "page" && slug.current == $slug][0]{ ..., "experiment": experimentVariants[ experimentId->_id == $experimentId && experimentId->status == "running" && variantId == $variantId ][0]{ experimentId->{name, _id}, variantId, headline } } ``` ## Analytics Integration ### Event Tracking ```typescript // Track experiment exposure function trackExposure(experimentId: string, variantId: string) { analytics.track('Experiment Viewed', { experimentId, variantId, timestamp: new Date().toISOString() }) } // Track conversion function trackConversion(experimentId: string, variantId: string, metric: string) { analytics.track('Experiment Conversion', { experimentId, variantId, metric, timestamp: new Date().toISOString() }) } ``` ### Data Layer ```typescript // Push to data layer for analytics tools window.dataLayer.push({ event: 'experiment_assignment', experiment_id: experimentId, variant_id: variantId }) ``` ## Best Practices ### Content Team Workflow 1. Create experiment document with hypothesis 2. Create variant content 3. Set status to "running" 4. Monitor results 5. Set status to "concluded" and record winner ### Avoid Flicker - Assign variants server-side when possible - Use CSS to hide content until variant determined - Pre-render both variants, show based on assignment ### Clean Up - Archive concluded experiments - Remove losing variant content - Implement winner as default