Next.js
Server side A/B testing through Next.js middleware
This example runs a server side A/B test with the Next.js SDK. The middleware decides the variant before the page is rendered and rewrites the request to an internal route, so the first paint already matches the visitor's variant — no layout shift. The React SDK is still used on the client to post analytics.
Install
npm i @obelism/improve-sdk @obelism/improve-sdk-next @obelism/improve-sdk-reactShared config
A small shared config keeps the organization and environment in one place for both the middleware and the client provider.
import { ImproveSetupArgs } from '@obelism/improve-sdk/types'
export const IMPROVE_CONFIG: ImproveSetupArgs = {
organizationId: 'org_MJFL46Z0WXGQ5OHW1ZXSM3Q88S',
environment: 'staging',
} as constMiddleware
The middleware creates an ImproveServerSDK, fetches the config, and hands the request to generateImproveNextMiddleware. Each server test maps a route to the internal slugs visitors are rewritten to.
import { ImproveServerSDK } from '@obelism/improve-sdk/server'
import { generateImproveNextMiddleware } from '@obelism/improve-sdk-next'
import { type NextRequest } from 'next/server'
import { IMPROVE_CONFIG } from './app/improveConfig'
export const config = {
matcher: '/',
}
const improveSdk = new ImproveServerSDK({
...IMPROVE_CONFIG,
token: 'xxxx-xxxx-xxxx-xxxx',
})
const improveMiddlewareHandler = generateImproveNextMiddleware({
improveSdk,
serverABtests: [
{
slug: 'startpage-visual',
routeHandler: '/',
options: [
{
value: 'control',
slug: '/',
},
{
value: 'variation',
slug: '/variation',
},
],
},
],
})
export const middleware = async (request: NextRequest) => {
await improveSdk.fetchConfig()
return improveMiddlewareHandler(request)
}Note: the runnable example hardcodes the server token for brevity. In your own app load it from an environment variable so it never ends up in version control.
Client provider
The provider has to be a client component because it loads the SDK and fetches the config in a useEffect. Re-export it from a 'use client' file.
'use client'
import { generateImproveProvider } from '@obelism/improve-sdk-react'
import { IMPROVE_CONFIG } from './improveConfig'
const improveReact = generateImproveProvider(IMPROVE_CONFIG)
export const ImproveProvider = improveReact.ImproveProvider
export const useTestValue = improveReact.useTestValue
export const useFlagValue = improveReact.useFlagValue
export const usePostAnalytic = improveReact.usePostAnalyticWrap the app with the provider in the root layout:
import { ImproveProvider } from './improve'
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>
<ImproveProvider>{children}</ImproveProvider>
</body>
</html>
)
}Routes
Each variant is a normal route. The middleware rewrites / to /variation for visitors in that variant, so both pages render the same HomePage with a different variant prop.
import { HomePage } from './HomePage'
export default function Home() {
return <HomePage variant="control" />
}import { HomePage } from '../HomePage'
export default function Home() {
return <HomePage variant="variation" />
}Tracking
Client components post analytics with usePostAnalytic. It takes no arguments and events are test-independent — call it with an event name (and optional message) and the server attributes the event to whichever tests/flags the visitor was exposed to. Because this example resolves the variant server-side (via middleware rewrite), read the variant client-side with useTestValue so the SDK records the exposure that events are attributed to. TrackPageView reports a page load, Visual reports a click on the variant.
'use client'
import { useEffect } from 'react'
import { usePostAnalytic, useTestValue } from './improve'
export const TrackPageView = ({ page }: { page: string }) => {
const postAnalytic = usePostAnalytic()
// Reading the variant records the exposure events are attributed to.
useTestValue('startpage-visual')
useEffect(() => {
postAnalytic('page_view', page)
}, [page, postAnalytic])
return null
}'use client'
import Image from 'next/image'
import { usePostAnalytic } from './improve'
export const Visual = ({ variant }: { variant: 'control' | 'variation' }) => {
const postAnalytic = usePostAnalytic()
return (
<Image
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
style={variant === 'control' ? {} : { transform: 'scaleX(-1)' }}
onClick={() => {
postAnalytic('visual_click')
}}
/>
)
}