Skip to main content

Installation

npm install @rlvt/web-sdk
No additional dependencies. The adapter uses structural typing — it does not import from @remix-run/node or @remix-run/server-runtime.

Setup

1. Create the client instance

// app/lib/reelevant.server.ts
import { ReelevantClient } from '@rlvt/web-sdk'

export const rlvt = new ReelevantClient({
  timeout: 50,
})

2. Ensure identity in the root loader

// app/root.tsx
import { ensureIdentityCookie } from '@rlvt/web-sdk/remix'
import type { LoaderFunctionArgs } from '@remix-run/node'

export async function loader({ request }: LoaderFunctionArgs) {
  const identityCookie = ensureIdentityCookie(request)

  return new Response(JSON.stringify({}), {
    headers: identityCookie ? { 'Set-Cookie': identityCookie } : {},
  })
}

Request flow

Using createLoader

The createLoader helper auto-extracts identity and context from the Remix request:
// app/routes/_index.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { createLoader } from '@rlvt/web-sdk/remix'
import { rlvt } from '~/lib/reelevant.server'

export async function loader({ request }: LoaderFunctionArgs) {
  const { run, runAll } = createLoader({ client: rlvt, request })

  const hero = await run({ workflowId: 'wf-hero', entrypoint: '43a490a0' })
  return json({ hero })
}

export default function Index() {
  const { hero } = useLoaderData<typeof loader>()

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

  return <DefaultHero />
}

Multiple zones

export async function loader({ request }: LoaderFunctionArgs) {
  const { runAll } = createLoader({ client: rlvt, request })

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

  return json({ hero, sidebar })
}

Lower-level helpers

runOptionsFromRequest(request)

Extract identity and context fields manually:
import { runOptionsFromRequest } from '@rlvt/web-sdk/remix'

export async function loader({ request }: LoaderFunctionArgs) {
  const context = runOptionsFromRequest(request)
  // context = { userId, userAgent, ip, referer }

  const result = await rlvt.run({
    workflowId: 'wf-hero',
    entrypoint: '43a490a0',
    ...context,
  })

  return json({ result })
}

ensureIdentityCookie(request)

Returns a Set-Cookie header string if no identity exists, or null if the visitor already has one:
import { ensureIdentityCookie } from '@rlvt/web-sdk/remix'

export async function loader({ request }: LoaderFunctionArgs) {
  const identity = ensureIdentityCookie(request)
  const data = { /* ... */ }

  return json(data, {
    headers: identity ? { 'Set-Cookie': identity } : {},
  })
}

Handling JSON responses

export default function Page() {
  const { zone } = useLoaderData<typeof loader>()

  if (zone.body.type === 'json') {
    const { products } = zone.body.content as { products: Product[] }
    return (
      <div className="grid grid-cols-3 gap-4">
        {products.map(p => <ProductCard key={p.id} product={p} />)}
      </div>
    )
  }

  return <DefaultContent />
}

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:
// Redirect link — use redirectionUrl as <a href>
export default function Page() {
  const { hero } = useLoaderData<typeof loader>()

  return (
    <div data-rlvt-ssr="true">
      {hero.body.type === 'html' && (
        <>
          <div dangerouslySetInnerHTML={{ __html: hero.body.content }} />
          <a href={hero.redirectionUrl}>Shop now</a>
        </>
      )}
    </div>
  )
}
// Server-side fire-and-forget (in an action)
export async function action({ request }: ActionFunctionArgs) {
  const { run } = createLoader({ client: rlvt, request })
  const result = await run({ workflowId: 'wf-hero', entrypoint: '43a490a0' })
  await result.trackClick()
  return json({ ok: true })
}
See Core SDK — Click tracking for full details.

Compatibility with the client tracker

Add data-rlvt-ssr="true" to your wrapper element. The client-side tracker automatically skips server-rendered zones.