DEV Community

RAXXO Studios
RAXXO Studios

Posted on • Originally published at raxxo.shop

Wiring Magnific Images Into a Vercel Edge Config A/B Test

  • Generated 3 hero variants in Magnific in under 4 minutes for the same prompt seed

  • Stored variant metadata in Vercel Edge Config, sub-10ms reads from middleware

  • Routed traffic 33/33/34 via cookie hash + geo, no client flash, no extra requests

  • Variant B beat the control by 18.4% on hero CTR after 11 days, 4,212 sessions

I ran a hero A/B test on raxxo.shop last month with three Magnific images and Vercel Edge Config. Setup took an hour. Decision came back in 11 days. Here is the exact wiring, the schema, and the middleware code, copy-paste ready.

Generate the variant set in Magnific

The trick with A/B test imagery is keeping the variants close enough that you are testing one thing, not three. Same subject, same lighting, one variable. I usually pick the variable first (composition tightness, color temperature, or product placement), then write the prompt accordingly.

For this test I wanted to see if a tighter crop on the product hero would lift CTR. So three variants, same seed, one parameter change.

Prompt I used in Magnific Mystic for the variant set:


hero product shot, dark studio, single subject centered,
soft rim light from upper right, 16:9, photoreal,
[VARIANT_A: wide composition, product fills 30% of frame]
[VARIANT_B: medium composition, product fills 55% of frame]
[VARIANT_C: tight composition, product fills 75% of frame]
seed: 88142, steps: 50, creativity: 4, hdr: 6

Enter fullscreen mode Exit fullscreen mode

I render all three back to back, same seed, only the composition line changes. Magnific keeps the lighting and subject identical, which is exactly what you want when one variable is supposed to drive the whole effect.

Output goes to PNG at 2400x1350. I run them through the same studio look in Lightroom (a 4-second batch action), then drop into Shopify Files via the API. If you want the upload-to-Shopify side automated, I wrote that one up at Magnific to Shopify image pipeline. For Vercel Blob the pattern is the same, just swap the upload helper.

End state: three CDN URLs, three alt strings, three headlines I want to test alongside the images.

Edge Config schema (variant -> image, alt, headline)

Edge Config is a key-value store that reads in single-digit milliseconds from the edge. It is built for exactly this: small config you read on every request and update without redeploying. There is a 512KB total size cap, which is fine for hundreds of variant rows.

I keep one key per experiment. The value is an object with the variant slug as the inner key:


