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.
Personalization always needs an SDK token, the same one used for events. A browser client gets it from your token route. The call is rejected without one.
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.
{
"personalized": true,
"baseEntryId": "115680",
"swaps": [
{ "identifier": "hero_banner_list", "replacementEntryId": "166060", "mode": "section" }
],
"content": { "…": "finished content, with merge: true" }
}merge: true, Xenia fetches and merges the content from your connected
Content Cloud space and returns it ready to render in content.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.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.
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 attachedIn Next.js, xeniaServerFromNext() does this
binding for you.
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.
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.
Render the default first and swap in the personalized version when it arrives.
The hooks do this for you. On the server, pass { fallback: null } and render the
default whenever the result is null.