Forcing an A/B test variant (Next.js Pages Router)
Last updated: March 23, 2026
Executive summary
What this does: You can share a link that shows your site as if a specific Uniform A/B test variation won—useful for reviews, QA, or demos—by adding one query parameter to the URL.
How marketers / PMs use it: Append uniform_force_ab_test_variant_id=<variant-id> to any page URL (use the variation’s Context tag id from Uniform, not only the human-readable label). Example:
https://yoursite.com/some-page?uniform_force_ab_test_variant_id=your-variant-idWhat happens under the hood (two complementary mechanisms):
Visitor cookie (
ufvd) — The server adjusts Uniform’s visitor cookie so stored A/B assignments line up with the forced variant where applicable. This supports Uniform Context behavior during SSR (personalization, tests resolved via context).Canvas composition — For Canvas
$testcomponents, the server temporarily sets that variation’s audience share to 100% and the others in the same test to 0% in the payload sent to the page. Only tests that actually contain a variation with that id are changed; other tests behave normally.
Important limitations:
⚠ If a visitor already has an assignment for a test in their cookie, Context may keep that assignment; the composition skew mainly affects the “random pick” path for new assignments. To test it properly, run the variant forcing in incognito as a fresh request:
http://localhost:3003/?uniform_force_ab_test_variant_id=<your=variant-id>This starter skips both behaviors in Next.js preview mode (
context.preview), so draft preview in the CMS is not distorted.Treat forced URLs as a power user / internal feature unless you add auth or strip the parameter from analytics.
Deep dive: architecture and integration
Reference starter
The code with this change applied to Uniform's vanilla Next.js page router starter is here for your reference, you can deploy it to test and see how it works before integrating it with your own Next.js codebase:
https://github.com/uniformdev/examples/tree/example/force-abtest-variation
Problem shape
Uniform picks A/B variations using:
Context — Visitor state in the
ufvdcookie, read during SSR when you callcreateUniformContext(this starter does that in_document.tsxviaNextCookieTransitionDataStore).Canvas
$testtrees — Variations under a$testcomponent carry$tstVrntparameters (id,testDistribution, etc.). The Context runtime picks a variation using stored ids when present, otherwise weighted random using normalized distributions.
To “force” a link, you must influence one or both of those paths before the React tree and Context consume them.
Why getServerSideProps alone is not enough for Context
In the Pages Router, Document.getInitialProps runs createUniformContext(ctx) before it calls Document.getInitialProps → renderPage() → your page’s getServerSideProps. The cookie store reads req.headers.cookie when the Context is constructed.
Therefore:
Patching cookies only at the start of
getServerSidePropsis too late for<UniformContext />/enableNextSsrin_document.You must patch the incoming Node request in
_document.getInitialProps, beforecreateUniformContext, or use Edge middleware that rewrites theCookieheader before the request hits Node (this starter does not ship root middleware;_documentis enough here).
Layer 1 — Cookie / ufvd (forceAbTestVariantCookie.ts)
File: lib/uniform/forceAbTestVariantCookie.ts
Entry point for SSR: applyForcedAbTestVariantToIncomingMessage(req, query)
Reads
uniform_force_ab_test_variant_idfrom the Next query object (same param name as Uniform’s docs).If present and an
ufvdcookie exists, re-encodes the A/B portion ofufvdso every test entry is rewritten to use the forced variant id (same string manipulation as the App Router middleware recipe).Mutates
req.headers.cookiein place so all later server code sees the updated header.
Exports useful for integration:
Export Purpose | |
| Constant |
| Safe read from |
| Low-level: new |
| Call from |
Dependency: Uses the [cookie](https://www.npmjs.com/package/cookie) package to parse the Cookie header (already pulled in via @uniformdev/context-next in typical installs).
Wire-up in this starter: pages/_document.tsx calls applyForcedAbTestVariantToIncomingMessage(ctx.req, ctx.query) immediately before createUniformContext(ctx).
Layer 2 — Canvas composition distributions (forceCanvasTestVariantInComposition.ts)
File: lib/uniform/forceCanvasTestVariantInComposition.ts
Entry point: applyForcedCanvasTestVariantDistributions(composition, forcedVariantId)
Walks the composition tree (
RootComponentInstance).For each
$testnode (CANVAS_TEST_TYPE), inspects thetestslot (CANVAS_TEST_SLOT).If any child has
$tstVrnt(CANVAS_TEST_VARIANT_PARAM) withvalue.id === forcedVariantId, then for all variants in that slot setstestDistributionto 100 (match) or 0 (others).If no variant in that test uses that id, the test is unchanged (per the Notion Pages Router behavior).
Wire-up in this starter: pages/[[...slug]].tsx (and the .ssr duplicate) inside withUniformGetServerSideProps → handleComposition:
const forcedVariantId = getForcedAbTestVariantFromQuery(context.query);
if (!context.preview && composition && forcedVariantId) {
applyForcedCanvasTestVariantDistributions(composition, forcedVariantId);
}How the two layers differ
Aspect Cookie ( Composition skew | ||
Scope | Rewrites all A/B slots in | Only |
Best when | You need Context-aligned behavior similar to App Router forcing | You want Canvas-native behavior: leave unrelated tests alone |
Existing cookie assignment | Can override encoded assignments for tests in | If Context already chose a variant for a test, that id may still win over distributions |
Runs |
|
|
Using both covers typical Canvas + Context SSR setups; understand the existing assignment caveat so QA knows when to clear cookies or use incognito.
Integrating into an existing Next.js (Pages Router) + Uniform app
Copy the two library modules into your repo (adjust import paths):
lib/uniform/forceAbTestVariantCookie.tslib/uniform/forceCanvasTestVariantInComposition.ts
_document.tsx— If you use Uniform Context SSR (createUniformContext+enableNextSsringetInitialProps), add beforecreateUniformContext:if (ctx.req) { applyForcedAbTestVariantToIncomingMessage(ctx.req, ctx.query); }getServerSidePropswith Canvas — In every route that useswithUniformGetServerSideProps(or your own fetch + composition pipeline), after you have acompositionobject and outside preview:import { getForcedAbTestVariantFromQuery } from "@/lib/uniform/forceAbTestVariantCookie"; import { applyForcedCanvasTestVariantDistributions } from "@/lib/uniform/forceCanvasTestVariantInComposition"; // inside handleComposition (or equivalent): const forcedVariantId = getForcedAbTestVariantFromQuery(context.query); if (!context.preview && composition && forcedVariantId) { applyForcedCanvasTestVariantDistributions(composition, forcedVariantId); }SSG (
getStaticProps) — This pattern runs per request only where you havegetServerSideProps(or middleware + client fetch). Static pages built at build time will not re-runhandleCompositionper visitor; use SSR for routes that must honor the query param, or adopt a different strategy (e.g. client-side composition fetch with the param).Optional: Edge middleware — If you cannot use
_document(unusual for Pages Router) or want the cookie rewritten before Node, you can port the cookie rewrite tomiddleware.tsusingNextResponse.next({ request: { headers } })as in Uniform’s App Router article. This starter removed duplicate root middleware because_documentalready applies the cookie patch on Node.Production hygiene (recommended by Uniform):
Restrict who can use forced URLs (secret token, IP allowlist, or environment check).
Exclude requests carrying
uniform_force_ab_test_variant_idfrom conversion/analytics or treat them as internal traffic.
Files in this starter
File Role | |
| Query parsing + |
|
|
| Invokes cookie layer before |
| Invokes composition layer in |
Finding the variant id
Use the variation’s Context tag id in Uniform (the value stored on $tstVrnt / what appears in APIs as the variant’s id in test placement), not necessarily the display “Variation name” string. If the wrong id is used, the composition layer no-ops for that test; the cookie layer may still rewrite ufvd globally, which can be undesirable—so validating ids matters.