Marketing Pages

This example shows how to use feature flags for marketing pages. Marketing pages are typically static, and served from a CDN at the edge.

When A/B testing on marketing pages it's important to avoid layout shift and jank, and to keep the pages static. At first glance this seems add odds with the dynamic nature of feature flags. This example shows how to keep a page static and serveable from the CDN even when running multiple A/B tests on the page.

Precomputing

The approach used to keep pages static even when using feature flags on them is described in more detail on the Precompute section. At a high level we use Edge Middleware to rewrite the incoming request between static versions of the page. These static versions represent the different feature flag states and can be computed at build time or at request time. If they are computed at request time we can use Incremental Static Regeneration (ISR). Relying on ISR avoids the combinatory explosion which would otherwise increase the build time, while simulatenously allowing pages to stay cached after the first time they were requested.

Learn more about precompute.

Identifying

This example uses a random id stored in a cookie to target users. Next.js can't set cookies while rendering pages, so you must use Edge Middleware to generate the random id and store it in a cookie.

This also allows the page to stay static as the page will never access cookies. Instead, the flags using cookies will be evaluated in Edge Middleware and only the resulting flags will be available when rendering the page.

middleware.ts
import { precompute } from '@vercel/flags/next';
import { type NextRequest, NextResponse } from 'next/server';
import { marketingFlags } from './flags';
import { getOrGenerateVisitorId } from './get-or-generate-visitor-id';
export async function marketingMiddleware(request: NextRequest) {
// assign a cookie to the visitor
const visitorId = await getOrGenerateVisitorId(
request.cookies,
request.headers,
);
// precompute the flags
const code = await precompute(marketingFlags);
// rewrite the page with the code and set the cookie
return NextResponse.rewrite(
new URL(`/examples/marketing-pages/${code}`, request.url),
{
headers: {
// Set the cookie on the response
'Set-Cookie': `marketing-visitor-id=${visitorId}; Path=/`,
// Add a request header, so the page knows the generated id even
// on the first-ever request which has no request cookie yet.
//
// This is later used by the getOrGenerateVisitorId function.
'x-marketing-visitor-id': visitorId,
},
},
);
}

The getOrGenerateVisitorId function generates a random id, or returns the one stored in a cookie if one already exists. The function is further deduplicated to ensure it generates the same id for the same request, even when called multiple times.

get-or-generate-visitor-id.ts
import { nanoid } from 'nanoid';
import { dedupe } from '@vercel/flags/next';
import type { ReadonlyHeaders, ReadonlyRequestCookies } from '@vercel/flags';
import type { NextRequest } from 'next/server';
const generateId = dedupe(async () => nanoid());
// This function is not deduplicated, as it is called with
// two different cookies objects, so it can not be deduplicated.
//
// However, the generateId function will always generate the same id for the
// same request, so it is safe to call it multiple times within the same runtime.
export const getOrGenerateVisitorId = async (
cookies: ReadonlyRequestCookies | NextRequest['cookies'],
headers: ReadonlyHeaders | NextRequest['headers'],
) => {
// check cookies first
const cookieVisitorId = cookies.get('marketing-visitor-id')?.value;
if (cookieVisitorId) return cookieVisitorId;
// check headers in case middleware set a cookie on the response, as it will
// not be present on the initial request
const headerVisitorId = headers.get('x-marketing-visitor-id');
if (headerVisitorId) return headerVisitorId;
// if no visitor id is found, generate a new one
return generateId();
};

Having a reliable getOrGenerateVisitorId function means we can call it in Edge Runtime and in our flag's identify function and both will see the exact same id, even when no cookie was present initially.

flags.ts
// identify who is requesting the page
const identify = dedupe(
async ({
cookies,
}: {
cookies: ReadonlyRequestCookies;
}): Promise<Entities> => {
const visitorId = await getOrGenerateVisitorId(cookies);
return { visitor: visitorId ? { id: visitorId } : undefined };
},
);
export const marketingAbTest = flag<boolean, Entities>({
key: 'marketing-ab-test-flag',
// use identify to establish the evaluation context,
// which will be passed as "entities" to the decide function
identify,
decide({ entities }) {
if (!entities?.visitor) return false;
return /^[a-n0-5]/i.test(entities.visitor.id);
},
});

Learn more about identify.

Ensuring the generated id is always available

When a user visits the page for the first time they will not have the marketing-visitor-id cookie. Edge Middleware will generate an id and store that in a cookie. However, this means the page will not see the generated cookie as it will only be supplied with the next request. This means a dynamic page would have no knowledge of the generated id.

To solve this issue the approach above also sets a x-marketing-visitor-id request header from Edge Middleware, which holds the parsed or generated id. This allows the getOrGenerateVisitorId function to always see the id by either reading it from the cookie or the header. This works even when the id was freshly generated by Edge Middleware and no cookie is present on the request.

This is only relevant when the underlying page is dynamic, as a static page can by definition not read any cookies or headers.

Example

The example below shows the usage of two feature flags on a static page. These flags represent two A/B tests which you could be running simulatenously.