{
  "exp_hero_2026_05": {
    "active": true,
    "weights": { "a": 33, "b": 33, "c": 34 },
    "geo_lock": null,
    "variants": {
      "a": {
        "image": "https://cdn.shopify.com/s/files/1/.../hero-a.webp",
        "alt": "Wide hero, product center frame",
        "headline": "Built in Berlin, shipped worldwide.",
        "label": "wide"
      },
      "b": {
        "image": "https://cdn.shopify.com/s/files/1/.../hero-b.webp",
        "alt": "Medium hero, product fills frame",
        "headline": "Built in Berlin, shipped worldwide.",
        "label": "medium"
      },
      "c": {
        "image": "https://cdn.shopify.com/s/files/1/.../hero-c.webp",
        "alt": "Tight hero, product close-up",
        "headline": "Built in Berlin, shipped worldwide.",
        "label": "tight"
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Two things worth flagging. First, weights lives next to the variants so I can shift traffic without touching middleware code. Second, geo_lock lets me restrict an experiment to one country if I want to test German visitors only. Default is null (open to everyone). I have used the same shape across five experiments now and it scales fine.

You can update Edge Config three ways: Vercel dashboard, REST API, or the @vercel/edge-config CLI helper. For one-off tweaks I just use the dashboard. For programmatic updates (like rotating featured products on a schedule), there is a separate write endpoint.

Middleware split (~30 lines)

Middleware is where the routing happens. It runs at the edge before the response is built, picks a variant, sticks it in a cookie so the visitor sees the same image on every page, and rewrites a search param the page can read.


// middleware.ts
import { NextResponse, type NextRequest } from 'next/server'
import { get } from '@vercel/edge-config'

const COOKIE = 'rx_exp_hero'
const EXP_KEY = 'exp_hero_2026_05'

export async function middleware(req: NextRequest) {
  const url = req.nextUrl.clone()
  if (!url.pathname.startsWith('/') || url.pathname.startsWith('/api')) {
    return NextResponse.next()
  }

  const exp = await get(EXP_KEY)
  if (!exp?.active) return NextResponse.next()

  const country = req.geo?.country ?? 'XX'
  if (exp.geo_lock && country !== exp.geo_lock) {
    return NextResponse.next()
  }

  let variant = req.cookies.get(COOKIE)?.value
  if (!variant || !exp.variants[variant]) {
    variant = pickWeighted(exp.weights)
  }

  url.searchParams.set('v', variant)
  const res = NextResponse.rewrite(url)
  res.cookies.set(COOKIE, variant, { maxAge: 60 * 60 * 24 * 30, path: '/' })
  res.headers.set('x-rx-variant', variant)
  return res
}

function pickWeighted(w: Record): string {
  const total = Object.values(w).reduce((a, b) => a + b, 0)
  let r = Math.random() * total
  for (const [k, v] of Object.entries(w)) {
    if ((r -= v) < 0) return k
  }
  return Object.keys(w)[0]
}

export const config = { matcher: ['/((?!_next|.*\\..*).*)'] }

Enter fullscreen mode Exit fullscreen mode

That is the whole router. It reads Edge Config (cached, sub-10ms), checks geo, picks a variant if the visitor does not already have one, sets a 30-day cookie so they stay sticky, and adds an x-rx-variant response header for debugging.

The page component reads the variant from the search param (or directly from Edge Config server-side, your call) and renders the right image, alt, and headline. No client-side flash. No flicker. The visitor sees one image, always the same one, until the experiment ends or you wipe the cookie.

If you want the deeper context on the patterns this builds on, see 5 Vercel Edge Config patterns I use for Shopify A/B tests. It walks through the kill-switch, the geo flag, and the multi-arm bandit pattern that I did not use here.

Tracking the result without a vendor

I do not run PostHog on raxxo.shop. The experiment cookie plus a single beacon is enough for a hero test, and it keeps the dependency footprint tiny.

In the page, I fire one event when the hero enters the viewport:


// app/components/HeroBeacon.tsx
'use client'
import { useEffect } from 'react'

export function HeroBeacon({ variant }: { variant: string }) {
  useEffect(() => {
    const sent = sessionStorage.getItem('rx_hero_seen')
    if (sent) return
    sessionStorage.setItem('rx_hero_seen', '1')
    navigator.sendBeacon(
      '/api/exp/hero',
      JSON.stringify({ variant, t: Date.now() })
    )
  }, [variant])
  return null
}

Enter fullscreen mode Exit fullscreen mode

The /api/exp/hero route writes one row to a Postgres table (exp_id, variant, event, ts, country). For CTR I add a second beacon on the hero CTA click. That is it. Two events, one variant column, raw counts.

If you already use PostHog or Plausible, swap the sendBeacon for their event API and let their UI do the math. PostHog has a built-in feature flag system that overlaps with this setup. I prefer the Edge Config + raw events split because it keeps the routing fast and the analytics flexible.

For the math I ran a quick chi-square in a notebook every other day. If you want a hosted option, Convert.com and ABLyft both do the stats for you and have free tiers.

What 11 days of real numbers looked like

The test ran from April 24 to May 5, 4,212 sessions, hero CTR as the primary metric.


Variant  Sessions  Hero CTR    Lift     p-value
A wide      1,387     8.2%     -        -
B medium    1,398     9.7%     +18.4%   0.034
C tight     1,427     8.6%      +4.7%    0.61

Enter fullscreen mode Exit fullscreen mode

Variant B (medium composition) won. Tight crop did not move the needle in either direction. I shipped B as the new default by flipping active: false in Edge Config and updating the static fallback in the page component. Total time to deploy the winner: about 90 seconds.

A few things I would do differently next time. The 33/33/34 split is fine for three variants but slow if one is clearly losing. A multi-arm bandit reweights toward the winner mid-test and ends faster. I will use that pattern for the next one. Also, 4,212 sessions is below what I would want for a 5% lift detection, so I got lucky that B's effect was big enough to clear significance. For smaller expected lifts, plan for at least 10,000 sessions per variant.

The other surprise: variant B's Magnific generation took 38 seconds for the final upscale pass. The other two were closer to 25. Mystic batches are not always uniform on time, which matters if you are auto-generating dozens of variants in a CI step.

Bottom line

Three Magnific images, one Edge Config key, 30 lines of middleware. Sub-10ms variant routing, no client flash, 18.4% lift on the winner. The whole stack costs Vercel Pro (which I already pay for) plus Magnific (32 EUR/month on the Premium tier). No LaunchDarkly, no Optimizely, no third dashboard to log into.

If you want the Magnific side of this dialed in (prompts that produce variant sets with one parameter changed), the affiliate link gets you a few extra credits on signup. For the upload pipeline that gets the images into Shopify or Vercel Blob, I have the full walkthrough at Magnific to Shopify image pipeline. And for the broader Edge Config patterns (kill-switches, geo flags, bandits), 5 Vercel Edge Config patterns I use for Shopify A/B tests is the parent article. The full lab is at /pages/lab-overview if you want to browse the rest of the tutorials.

Top comments (0)