> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trevosdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Next.js

> Resolve variants on the server so the first paint is correct for everyone.

Everything in [React](/install/react) works in Next.js as-is. `@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.

```bash theme={null}
npm install @trevosdk/nextjs @trevosdk/react
```

`next` is an optional peer dependency (>= 14).

## 1. Request hook: Proxy or Middleware

Sets a stable `trevo_id` cookie so the server and browser bucket the same visitor
identically. Use the file and named export for your Next.js version:

| Next.js version | File | Trevo export |
| - | - | - |
| 16 and newer | `proxy.ts` (or `proxy.js`) | `proxy` |
| 14 and 15 | `middleware.ts` (or `middleware.js`) | `middleware` |

For Next.js 16 and newer:

```ts theme={null}
// proxy.ts
export { proxy } from '@trevosdk/nextjs';

export const config = {
  matcher: ['/((?!api(?:/|$)|_next/static|_next/image|favicon\\.ico|sitemap\\.xml|robots\\.txt|.*\\.(?:avif|css|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|otf|pdf|png|svg|ttf|txt|wav|webm|webp|woff2?|xml|zip)$).*)'],
};
```

For Next.js 14 and 15:

```ts theme={null}
// middleware.ts
export { middleware } from '@trevosdk/nextjs';

export const config = {
  matcher: ['/((?!api(?:/|$)|_next/static|_next/image|favicon\\.ico|sitemap\\.xml|robots\\.txt|.*\\.(?:avif|css|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|otf|pdf|png|svg|ttf|txt|wav|webm|webp|woff2?|xml|zip)$).*)'],
};
```

Create **exactly one** of these files. Never add both `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:

```ts theme={null}
// proxy.ts (Next.js 16+)
import { withTrevoProxy } from '@trevosdk/nextjs';
import { existingProxy } from './lib/existing-proxy';

export const proxy = withTrevoProxy(existingProxy);

// Keep the app's existing matcher/config here as a literal.
export const config = {
  matcher: ['/((?!api(?:/|$)|_next/static|_next/image|favicon\\.ico|sitemap\\.xml|robots\\.txt|.*\\.(?:avif|css|eot|gif|ico|jpe?g|js|json|map|mjs|mp3|mp4|otf|pdf|png|svg|ttf|txt|wav|webm|webp|woff2?|xml|zip)$).*)'],
};
```

On Next.js 14/15, put the same pattern in `middleware.ts` and export it as
`middleware`:

```ts theme={null}
import { withTrevoMiddleware } from '@trevosdk/nextjs';
import { existingMiddleware } from './lib/existing-middleware';

export const middleware = withTrevoMiddleware(existingMiddleware);
```

Both helpers keep the existing response body and status, redirects and rewrites, response
headers and cookies, and downstream request-header overrides. They then add Trevo's
identity and preview headers to that same result. The version-neutral `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:

```tsx theme={null}
// app/<experiment-segment>/layout.tsx — use the nearest shared layout, not the root
import type { ReactNode } from 'react';
import { getTrevoBootstrap } from '@trevosdk/nextjs';
import { TrevoProvider } from '@trevosdk/react';

export default async function Layout({ children }: { children: ReactNode }) {
  const apiKey = process.env.NEXT_PUBLIC_TREVO_API_KEY;
  if (!apiKey) throw new Error('Missing NEXT_PUBLIC_TREVO_API_KEY');
  const bootstrap = await getTrevoBootstrap({
    apiKey,
    experimentKeys: ['checkout-cta'],
  });

  return (
    <TrevoProvider apiKey={apiKey} bootstrap={bootstrap}>
      {children}
    </TrevoProvider>
  );
}
```

`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-rule-that-keeps-client-and-server-agreeing).

(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:

```tsx theme={null}
const bootstrap = await getTrevoBootstrap();

<CheckoutCta initialVariant={bootstrap['checkout-cta']} />
```

```tsx theme={null}
'use client';
const variant = useExperiment('checkout-cta', { initialVariant });
```

`initialVariant` takes precedence over the provider's `bootstrap`.

### Pages Router

The same primitives work from `getServerSideProps`:

```tsx theme={null}
// pages/pricing.tsx
import { resolveExperiments, TREVO_ID_COOKIE } from '@trevosdk/nextjs/server';

export async function getServerSideProps({ req }) {
  const apiKey = process.env.NEXT_PUBLIC_TREVO_API_KEY;
  if (!apiKey) throw new Error('Missing NEXT_PUBLIC_TREVO_API_KEY');
  const bootstrap = await resolveExperiments({
    apiKey,
    bucketingId: req.cookies[TREVO_ID_COOKIE] ?? '',
  });
  return { props: { bootstrap } };
}
```

Pass `bootstrap` to `<TrevoProvider>` exactly as in the App Router example.

## 3. Track conversions as normal

```tsx theme={null}
'use client';
const trevo = useTrevo();
trevo?.track('purchase_completed', { value: 49.99 });
```

For conversions that happen on your backend — payment webhooks especially — use
[`@trevosdk/node`](/install/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:

```ts theme={null}
import { resolveExperiments, TREVO_ID_COOKIE } from '@trevosdk/nextjs/server';

const bucketingId = req.cookies[TREVO_ID_COOKIE];
const variants = bucketingId
  ? await resolveExperiments({ apiKey, bucketingId })
  : {}; // Return control until a request hook has persisted a stable id.
```

`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 the `trevo_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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.