@trevosdk/nextjs adds the
one thing only a server can do: resolve variants before the page renders, so first-time
visitors never see the control version flash.
next is an optional peer dependency (>= 14).
1. Request hook: Proxy or Middleware
Sets a stabletrevo_id cookie so the server and browser bucket the same visitor
identically. Use the file and named export for your Next.js version:
For Next.js 16 and newer:
proxy.* and middleware.* to one
app: Next.js treats that as a conflict. Put the file beside the app’s app or pages
directory. That is <app-package>/proxy.ts when app/ or pages/ is at the root, or
<app-package>/src/proxy.ts for src/app/ or src/pages/; use the equivalent
middleware.ts path on Next.js 14/15. In a monorepo, <app-package> means the Next.js
app directory, not the workspace root.
If next.config.* defines custom pageExtensions, the request-hook filename must use one
of those suffixes. For example, pageExtensions: ['page.tsx', 'page.ts'] requires
proxy.page.ts on Next.js 16+ or middleware.page.ts on Next.js 14/15.
Write the matcher out as a literal, as above. Next extracts export const config
by static analysis and rejects imported identifiers, so importing a shared
constant fails next dev outright and, in a production build, silently falls back
to running the request hook on every request — static assets included.
Compose with auth or internationalization
An app can have only one Proxy or Middleware. If auth, internationalization, or another integration already owns it, wrap that handler instead of adding a second file:middleware.ts and export it as
middleware:
withTrevo alias is
also available for shared setup code.
Without this, the server has no identity to bucket on for a first-time visitor.
2. Resolve on the server
In a server component on the page running the experiment:getTrevoBootstrap() reads the trevo_id cookie and the API key from the environment, then
resolves every experiment with the same deterministic hash the browser uses. The client
starts with those assignments already in hand, so the first paint is correct.
It fails soft: if the config fetch fails, it returns an empty map and the client resolves on
its own as usual.
Previews work through it too: the request hook forwards ?trevo_force=key:variant to the render
as an x-trevo-force header, and getTrevoBootstrap() serves the forced variant instead of a
real assignment, so a page you preview doesn’t flip on hydration. Without the request hook, pass
forceVariants yourself. Forced renders record no exposure, and the browser stamps everything
it sends during a preview so nothing from a QA tab reaches results.
It takes options: experimentKeys fetches only the experiments the route needs, and
revalidate controls how long the server-side config fetch is cached. A signed-in user’s
id goes here too — see the rule below.
(The provider itself emits one page_view event with path when it initialises — don’t
add your own page_view call or visits count twice.)
Keeping most routes static
Reading a cookie forces a route to render dynamically. To confine that to the pages that need it, skip the provider-wide bootstrap and pass a single experiment instead:initialVariant takes precedence over the provider’s bootstrap.
Pages Router
The same primitives work fromgetServerSideProps:
bootstrap to <TrevoProvider> exactly as in the App Router example.
3. Track conversions as normal
@trevosdk/node instead. Those events cannot be lost to an ad blocker or a
closed tab.
Other backends
The Next.js entry is a thin wrapper.@trevosdk/nextjs/server has no Next.js imports, so
the same primitives run on any Node 18+ or edge runtime:
resolveExperiments() never records an exposure — the client does that when the variant
actually renders. Never replace a missing cookie with an id generated only for this call:
persist it on the response first, or the same visitor can be re-bucketed on every request.
The rule that keeps client and server agreeing
Both sides must bucket on the same identity at the same moment: the user id when the visitor is identified, otherwise thetrevo_id cookie value. Anything else assigns one
person different variants on the server and the client, which corrupts the experiment.
For anonymous visitors the request hook plus getTrevoBootstrap() handle this. Once a
visitor signs in, the browser buckets on their user id — pass it to the server too:
await getTrevoBootstrap({ userId: session?.user.id }). Omit it and SSR buckets on the
cookie while the client buckets on the user id: the variant flips on hydration. If you
hand-roll with resolveExperiments(), all of this is your responsibility.