XeniaXenia Docs

Personalization with the SDK

PreviousNext

Resolve a content key to the right content for each visitor, render the default on any failure, and track how slots perform.

A marketer sets up personalization in a journey's Content Cloud node: the default content, a content key, and the rules that pick alternatives. Your site asks for that content key and renders what comes back. The content key is the contract between the two, so the rules can change without a deploy.

Resolve a content key

import { usePersonalizationResolve } from "@xenia-martech/sdk-js/client/react";
 
function Hero({ base }: { base: HeroContent }) {
  const { data, isLoading } = usePersonalizationResolve("homepage-hero", {
    data: { segment: "vip" },
    merge: true,
  });
  const hero = (data?.content as HeroContent | null) ?? base;
  return <h1>{isLoading ? base.title : hero.title}</h1>;
}

The default renders immediately and is replaced once the answer arrives.

data carries whatever the rules decide on, such as a segment or country. Use locale to ask for a specific language.

What comes back

{
  "personalized": true,
  "baseEntryId": "115680",
  "swaps": [
    { "identifier": "hero_banner_list", "replacementEntryId": "166060", "mode": "section" }
  ],
  "content": { "…": "finished content, with merge: true" }
}
  • With merge: true, Xenia fetches and merges the content from your connected Content Cloud space and returns it ready to render in content.
  • Without it, apply swaps against your own CMS: replace the block named by identifier with the entry replacementEntryId. mode is section to replace the whole block, or items to keep the block and swap what is inside it.
  • When no rule matches, personalized is false, swaps is empty, and you render the default.

To resolve several keys at once, use resolveBatch(items) (up to 50) or the usePersonalizationResolveSlots() hook.

Fields the SDK adds for you

Every resolve call carries a fixed set of fields the rules can use without you sending them: path, query, url, referrer, deviceId, visitorId, customerId, email, and the UTM fields utmSource, utmMedium, utmCampaign, utmContent and utmId. They cannot be overridden by your own data.

In the browser they are collected automatically, and customerId and email come from identify(visitorId, { customerId, email }). On the server, bind them once per request:

const xenia = createXeniaServer({
  tenantId, environmentKey, token,
  visitorId: getVisitorId(cookieHeader),
  context: contextFromUrl(currentUrl, referrer),
});
 
await xenia.resolve("homepage-hero"); // fields already attached

In Next.js, xeniaServerFromNext() does this binding for you.

Tracking slot performance

To see impressions and clicks for a personalized slot, mark the element you render it into:

<div
  data-xenia-slot="offer-slot"
  data-xenia-content-key="offer.hero"
  data-xenia-content-entry-id="entry-99"
  data-xenia-base-entry-id="entry-1"
  data-xenia-personalized="true"
>
  …
</div>

The SDK then records an impression when the slot scrolls into view and a click when it is clicked. Holdout exposure is recorded by the resolve call itself, so there is nothing to add for that.

Personalization from a journey reply

For a decision that belongs in a journey rather than a content rule, call a webhook trigger and render its reply as data:

import { usePersonalization } from "@xenia-martech/sdk-js/client/react";
 
const { data, isLoading } = usePersonalization("<trigger id>", { segment: "vip" });
const hero = (data as HeroContent | null) ?? base;

The Webhook Response node returns whatever JSON you want rendered. To fill several slots from one reply, use usePersonalizationSlots(triggerId, slots, data). The reply's xeniaContentSwaps are spliced into your base slots, and any slot without a swap comes back unchanged.

Failing safely