{"id":18,"date":"2026-06-06T21:48:38","date_gmt":"2026-06-06T21:48:38","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=18"},"modified":"2026-06-07T01:21:57","modified_gmt":"2026-06-07T01:21:57","slug":"a-b-testing-at-the-ssr-level-cookie-based-variant-selection","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=18","title":{"rendered":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Eleventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why A\/B Testing in SSR Is Different<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a client-side SPA, A\/B testing is simple: check a flag, choose a variant, render.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In SSR, the decision has to happen <strong>before the HTML is generated<\/strong>. The server renders the full page and sends it to the browser. If variant selection happens after the server renders A but the client expects B, hydration breaks (Article 9).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The variant must be fixed at the start of the SSR request, before any component renders. Every component must see the same assignment for the entire request.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Architecture: Cookie-Based Variant Assignment<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A common pattern uses a cookie (for example, <code>abt<\/code>) to persist variant assignments across page loads. The cookie stores a compact format: <code>testId:variant,testId:variant,...<\/code><\/p>\n\n\n\n<pre class=\"mermaid\">flowchart TD\n  subgraph Step1[\"Step 1: First Visit (no cookie)\"]\n    A1[\"Browser&lt;br\/&gt;Request: \/homepage\"] --&gt; B1[\"Server&lt;br\/&gt;(SSR entry)\"]\n    B1 --&gt; C1[\"Read cookie #quot;abt#quot;&lt;br\/&gt;Result: empty\"]\n    C1 --&gt; D1[\"Fetch active A\/B tests&lt;br\/&gt;from headless CMS\"]\n    D1 --&gt; E1[\"For each test:&lt;br\/&gt;hash(userId + testId) \u2192 variant\"]\n    E1 --&gt; F1[\"Set cookie:&lt;br\/&gt;abt=pricing:A,hero:B\"]\n    F1 --&gt; G1[\"Render HTML using&lt;br\/&gt;pricing=A, hero=B\"]\n    G1 --&gt; H1[\"Return HTML&lt;br\/&gt;+ Set-Cookie header\"]\n    H1 --&gt; I1[\"Browser receives HTML&lt;br\/&gt;+ cookie persisting assignment\"]\n  end\n\n  subgraph Step2[\"Step 2: Subsequent Visit (cookie exists)\"]\n    A2[\"Browser&lt;br\/&gt;Request: \/products&lt;br\/&gt;Cookie: abt=pricing:A,hero:B\"] --&gt; B2[\"Server&lt;br\/&gt;(SSR entry)\"]\n    B2 --&gt; C2[\"Read cookie&lt;br\/&gt;{pricing: A, hero: B}\"]\n    C2 --&gt; D2[\"Render HTML using&lt;br\/&gt;persisted variants\"]\n    D2 --&gt; E2[\"No re-assignment \u2014&lt;br\/&gt;user sees consistent UX\"]\n  end<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Why Cookies and Not Server-Side State<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Cookies are the only mechanism that satisfies all constraints:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Available during SSR<\/strong> \u2014 the server reads the cookie before rendering<\/li>\n<li><strong>Persistent across page loads<\/strong> \u2014 the user sees the same variant consistently<\/li>\n<li><strong>No server-side state<\/strong> \u2014 no session store, no database lookup, scales to any number of replicas<\/li>\n<li><strong>Works with CDN caching<\/strong> \u2014 the cookie varies the cache key per variant when needed<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">CMS-Driven Test Configuration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In large enterprise applications, A\/B tests are typically defined as entries in a headless CMS, not in application code. Each test entry might specify:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Field<\/th><th>Example<\/th><th>Purpose<\/th><\/tr><\/thead><tbody><tr><td><code>testId<\/code><\/td><td><code>pricing-display<\/code><\/td><td>Unique identifier<\/td><\/tr><tr><td><code>variants<\/code><\/td><td><code>[\"A\", \"B\"]<\/code><\/td><td>Available variants<\/td><\/tr><tr><td><code>trafficSplit<\/code><\/td><td><code>[50, 50]<\/code><\/td><td>Percentage per variant<\/td><\/tr><tr><td><code>startDate<\/code><\/td><td><code>2025-06-01<\/code><\/td><td>Test activation date<\/td><\/tr><tr><td><code>endDate<\/code><\/td><td><code>2025-07-01<\/code><\/td><td>Test deactivation date<\/td><\/tr><tr><td><code>targetPages<\/code><\/td><td><code>[\"\/products\/*\"]<\/code><\/td><td>URL patterns where the test applies<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Content editors create, update, and schedule tests without involving the development team. The application fetches active test configuration from the CMS at startup and caches it.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  CMS[\"(Headless CMS)\"] --&gt;|\"Define tests&lt;br\/&gt;(testId, variants,&lt;br\/&gt;trafficSplit, dates, pages)\"| Conf[AB Test Config]\n  Conf --&gt; App[\"Nuxt App&lt;br\/&gt;(on startup)\"]\n  App --&gt; Cache[In-memory config cache]\n  Cache --&gt; SSR[\"SSR requests&lt;br\/&gt;(variant assignment)\"]<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Composable: <code>useAbTest<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Components consume A\/B test variants through a composable:<\/p>\n\n\n\n<pre class=\"mermaid\">sequenceDiagram\n  participant C as Component\n  participant U as useAbTest('pricing')\n  participant S as SSR\/Client Runtime\n  participant ST as useState store\n\n  C-&gt;&gt;U: const { variant } = useAbTest('pricing')\n  U-&gt;&gt;S: Read current request's&lt;br\/&gt;variant map\n  S-&gt;&gt;ST: Get variant map&lt;br\/&gt;(from cookie \/ payload)\n  ST--&gt;&gt;S: { pricing: 'A', ... }\n  S--&gt;&gt;U: 'A'\n  U--&gt;&gt;C: variant = 'A'\n\n  Note over S,ST: On SSR: variant map is created&lt;br\/&gt;before first component render\n\n  Note over S,ST: On client: variant map is hydrated&lt;br\/&gt;from Nuxt payload (no re-assignment)<\/pre>\n\n\n\n<pre><code class=\"language-text\">Usage in a Vue component:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 const { variant } = useAbTest('pricing')    \u2502\n\u2502                                             \u2502\n\u2502 &lt;template&gt;                                  \u2502\n\u2502   &lt;PricingDisplayA v-if=\"variant === 'A'\" \/&gt;\u2502\n\u2502   &lt;PricingDisplayB v-else \/&gt;                \u2502\n\u2502 &lt;\/template&gt;                                 \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\nWhat happens under the hood:\n  1. useAbTest reads the current request's\n     variant map (from cookie or server state)\n  2. Returns the assigned variant for this test\n  3. On SSR: variant is determined before render\n  4. On client: variant is hydrated from SSR state\n     (no re-assignment, no flicker)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>useAbTest<\/code> is SSR-safe: it reads from <code>useState<\/code> (transferred from server to client via the Nuxt payload), so the client always sees what the server rendered.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">URL-Based Override<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">During development and QA, testers need to force specific variants without waiting for random assignment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A URL parameter overrides the cookie:<\/p>\n\n\n\n<pre><code class=\"language-text\">Normal visit:\n  \/products \u2192 variant from cookie (e.g., A)\n\nOverride:\n  \/products?abt=pricing:B \u2192 forces variant B for pricing test\n  Cookie IS updated \u2014 the override persists across subsequent visits\n\nQA Workflow:\n  1. Test variant A: \/products?abt=pricing:A \u2192 verify\n  2. Test variant B: \/products?abt=pricing:B \u2192 verify\n  3. Compare \u2192 approve test<\/code><\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart TD\n  A[\"Browser&lt;br\/&gt;\/products?abt=pricing:B\"] --&gt; B[Server middleware]\n  B --&gt; C[\"Parse query param&lt;br\/&gt;abt=pricing:B\"]\n  C --&gt; D[\"Read existing cookie&lt;br\/&gt;abt=pricing:A,hero:B\"]\n  D --&gt; E[\"Merge override&lt;br\/&gt;pricing \u2192 B\"]\n  E --&gt; F[\"Set updated cookie&lt;br\/&gt;abt=pricing:B,hero:B\"]\n  F --&gt; G[\"SSR render using&lt;br\/&gt;pricing=B, hero=B\"]\n  G --&gt; H[\"Subsequent visits&lt;br\/&gt;\/products \u2192 use cookie&lt;br\/&gt;(no query needed)\"]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The override takes precedence and is merged into the cookie permanently. The server middleware merges the query parameter into existing assignments, and subsequent visits without the parameter continue using the overridden variant.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Debug Tooling<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A non-production debug panel can show active tests and allow switching variants in real time:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  Dev[QA \/ Developer] --&gt; UI[\"Debug Panel&lt;br\/&gt;A\/B Tests Tab\"]\n  UI --&gt;|Switch to A\/B| Tool[set-ab-test tool]\n  Tool --&gt; Cookie[Update 'abt' cookie]\n  Cookie --&gt; Reload[Reload page]\n  Reload --&gt; SSR[\"SSR render&lt;br\/&gt;with new variants\"]\n\n  SSR --&gt; UI2[\"Debug Panel shows&lt;br\/&gt;updated current variant\"]<\/pre>\n\n\n\n<pre><code class=\"language-text\">Debug Panel \u2014 A\/B Tests Tab:\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Active A\/B Tests                               \u2502\n\u2502                                                \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502\n\u2502 \u2502 pricing-display                          \u2502   \u2502\n\u2502 \u2502 Current: A                               \u2502   \u2502\n\u2502 \u2502 [Switch to B] [Switch to A]              \u2502   \u2502\n\u2502 \u2502 Traffic: 50\/50                           \u2502   \u2502\n\u2502 \u2502 Active: Jun 1 - Jul 1                    \u2502   \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518   \u2502\n\u2502                                                \u2502\n\u2502 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510   \u2502\n\u2502 \u2502 hero-layout                              \u2502   \u2502\n\u2502 \u2502 Current: B                               \u2502   \u2502\n\u2502 \u2502 [Switch to A] [Switch to B]              \u2502   \u2502\n\u2502 \u2502 Traffic: 70\/30                           \u2502   \u2502\n\u2502 \u2502 Active: May 15 - Jun 30                  \u2502   \u2502\n\u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518   \u2502\n\u2502                                                \u2502\n\u2502 Cookie: abt=pricing-display:A,hero-layout:B    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Clicking &#8220;Switch to B&#8221; updates the cookie and reloads the page with the new variant. An internal debug chatbot can also switch variants programmatically via a <code>set-ab-test<\/code> tool.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Measuring Results<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Variant assignments are tracked in analytics. Each page view includes the active variant assignments as custom properties:<\/p>\n\n\n\n<pre><code class=\"language-text\">Analytics Event:\n  event: \"page_view\"\n  properties:\n    page: \"\/products\"\n    ab_pricing: \"A\"\n    ab_hero: \"B\"<\/code><\/pre>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  User[User page view] --&gt; Page[Nuxt page]\n  Page --&gt; Analytics[Analytics SDK]\n  Analytics --&gt; Event[\"page_view event&lt;br\/&gt;+ ab_* properties\"]\n  Event --&gt; Store[Analytics backend]\n  Store --&gt; Seg[\"Analytics dashboard&lt;br\/&gt;segment by variant\"]\n  Seg --&gt; Decision[\"Decide winning variant&lt;br\/&gt;(A or B)\"]\n  Decision --&gt; CMS[\"(CMS config&lt;br\/&gt;update default or&lt;br\/&gt;deactivate test)\"]<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The analytics team segments conversion rates by variant. A typical workflow:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Run the test for 2\u20134 weeks (enough data for statistical significance)<\/li>\n<li>Compare conversion rates per variant in the analytics dashboard<\/li>\n<li>If variant B wins: update the CMS to make B the default (100% traffic)<\/li>\n<li>If variant A wins: deactivate the test in the CMS (A was already the default)<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">No code changes are required either way. The lifecycle is entirely CMS-driven.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Cache Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A\/B testing interacts with page caching (Article 6). The cache key (for example, <code>page-data:\/products<\/code>) does <strong>not<\/strong> include variant assignments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This works because variants control <strong>which Vue component renders<\/strong>, not which CMS data is fetched. CMS data is the same regardless of variant \u2014 the variant only determines the component tree during SSR. Since the cache stores query results, not rendered HTML, the same data serves all variants.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  A[\"Incoming request&lt;br\/&gt;(with abt cookie)\"] --&gt; B[Cache layer]\n  B --&gt;|Key: page-data:\/products| C[Cached CMS data]\n  C --&gt; D[SSR renderer]\n\n  subgraph VariantA[User with variant A]\n    D --&gt; AComp[\"Render &amp;lt;PricingDisplayA \/&amp;gt;&lt;br\/&gt;using shared CMS data\"]\n  end\n\n  subgraph VariantB[User with variant B]\n    D --&gt; BComp[\"Render &amp;lt;PricingDisplayB \/&amp;gt;&lt;br\/&gt;using shared CMS data\"]\n  end<\/pre>\n\n\n\n<pre><code class=\"language-text\">Cache layer:\n  Cache key: page-data:\/products\n  Cached: CMS data (shared across all variants)\n\nA\/B layer (runs after cache):\n  Variant A user \u2192 CMS data \u2192 renders &lt;PricingDisplayA \/&gt;\n  Variant B user \u2192 CMS data \u2192 renders &lt;PricingDisplayB \/&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Variants must not trigger different CMS queries. If a future test requires variant-specific CMS content, the cache key would need to include variant assignments \u2014 but in many systems this is not necessary.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The variant decision must happen before SSR begins<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the fundamental constraint. Any approach that determines the variant after the server starts rendering will cause hydration mismatches. The cookie must be read and the variant map populated before the first component\u2019s <code>setup()<\/code> runs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Cookies are the right persistence mechanism for SSR A\/B tests<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Server-side sessions add state. Local storage is unavailable during SSR. URL parameters are not persistent. Cookies are the only mechanism that is available on the server, persistent across requests, and stateless.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">CMS-driven test configuration removes developer bottlenecks<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When tests are defined in the CMS, marketing or product teams can create and schedule them independently. Development only needs to build the variant components.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Debug tooling for A\/B tests is essential, not optional<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Without a way to force a variant, QA has to clear cookies and hope for the right assignment. URL override and the debug panel make variant testing deterministic.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 20<\/strong>: <em>CMS Live Preview \u2014 Real-Time WYSIWYG Editing in SSR<\/em> \u2014 Making the CMS preview iframe work with server-side rendering.<\/li>\n<li><strong>Article 21<\/strong>: <em>Structured Logging in Nuxt \u2014 From <code>console.log<\/code> to Production Observability<\/em> \u2014 Multi-sink logging with runtime control.<\/li>\n<li><strong>Article 22<\/strong>: <em>Building Production-Ready Nuxt Modules \u2014 Lifecycle, Hooks, and Conventions<\/em> \u2014 The patterns behind reliable module development.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Eleventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Why A\/B Testing in SSR Is Different In a client-side SPA, A\/B testing is simple: check a flag, choose a variant, render. In SSR, the decision has to happen before the HTML is generated. The server renders the full page [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":230,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-18","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=18\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production\" \/>\n<meta property=\"og:description\" content=\"Eleventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Why A\/B Testing in SSR Is Different In a client-side SPA, A\/B testing is simple: check a flag, choose a variant, render. In SSR, the decision has to happen before the HTML is generated. The server renders the full page [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=18\" \/>\n<meta property=\"og:site_name\" content=\"Software Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:48:38+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-07T01:21:57+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1880\" \/>\n\t<meta property=\"og:image:height\" content=\"1249\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"9 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"A\\\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection\",\"datePublished\":\"2026-06-06T21:48:38+00:00\",\"dateModified\":\"2026-06-07T01:21:57+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18\"},\"wordCount\":825,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/11-ab-testing-at-ssr-level.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18\",\"name\":\"A\\\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/11-ab-testing-at-ssr-level.jpg\",\"datePublished\":\"2026-06-06T21:48:38+00:00\",\"dateModified\":\"2026-06-07T01:21:57+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=18\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/11-ab-testing-at-ssr-level.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/11-ab-testing-at-ssr-level.jpg\",\"width\":1880,\"height\":1249},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=18#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"A\\\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=18","og_locale":"en_US","og_type":"article","og_title":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production","og_description":"Eleventh in a series about migrating from legacy architectures to a modern Nuxt 4 stack. Why A\/B Testing in SSR Is Different In a client-side SPA, A\/B testing is simple: check a flag, choose a variant, render. In SSR, the decision has to happen before the HTML is generated. The server renders the full page [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=18","og_site_name":"Software Production","article_published_time":"2026-06-06T21:48:38+00:00","article_modified_time":"2026-06-07T01:21:57+00:00","og_image":[{"width":1880,"height":1249,"url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","type":"image\/jpeg"}],"author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"9 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=18#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=18"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection","datePublished":"2026-06-06T21:48:38+00:00","dateModified":"2026-06-07T01:21:57+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=18"},"wordCount":825,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=18#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=18#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=18","url":"https:\/\/softwareproduction.eu\/?p=18","name":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection - Software Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=18#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=18#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","datePublished":"2026-06-06T21:48:38+00:00","dateModified":"2026-06-07T01:21:57+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=18#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=18"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=18#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","width":1880,"height":1249},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=18#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/11-ab-testing-at-ssr-level.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/18","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=18"}],"version-history":[{"count":9,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/18\/revisions"}],"predecessor-version":[{"id":231,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/18\/revisions\/231"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/230"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=18"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=18"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=18"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}