Examples
Nuxt
Server side A/B testing through Nuxt server middleware
This example runs a server side A/B test with the Nuxt SDK. A server middleware decides the variant before the page renders, so the first paint already matches the visitor's variant: no layout shift. The Vue SDK is still used in the browser to record the exposure and post analytics.
Install
npm i @obelism/improve-sdk @obelism/improve-sdk-nuxt @obelism/improve-sdk-vueShared config
A small shared config keeps the organization and environment in one place for both the middleware and the provider. Nuxt's shared/ folder is for code both sides import.
import type { ImproveSetupArgs } from '@obelism/improve-sdk/types'
export const IMPROVE_CONFIG: ImproveSetupArgs = {
organizationId: 'org_MJFL46Z0WXGQ5OHW1ZXSM3Q88S',
environment: 'staging',
}The API token stays on the server, in the private runtime config, filled from NUXT_IMPROVE_API_TOKEN:
export default defineNuxtConfig({
runtimeConfig: {
improveApiToken: '',
},
})Middleware
The middleware creates an ImproveServerSDK and hands it to generateImproveNuxtMiddleware, with the route each server test runs on. It fetches the datafile on the first request.
import { ImproveServerSDK } from '@obelism/improve-sdk/server'
import { generateImproveNuxtMiddleware } from '@obelism/improve-sdk-nuxt'
import { IMPROVE_CONFIG } from '#shared/improveConfig'
const improveSdk = new ImproveServerSDK({
...IMPROVE_CONFIG,
token: () => useRuntimeConfig().improveApiToken,
// Pages wait for the datafile: fail fast rather than retry.
configRetries: 0,
})
export default generateImproveNuxtMiddleware({
improveSdk,
serverABtests: [{ slug: 'startpage-visual', routeHandler: '/' }],
})Client provider
The provider loads the client SDK and its config after mount. Generate it once and export what the pages use:
import { generateImproveProvider } from '@obelism/improve-sdk-vue'
import { IMPROVE_CONFIG } from '#shared/improveConfig'
export const { ImproveProvider, useImproveStatus, useTestValue, usePostAnalytic } =
generateImproveProvider(IMPROVE_CONFIG)Wrap the app with the provider:
<script setup lang="ts">
import { ImproveProvider } from './improve'
</script>
<template>
<ImproveProvider>
<NuxtPage />
</ImproveProvider>
</template>The page
Unlike Next.js, there is no route per variant: Nuxt hydrates the route the browser shows, so the page renders the variant itself. useState hands the server's decision to the browser, and useTestValue renders it: the server's value as the fallback, then the same value read from the cookie once the SDK is set up. After a client-side navigation no request reached the middleware, and the browser decides.
<script setup lang="ts">
import { getImproveContext } from '@obelism/improve-sdk-nuxt'
import { useTestValue } from '~/improve'
const decided = useState('improve:startpage-visual', () => {
const event = useRequestEvent()
return (event && getImproveContext(event)['startpage-visual']) || null
})
// Reading the variant in the browser records the exposure events are attributed to.
const variant = useTestValue('startpage-visual', decided.value ?? 'control')
useTrackPageView('homepage')
</script>
<template>
<main>
<Visual :variant="variant === 'variation' ? 'variation' : 'control'" />
</main>
</template>Tracking
The page posts analytics with usePostAnalytic. Events are test-independent: call it with an event name and a required scope and the server attributes the event to whichever tests/flags the visitor was exposed to. useTrackPageView reports a page load once the SDK is set up, Visual reports a click on the variant.
import { useImproveStatus, usePostAnalytic } from '~/improve'
export const useTrackPageView = (page: string) => {
const status = useImproveStatus()
const postAnalytic = usePostAnalytic()
// Nothing is sent before the SDK is set up.
watch(
status,
(current) => {
if (current === 'setup') postAnalytic('page_view', page)
},
{ immediate: true },
)
}<script setup lang="ts">
import { usePostAnalytic } from '~/improve'
defineProps<{ variant: 'control' | 'variation' }>()
const postAnalytic = usePostAnalytic()
</script>
<template>
<img
src="/nuxt.svg"
alt="Nuxt logo"
width="180"
height="37"
:style="variant === 'control' ? {} : { transform: 'scaleX(-1)' }"
@click="postAnalytic('visual_click', 'homepage')"
/>
</template>Keep tested routes out of prerendering and route caches (prerender, swr, isr in routeRules): a cached page shows one variant to everyone. The middleware marks a tested page Cache-Control: private for the same reason.