Skip to main content

Installation

npm install @rlvt/web-sdk react

Setup

1. Create the client instance

Create a shared client instance in a server-only module:
// lib/reelevant.ts
import { ReelevantClient } from '@rlvt/web-sdk'

export const rlvt = new ReelevantClient({
  timeout: 50,  // tight timeout for SSR — falls back gracefully
})

2. Add identity middleware

Create middleware.ts at the root of your project:
// middleware.ts
import { createMiddleware } from '@rlvt/web-sdk/next'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const ensureIdentity = createMiddleware()

export function middleware(request: NextRequest) {
  const response = NextResponse.next()
  ensureIdentity(request, response)
  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
This ensures every visitor has an rlvt_tmpId cookie before any personalised content is requested.

Request flow

The ReelevantZone is an async React Server Component that fetches and renders personalised content:
// app/page.tsx
import { ReelevantZone } from '@rlvt/web-sdk/next'
import { extractUserId } from '@rlvt/web-sdk'
import { cookies } from 'next/headers'
import { rlvt } from '@/lib/reelevant'

export default async function Page() {
  const cookieStore = await cookies()
  const userId = extractUserId(cookieStore.toString())

  return (
    <main>
      <ReelevantZone
        client={rlvt}
        workflowId="wf-hero-banner"
        entrypoint="43a490a0"
        userId={userId}
        className="hero-section"
        fallback={<DefaultHero />}
      />
    </main>
  )
}

function DefaultHero() {
  return <div className="hero-section">Welcome to our store</div>
}

ZoneProps

PropTypeDescription
clientReelevantClientPre-configured client instance
workflowIdstringWorkflow ID
entrypointstringEntrypoint shortId (8-char alphanumeric, e.g. 43a490a0)
userIdstring?Visitor identity
paramsRecord<string, string>?URL parameters
localestring?Locale
userAgentstring?User-Agent
ipstring?Client IP
refererstring?Page URL
classNamestring?CSS class on wrapper
fallbackReactNode?Shown when content is empty
render(result: RunResult) => ReactNodeCustom render for JSON

Custom rendering with JSON

For headless personalisation where you want to build your own UI:
<ReelevantZone
  client={rlvt}
  workflowId="wf-products"
  entrypoint="f6a83d09"
  userId={userId}
  render={(result) => {
    if (result.body.type !== 'json') return null
    const { products } = result.body.content as { products: Product[] }
    return (
      <div className="grid grid-cols-3 gap-4">
        {products.map(p => <ProductCard key={p.id} product={p} />)}
      </div>
    )
  }}
/>

Manual usage (without ReelevantZone)

If you prefer direct control:
// app/page.tsx
import { rlvt } from '@/lib/reelevant'
import { extractUserId } from '@rlvt/web-sdk'
import { cookies, headers } from 'next/headers'

export default async function Page() {
  const cookieStore = await cookies()
  const headersList = await headers()
  const userId = extractUserId(cookieStore.toString())

  const result = await rlvt.run({
    workflowId: 'wf-hero',
    entrypoint: '43a490a0',
    userId,
    userAgent: headersList.get('user-agent') ?? undefined,
  })

  if (result.body.type === 'html') {
    return (
      <div
        data-rlvt-ssr="true"
        dangerouslySetInnerHTML={{ __html: result.body.content }}
      />
    )
  }

  return <DefaultContent />
}

Multiple zones on a page

Fetch multiple zones in parallel for optimal performance:
export default async function Page() {
  const cookieStore = await cookies()
  const userId = extractUserId(cookieStore.toString())

  const [hero, sidebar] = await rlvt.runAll([
    { workflowId: 'wf-hero', entrypoint: '43a490a0', userId },
    { workflowId: 'wf-sidebar', entrypoint: 'e5f302b8', userId },
  ])

  return (
    <main>
      <ReelevantZone client={rlvt} workflowId="wf-hero" entrypoint="43a490a0" userId={userId} />
      <aside>
        <ReelevantZone client={rlvt} workflowId="wf-sidebar" entrypoint="e5f302b8" userId={userId} />
      </aside>
    </main>
  )
}
Each ReelevantZone is an independent async component that fetches in parallel when rendered at the same level. You don’t need to use runAll explicitly with components — Next.js handles the parallel fetching automatically.

Click tracking

Click tracking must always be set up after display. Every content display should have a corresponding click tracking mechanism — either a redirect link or a trackClick() call.
Every RunResult includes redirectionUrl and trackClick(). Two patterns: Use redirectionUrl directly as an <a href>:
export default async function Page() {
  const cookieStore = await cookies()
  const userId = extractUserId(cookieStore.toString())

  const result = await rlvt.run({ workflowId: 'wf-promo', entrypoint: '1a7bc4d2', userId })

  return (
    <div data-rlvt-ssr="true">
      {result.body.type === 'html' && (
        <>
          <div dangerouslySetInnerHTML={{ __html: result.body.content }} />
          <a href={result.redirectionUrl}>Shop now</a>
        </>
      )}
    </div>
  )
}

Server-side fire-and-forget

Call result.trackClick() from a Server Action — it records the click and swallows all errors:
// app/actions.ts
'use server'
import { rlvt } from '@/lib/reelevant'

export async function handleClick(workflowId: string, entrypoint: string, userId: string) {
  const result = await rlvt.run({ workflowId, entrypoint, userId })
  await result.trackClick()
}
See Core SDK — Click tracking for full details.

Compatibility with the client tracker

The SDK adds data-rlvt-ssr="true" to rendered elements. The client-side Reelevant tracker automatically skips zones with this attribute, so there is no double-loading or flickering. You can still use the client tracker on the same page for:
  • Event tracking (clicks, impressions, conversions)
  • Client-only zones (consent-dependent content, overlays)
  • On-site integrations (datalayer triggers, URL rules)