How tested pages are cached
Next.js caches a page once per path, and a CDN in front of it does the same. A product page in a price test needs one copy per test group instead. The SDK’s middleware solves this with a page key: it works out which price tests change the page and which test group the visitor is in for each, and rewrites the request to a path that names them. Next.js then caches one copy per key, and your CDN never stores a tested page. A page key holds the price tests that change the page, the visitor’s test group in each, the country the page is priced for, and a version of your tests, so an edit or a new launch is a new key. A visitor in Variant A of test 49603 and Control of test 50211, priced for the US, getsv1k3z9q.cUS.49603-1.50211-0; a visitor the second test excluded gets a shorter key with one segment, and their own copy. It holds nothing about the visitor, so everyone in the same test groups shares one copy. Only a price test enters a key, including the price change of a combined test: shipping, offer, and checkout tests apply in checkout from the cart attributes. A price test changes the home page, collection and search pages, and product pages; the cart and account pages are never rewritten.
What the middleware does with each kind of request:
We trade CDN hits on tested pages for the right price on every page and a preview link that always shows the test group you asked for. If your CDN can add a cookie to its cache key, you can cache tested pages there too.
Supported versions
Options
abconvertMiddleware and planMiddleware take the createABConvert options except request and pageKey, plus these:
If you already have middleware
If your middleware already rewrites or redirects, for example to add a locale prefix, callplanMiddleware instead of abconvertMiddleware. It returns what abconvertMiddleware would do, as data, and you apply it to your own response:
Server (middleware)
planMiddleware returns null for a request that is not a page, such as an image. Apply every field it returns, as the code does; the reference lists them. With a locale prefix, the tested routes move under app/[locale]/.
Product pages that show only their own price
By default a product page’s key carries every price test, because product pages often show other products’ prices too, such as related products. If your product pages show no price but their own product’s, setonlyOwnProductPrices: true in abconvertOptions: a product page’s key then carries only the tests that include its product, and a product in no test stays cacheable at your CDN. If a related product’s price does appear, leave it off, or that product shows its original price while checkout charges the test price.
Shipping rates and offers on a cached page
A page rendered from a page key carries its price tests in full. Every other running test is on the page too, but only as a light entry, with its changes stripped, so the cached copy holds nothing one visitor’s shipping test group would show.getShippingRates, getFreeShippingThreshold, and getOffers therefore return nothing on such a page, on the server and in the browser.
For a free shipping bar or an offer banner on every page, read them in a route handler that creates the client from the request, and render the bar in a client component that fetches the visitor’s current values on each page load. The cached page carries no threshold, and each visitor gets their own test group’s. Keep the route handler out of the middleware’s matcher, as /api already is: the client reads the visitor’s cookies from the request itself and sets none.
null for a visitor in no shipping test, or in a test group with no free rate, and offers is [] for a visitor in no offer test. The bar renders whichever the visitor has, and nothing when they have neither: render your usual bar in its place.
A cart drawer priced in the browser has the same limit: a page rendered from a page key hands the browser only the test prices for its own key, so the drawer shows other products at their original price. Price the drawer in the server action or route handler that returns its cart, with the client made from the request.
Server actions
A server action from a tested page is rewritten to the page’s key, so its response renders from the same route as the page. That needs thenext-action header Next.js sends when JavaScript calls the action. A form posted before the page hydrated, or with JavaScript off, renders its response from the usual route instead, with the original prices, until the next page load.
On a page that renders per request anyway, the x-abconvert-url request header holds the URL the visitor opened. Reading request headers makes a page dynamic, so do not read it on a page you want cached.
Revalidate the page key copies too
If you refresh a product page on demand, for example from Shopify’s product update webhook, revalidate both routes.revalidatePath('/products/blue-snowboard') refreshes the usual route only; the copies a test renders live under the page key route, and they keep the old product data until their revalidate window ends.
Server (route handler)
When the tests cannot be loaded
A page rendered from a page key is cached for everyone in its test groups, so it never falls back to your original store. When the tests the key names cannot be loaded,createABConvert with that key waits up to 10 seconds, or your timeout if longer, then rejects. The render fails and Next.js keeps its last good copy of the page, instead of caching your original store under a test group’s key. With no copy yet, that request fails. A page with an empty key never rejects this way. See Loading and failures.
A force from forceTestGroup on the server lasts only for that render, because the Next.js client sets no cookies. Use a preview link instead.
Cache tested pages at your CDN
Switch to the second mode once your tests run as expected. Two settings work together, and the order matters:
-
Add the
abconvert-keycookie and ABConvert’s three query parameters,abconvert_force,abconvert_preview_key, andabconvert_experiment_id, to your CDN’s cache key in the same change. The cookie keeps one copy per combination of test groups; the parameters keep preview and forced links out of those copies, so a preview link is never answered from a stored copy. On Google Cloud CDN:Cloud CDN keys on the full query string by default, which covers the three parameters. If your backend service leaves the query string out of its key, or keys only on a list of parameters, add these three. Do not narrow the key to only these three, or your search and filter pages share one copy. -
Then set
cdnKeyedByCookie:Server (middleware)
cdnKeyedByCookie first, then the cookie from the cache key.
In this mode the middleware sets abconvert-key to the visitor’s place in each price test, including the tests that leave them out. It holds no visitor or session ID and is the same on every page, so your CDN keeps one copy per combination of test groups, not one per visitor. Three things stay private whatever the mode: a response that sets a cookie, so a visitor’s first page is never cached; every page a URL redirect test covers; and any page when the visitor’s cookie does not match their test groups. A tested page your CDN stored before you changed a test stays until its s-maxage ends, since a CDN hit never reaches your middleware. See After you launch, edit, or end a test.
Two Google Cloud CDN details:
- Cloud CDN does not store a response whose
Varyheader names a header outside the few it supports, and Next.js sendsVary: RSCand other router headers. RewriteVarytoAccept-Encodingin front of Next.js, in the proxy or server that runs it, or Cloud CDN stores no page at all. - The cache key applies to every page behind the backend service, so untested pages are also stored once per cookie value. To keep them stored once, route your product, collection, and search paths to a second backend service with the cookie in its key.