# Next.js

`@splithub/next` covers the App Router (Next.js 14+): a middleware for the visitor cookie and redirect experiments, helpers for Server Components, and a client provider that agrees with the server on the first byte and collects analytics. `npx splithub init` creates the middleware and prints a provider snippet; add the layout integration below.

```bash
npm i @splithub/next @splithub/react @splithub/core
```

## Middleware

```ts
// middleware.ts
import { splithubMiddleware } from '@splithub/next';
import splitHubConfig from './splithub.config';

export default splithubMiddleware(splitHubConfig);
export const config = { matcher: ['/((?!_next|api|.*\\..*).*)'] };
```

It sets the `sh_vid` cookie, passes the id to Server Components in a header, rewrites the request for `redirect` experiments (the URL in the address bar does not change, the visitor sees no flash) and records edge exposures. In cloud mode it fetches live overrides at most once per 60 s per isolate; opt out with `splithubMiddleware(splitHubConfig, { remote: false })`.

## Layout

```tsx
// app/layout.tsx (Server Component)
import { SplitHubNextProvider } from '@splithub/next/client';
import { getSplitHub } from '@splithub/next/server';
import config from '../splithub.config';

export default async function RootLayout({ children }) {
  const sh = await getSplitHub(config);
  return (
    <html>
      <body>
        <SplitHubNextProvider config={config} property="sh-x1h78w" visitorId={sh.visitorId ?? undefined} remoteOverrides={sh.overrides}>
          {children}
        </SplitHubNextProvider>
      </body>
    </html>
  );
}
```

## Analytics

`property` on `SplitHubNextProvider` names the [analytics property](/docs/analytics) that receives pageviews and events; `npx splithub analytics connect` adds it to your layout. Leave it out and the provider runs experiments only.

- **Pageviews**: one on the first load and one on every App Router navigation, with time on page and scroll depth. No `usePathname()` hook is needed.
- **Events**: `useTrack()` from `@splithub/next/client` in a Client Component. One `track('Signup')` counts in the property and for every experiment whose goal it is.
- **Users**: `useIdentify()` after sign-in, `identify(null)` on sign-out.

```tsx
'use client';
import { useTrack } from '@splithub/next/client';

export function PlayButton({ id }: { id: number }) {
  const track = useTrack();
  return <button onClick={() => track('Video Played', { id })}>Play</button>;
}
```

`next dev` sends nothing to SplitHub Cloud; check tracking on a deployed preview or with `next build && next start`.

## Client Components

Same API as `@splithub/react`, imported from `@splithub/next/client`: `Experiment`, `Variant`, `useVariant`, `useValue`, `useTrack`, `useIdentify`, `usePageview`, plus `Exposure`.

## Server Components

```tsx
import { getSplitHub } from '@splithub/next/server';
import { Exposure } from '@splithub/next/client';
import config from '../../splithub.config';

export default async function Page() {
  const sh = await getSplitHub(config);
  const title = sh.value<string>('hero-title');
  return (
    <>
      <h1>{title}</h1>
      <Exposure id="hero-title" />
    </>
  );
}
```

`getSplitHub(config)` gives `.variant(id)` and `.value(id)` computed from the cookie the middleware set. `<Exposure>` records the exposure once on the client, so a server-rendered variant counts exactly like a client-rendered one.

## Self-hosted ingest route

Install `@splithub/node` and `@splithub/adapter-sqlite` for this example. SQLite requires Node.js 22.13 or newer.

```ts
// app/api/splithub/[[...path]]/route.ts
import { createSplitHubHandler } from '@splithub/node';
import { sqliteStore } from '@splithub/adapter-sqlite';
import config from '../../../../splithub.config';

const handler = createSplitHubHandler({ config, store: sqliteStore(), adminToken: process.env.SPLITHUB_ADMIN_TOKEN });
export const POST = handler;
export const GET = handler;
export const runtime = 'nodejs';
```

In cloud mode this file is not needed.

## Redirect experiments

Declare `kind: 'redirect'` with `match` and a `to` per variant; no code. The middleware rewrites `/` to `/start` for the `new-landing` variant and records the exposure at the edge. Details and the drop-in alternative: [Redirect experiments](/docs/redirect-experiments).

## Why there is no flicker

The middleware decides the visitor id before anything renders. Server Components compute the same deterministic assignment as the client, and the client provider receives that id and the same overrides as props. Both sides agree on the first byte, so hydration never swaps a variant.
