Skill 01 · Content Experimentation Best Practices
Subchapter 1.1
references/cms-integration.mdMarkdown7 KBView on GitHub
Integrating experimentation with your CMS enables content teams to run tests without developer intervention.
Store experiment variants as content in the CMS.
Pros: Content team autonomy, version controlled Cons: More complex queries, potential publish coordination
// 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' }),
]
})Store variants as fields on the content document.
Pros: Simpler queries, content stays together Cons: Less flexible, schema complexity
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' }),
]
})Use dedicated tools (Optimizely, LaunchDarkly, VWO) with CMS content.
Pros: Robust analytics, proven platforms Cons: Additional cost, integration complexity
// CMS stores experiment IDs, platform handles assignment
defineField({
name: 'experimentId',
type: 'string',
description: 'Optimizely experiment ID'
})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' }),
]
})// 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
]
}]
})// 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
}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.
*[_type == "page" && slug.current == $slug][0]{
...,
"experiment": experimentVariants[
experimentId->_id == $experimentId &&
experimentId->status == "running" &&
variantId == $variantId
][0]{
experimentId->{name, _id},
variantId,
headline
}
}// 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()
})
}// Push to data layer for analytics tools
window.dataLayer.push({
event: 'experiment_assignment',
experiment_id: experimentId,
variant_id: variantId
})