Docs / SDKs View as Markdown

Next.js

Middleware, Server Components, redirect experiments without flicker, live overrides.

@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. npx splithub init creates the middleware and prints a provider snippet; add the layout integration below.

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

Middleware

// 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

// 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} visitorId={sh.visitorId ?? undefined} remoteOverrides={sh.overrides}>
          {children}
        </SplitHubNextProvider>
      </body>
    </html>
  );
}

Client Components

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

Server Components

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.

// 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.

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.

SplitHub
Test ideas. Ship winners.