Skip to content
Dashboard

How to run A/B tests with Next.js and Vercel

Senior Developer Advocate

Link to headingRedesigning the Template Marketplace

Old version: limited selection of templates that focused mainly on framework starters.Old version: limited selection of templates that focused mainly on framework starters.
Old version: limited selection of templates that focused mainly on framework starters.
New version: wider selection of templates with fuzzy search & filters.New version: wider selection of templates with fuzzy search & filters.
New version: wider selection of templates with fuzzy search & filters.
We used Vercel's Edge Middleware to stagger our launch into 3 phases.We used Vercel's Edge Middleware to stagger our launch into 3 phases.
We used Vercel's Edge Middleware to stagger our launch into 3 phases.

Link to headingCatch-all Routes & On-demand Revalidation

Link to headingExperimenting at the Edge

middleware.ts
import { type NextRequest, NextResponse } from 'next/server';
// make sure the middleware only runs when
// the requested url starts with `/templates`
export const config = {
matcher: ['/templates(.*)'],
};
const THRESHOLD = 0.2; // initial threshold for the new variant (20%)
const COOKIE_NAME = 'tm_var'; // name of the cookie to store the variant
export function middleware(req: NextRequest) {
// get the variant from the cookie
// if not found, randomly set a variant based on threshold
const variant =
req.cookies.get(COOKIE_NAME) || (Math.random() < THRESHOLD ? 'new' : 'old');
const url = req.nextUrl.clone();
// if it's the old variant, rewrite to the old templates marketplace
if (variant === 'old') {
url.pathname = '/templates-old';
}
const res = NextResponse.rewrite(url);
// set the variant in the cookie if not already set
if (!req.cookies.get(COOKIE_NAME)) {
res.cookies.set(COOKIE_NAME, variant);
}
return res;
}

Middleware code to perform the A/B test at the edge.

Link to headingAvoiding layout shift with Edge Middleware

middleware.ts
import { type NextRequest, NextResponse } from 'next/server';
// make sure the middleware only runs when
// the requested url starts with `/templates`
export const config = {
matcher: ['/templates(.*)'],
};
const THRESHOLD = 0.2; // initial threshold for the new variant (20%)
const COOKIE_NAME = 'tm_var'; // name of the cookie to store the variant
export function middleware(req: NextRequest) {
// get the variant from the cookie
// if not found, randomly set a variant based on threshold
const variant =
req.cookies.get(COOKIE_NAME) || (Math.random() < THRESHOLD ? 'new' : 'old');
const url = req.nextUrl.clone();
// if it's the old variant, rewrite to the old templates marketplace
if (variant === 'old') {
url.pathname = '/templates-old';
} else {
// for the new variant, we need to perform a rewrite to /template/skeleton
// if there are query paramters in the request URL
const hasQueryParams = req.nextUrl.search.length > 0;
if (hasQueryParams) {
url.pathname = `/templates/skeleton`;
}
}
const res = NextResponse.rewrite(url);
// set the variant in the cookie if not already set
if (!req.cookies.get(COOKIE_NAME)) {
res.cookies.set(COOKIE_NAME, variant);
}
return res;
}

Middleware code to perform an edge rewrite to avoid layout shift.

templates/[[...slug]].tsx
import useSWR from "swr"
import { useRouter } from "next/router"
import { Card, PlaceholderCard, CardProps } from '@/components/cards'
export default function Templates({ skeleton } : { skeleton: boolean }) {
const router = useRouter()
const { data } = useSWR<CardProps[]>("/api/templates")
// if skeleton is true and the router is still loading, show a skeleton
if (!data || (skeleton && !router.isReady)) {
return (
<div>
{[...Array(6).keys()].map((i: number) => (
<PlaceholderCard key={i}/>
))}
</div>
)
}
return (
<div>
{data.map((d: CardProps, i) => (
<Card key={i} data={d} />
))}
</div>
)
}
export function getStaticProps(context) {
const skeleton = context.params?.slug?.includes('skeleton')
return {
props: {
skeleton,
},
}
}

Simplified version of the code that shows how we use SWR and middleware to conditionally render loader skeletons on the client side if there are query parameters present in the request URL.

Link to headingCollecting data & iterating

Link to headingTracking conversion rates with Heap

Link to headingTracking search & filter usage Stats with Algolia

Link to headingEdge Middleware: Run A/B tests without the tradeoffs