By following this guide, you will build a working AI-powered personalisation engine that dynamically serves different content to visitors based on their behaviour, location, and session context — all in real time at the edge. The full implementation takes roughly two to three hours and runs entirely on Vercel's Edge Network using Next.js 15, Vercel Edge Middleware, and the OpenAI API (as of July 2026).
What You'll Build
- An Edge Middleware layer that captures visitor signals (location, UTM source, referrer, past session data) before a page renders
- An OpenAI-powered classification step that maps each visitor to a personalisation segment in under 80 ms
- A Next.js 15 App Router layout that swaps hero copy, CTAs, and featured content per segment without a full page reload
- A lightweight analytics hook that records which variant each visitor sees, ready to feed into your A/B reporting tool
- A local development workflow that mocks edge context so you can test every segment on your machine
Prerequisites
- Node.js 20+ and pnpm 9 installed locally
- A Vercel account (Hobby tier works; Pro unlocks higher edge function concurrency)
- An OpenAI API key with access to
gpt-4o-mini(the model used in this guide) - Familiarity with Next.js App Router basics and TypeScript
- A working Next.js 15 project or a fresh scaffold — run
pnpm create next-app@latest my-personalised-site --typescriptto start
Step 1: Install Dependencies and Configure Environment Variables
Start by adding the packages you need. Keep the dependency footprint small — Edge Middleware has a 4 MB bundle size limit on Vercel.
pnpm add openai @vercel/edge geist
pnpm add -D @types/node
Create a .env.local file at the project root:
OPENAI_API_KEY=sk-...your-key...
NEXT_PUBLIC_SITE_URL=http://localhost:3000
PERSONALISATION_SECRET=some-random-32-char-string
Why does this step matter? Edge functions run outside the standard Node.js runtime. They cannot read environment variables at build time the same way serverless functions can. Vercel injects secrets defined in your project dashboard at request time — make sure you mirror your .env.local values in the Vercel dashboard under Settings → Environment Variables before deploying.
Common pitfall: Do not prefix OPENAI_API_KEY with NEXT_PUBLIC_. That would expose your key to the browser bundle.
Step 2: Define Your Personalisation Segments
Segments are the engine's output vocabulary. Define them in a shared constants file so both your middleware and your UI components reference the same values.
// lib/segments.ts
export const SEGMENTS = [
'enterprise-buyer',
'smb-owner',
'developer',
'marketer',
'returning-user',
'default',
] as const;
export type Segment = typeof SEGMENTS[number];
Each segment maps to a different content variant. For example, an enterprise-buyer visiting from a LinkedIn ad might see a case-study hero and a "Book a Demo" CTA. A developer arriving from a GitHub link sees a code-first headline and a documentation link. Keep segments between four and eight — more than that, and classification accuracy drops with lightweight models.
Step 3: Build the AI Classification Function
Create a utility that sends visitor signals to OpenAI and returns a segment. Run this inside Edge Middleware, not in a React Server Component — you want classification to happen before the HTML is generated.
// lib/classify-visitor.ts
import OpenAI from 'openai';
import type { Segment } from './segments';
import { SEGMENTS } from './segments';
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
export async function classifyVisitor(signals: {
country: string | null;
utmSource: string | null;
utmMedium: string | null;
referrer: string | null;
path: string;
}): Promise {
const prompt = `You are a visitor segmentation engine. Based on the signals below, return exactly one segment from this list: ${SEGMENTS.join(', ')}. Return only the segment string — no explanation.
Signals:
- Country: ${signals.country ?? 'unknown'}
- UTM Source: ${signals.utmSource ?? 'none'}
- UTM Medium: ${signals.utmMedium ?? 'none'}
- Referrer: ${signals.referrer ?? 'direct'}
- Page path: ${signals.path}`;
const response = await client.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
max_tokens: 20,
temperature: 0,
});
const raw = response.choices[0]?.message?.content?.trim().toLowerCase() ?? 'default';
return (SEGMENTS as readonly string[]).includes(raw) ? (raw as Segment) : 'default';
}
Why use gpt-4o-mini here? It delivers latency under 300 ms at the edge in most cases, and classification prompts use fewer than 150 tokens. At July 2026 pricing, classifying 100,000 visitors costs under $0.50 USD. If you need sub-50 ms latency, replace the OpenAI call with a local rules-based classifier and reserve the model for ambiguous cases.
Step 4: Write the Edge Middleware
Vercel Edge Middleware runs before your Next.js routes resolve. Use it to classify the visitor and write the segment into a cookie.
// middleware.ts (project root)
import { NextRequest, NextResponse } from 'next/server';
import { classifyVisitor } from './lib/classify-visitor';
import type { Segment } from './lib/segments';
const SEGMENT_COOKIE = 'lk_segment';
const COOKIE_MAX_AGE = 60 * 30; // 30 minutes
export const config = {
matcher: ['/', '/pricing', '/features/:path*'],
};
export async function middleware(req: NextRequest) {
// Skip classification if a fresh segment cookie already exists
const existing = req.cookies.get(SEGMENT_COOKIE)?.value as Segment | undefined;
if (existing) return NextResponse.next();
const url = req.nextUrl;
const segment = await classifyVisitor({
country: req.geo?.country ?? null,
utmSource: url.searchParams.get('utm_source'),
utmMedium: url.searchParams.get('utm_medium'),
referrer: req.headers.get('referer'),
path: url.pathname,
});
const response = NextResponse.next();
response.cookies.set(SEGMENT_COOKIE, segment, {
maxAge: COOKIE_MAX_AGE,
httpOnly: false, // readable by client JS for analytics
sameSite: 'lax',
secure: process.env.NODE_ENV === 'production',
});
return response;
}
What if the OpenAI call fails? The function falls back to 'default' silently. You can add a try/catch that logs to a Vercel Log Drain without breaking the response. Never let a classification error block page delivery.
Step 5: Read the Segment in Your React Components
Because the cookie is readable by JavaScript (httpOnly: false), you can access it in both Server and Client Components. For Server Components, read it from the incoming request headers using Next.js's cookies() helper.
// app/page.tsx
import { cookies } from 'next/headers';
import type { Segment } from '@/lib/segments';
import { HeroContent } from '@/components/HeroContent';
export default async function HomePage() {
const cookieStore = await cookies();
const segment = (cookieStore.get('lk_segment')?.value ?? 'default') as Segment;
return (
);
}
// components/HeroContent.tsx
import type { Segment } from '@/lib/segments';
const COPY: Record = {
'enterprise-buyer': { headline: 'Scale with confidence. We build for teams of 200+.', cta: 'Book a Strategy Call' },
'smb-owner': { headline: 'Grow your business without growing your team.', cta: 'See Pricing' },
'developer': { headline: 'Clean APIs. Zero compromise on DX.', cta: 'Read the Docs' },
'marketer': { headline: 'Marketing that moves as fast as your campaigns.', cta: 'View Case Studies' },
'returning-user': { headline: 'Welcome back. Here is what is new.', cta: 'See Updates' },
'default': { headline: 'Build better digital experiences.', cta: 'Get Started' },
};
export function HeroContent({ segment }: { segment: Segment }) {
const { headline, cta } = COPY[segment];
return (
{headline}
{cta}
);
}
Step 6: Track Variants for Analytics
Personalisation without measurement is guesswork. Add a lightweight client-side hook that fires a custom event to GA4 or any analytics endpoint when a visitor sees a variant.




