Configuring Adobe Target Integration

This article will walk you through the steps of setting up an integration with Adobe Target so you can personalize your pages via the Adobe Target Visual Experience Composer (VEC). Before you go further, please also check our native Experimentation capabilities.

We support 2 different ways of integrating with Adobe Target. The recommended future-proof approach is via the Adobe Experience Platform WebSDK, but we also support the legacy Adobe Target at.js approach.

Considerations

Before we dive into the technical implementations, let us quickly recap how Adobe Target works so we set the proper expectations.

Adobe Target lets you modify the content of an existing page based on the personalization parameters you define in Adobe Target Visual Experience Composer (VEC) or equivalent. The rules will be dynamically evaluated server-side and Adobe Target will deliver a list of page modifications that will be applied to the rendered page after the blocks have been decorated.

There are thus a few things to keep in mind:

  1. Page modifications are done on the final page markup after the blocks have been decorated. So if you want to change block behaviors based on modifications that Adobe Target will be doing (like setting a CSS class or attribute), you’ll have to leverage the MutationObserver API
  2. Adobe Target will be modifying blocks after they have been decorated and shown on the page. In most cases, this is not an issue as applying the modifications takes only a few milliseconds, but if you run complex code snippets this will likely trigger some page flickering and impact the user experience
  3. The roundtrip to the Adobe Target backend services to obtain the list of page modifications that need to be applied is done during the eager phase and will impact the overall LCP. The first call to the endpoint is also typically slower, while subsequent calls will be on a warmed-up service with cached responses
  4. Since the instrumentation has an overhead, we recommend only enabling it on selected pages that are meant to be experimented on or personalized. The easiest is to add a page metadata, like Target: on that will act as a feature flag.

In our tests, you can expect a baseline performance impact as below. To this you’d also need to add the overhead of more complex page modifications, especially when using custom JavaScript snippets.

Mobile

Largest Contentful Paint Total Blocking Time PageSpeed
1st call +1.3s 20~40ms 0~5 pts
subsequent calls +0.1s 20~40ms 0~3 pts

Desktop

Largest Contentful Paint Total Blocking Time PageSpeed
1st call +0.5s 0~20ms 0~3 pts
subsequent calls +0.3~0.5s 0~20ms 0~3 pts

Adobe Experience Platform WebSDK

To enable Adobe Target integration in your website using the Adobe Experience Platform WebSDK (aka alloy), please follow these steps in your project:

function initWebSDK(path, config) {
  // Preparing the alloy queue
  if (!window.alloy) {
    // eslint-disable-next-line no-underscore-dangle
    (window.__alloyNS ||= []).push('alloy');
    window.alloy = (...args) => new Promise((resolve, reject) => {
      window.setTimeout(() => {
        window.alloy.q.push([resolve, reject, args]);
      });
    });
    window.alloy.q = [];
  }
  // Loading and configuring the websdk
  return new Promise((resolve) => {
    import(path)
      .then(() => window.alloy('configure', config))
      .then(resolve);
  });
}

function onDecoratedElement(fn) {
  // Apply propositions to all already decorated blocks/sections
  if (document.querySelector('[data-block-status="loaded"],[data-section-status="loaded"]')) {
    fn();
  }

  const observer = new MutationObserver((mutations) => {
    if (mutations.some((m) => m.target.tagName === 'BODY'
      || m.target.dataset.sectionStatus === 'loaded'
      || m.target.dataset.blockStatus === 'loaded')) {
      fn();
    }
  });
  // Watch sections and blocks being decorated async
  observer.observe(document.querySelector('main'), {
    subtree: true,
    attributes: true,
    attributeFilter: ['data-block-status', 'data-section-status'],
  });
  // Watch anything else added to the body
  observer.observe(document.querySelector('body'), { childList: true });
}

function toCssSelector(selector) {
  return selector.replace(/(\.\S+)?:eq\((\d+)\)/g, (_, clss, i) => `:nth-child(${Number(i) + 1}${clss ? ` of ${clss})` : ''}`);
}

async function getElementForProposition(proposition) {
  const selector = proposition.data.prehidingSelector
    || toCssSelector(proposition.data.selector);
  return document.querySelector(selector);
}

async function getAndApplyRenderDecisions() {
  // Get the decisions, but don't render them automatically
  // so we can hook up into the AEM EDS page load sequence
  const response = await window.alloy('sendEvent', { renderDecisions: false });
  const { propositions } = response;
  onDecoratedElement(async () => {
    await window.alloy('applyPropositions', { propositions });
    // keep track of propositions that were applied
    propositions.forEach((p) => {
      p.items = p.items.filter((i) => i.schema !== 'https://ns.adobe.com/personalization/dom-action' || !getElementForProposition(i));
    });
  });

  // Reporting is deferred to avoid long tasks
  window.setTimeout(() => {
    // Report shown decisions
    window.alloy('sendEvent', {
      xdm: {
        eventType: 'decisioning.propositionDisplay',
        _experience: {
          decisioning: { propositions },
        },
      },
    });
  });
}

let alloyLoadedPromise = initWebSDK('./alloy.js', {
    datastreamId: '/* your datastream id here, formally edgeConfigId */',
    orgId: '/* your ims org id here */',
  });;
if (getMetadata('target')) {
  alloyLoadedPromise.then(() => getAndApplyRenderDecisions());
}
if (main) {
    decorateMain(main);
    // wait for alloy to finish loading
    await alloyLoadedPromise;
    // show the LCP block in a dedicated frame to reduce TBT
    await new Promise((res) => {
      window.requestAnimationFrame(async () => {
        await waitForLCP(LCP_BLOCKS);
        res();
      });
    });
  }

Adobe Target at.js (legacy)

To enable Adobe Target integration in your website using the legacy at.js approach, please follow these steps in your project:

function initATJS(path, config) {
  window.targetGlobalSettings = config;
  return new Promise((resolve) => {
    import(path).then(resolve);
  });
}

function onDecoratedElement(fn) {
  // Apply propositions to all already decorated blocks/sections
  if (document.querySelector('[data-block-status="loaded"],[data-section-status="loaded"]')) {
    fn();
  }

  const observer = new MutationObserver((mutations) => {
    if (mutations.some((m) => m.target.tagName === 'BODY'
      || m.target.dataset.sectionStatus === 'loaded'
      || m.target.dataset.blockStatus === 'loaded')) {
      fn();
    }
  });
  // Watch sections and blocks being decorated async
  observer.observe(document.querySelector('main'), {
    subtree: true,
    attributes: true,
    attributeFilter: ['data-block-status', 'data-section-status'],
  });
  // Watch anything else added to the body
  observer.observe(document.querySelector('body'), { childList: true });
}

function toCssSelector(selector) {
  return selector.replace(/(\.\S+)?:eq\((\d+)\)/g, (_, clss, i) => `:nth-child(${Number(i) + 1}${clss ? ` of ${clss})` : ''}`);
}

async function getElementForOffer(offer) {
  const selector = offer.cssSelector || toCssSelector(offer.selector);
  return document.querySelector(selector);
}

async function getElementForMetric(metric) {
  const selector = toCssSelector(metric.selector);
  return document.querySelector(selector);
}

async function getAndApplyOffers() {
  const response = await window.adobe.target.getOffers({ request: { execute: { pageLoad: {} } } });
  const { options = [], metrics = [] } = response.execute.pageLoad;
  onDecoratedElement(() => {
    window.adobe.target.applyOffers({ response });
    // keeping track of offers that were already applied
    options.forEach((o) => o.content = o.content.filter((c) => !getElementForOffer(c)));
    // keeping track of metrics that were already applied
    metrics.map((m, i) => getElementForMetric(m) ? i : -1)
        .filter((i) => i >= 0)
        .reverse()
        .map((i) => metrics.splice(i, 1));
  });
}

let atjsPromise = Promise.resolve();
if (getMetadata('target')) {
  atjsPromise = initATJS('./at.js', {
    clientCode: '/* your client code here */',
    serverDomain: '/* your client code here */.tt.omtrdc.net',
    imsOrgId: '/* your ims org id here */',
    bodyHidingEnabled: false,
    cookieDomain: window.location.hostname,
    pageLoadEnabled: false,
    secureOnly: true,
    viewsEnabled: false,
    withWebGLRenderer: false,
  });
  document.addEventListener('at-library-loaded', () => getAndApplyOffers());
}
if (main) {
    decorateMain(main);
    // wait for atjs to finish loading
    await atjsPromise;
    // show the LCP block in a dedicated frame to reduce TBT
    await new Promise((resolve) => {
      window.requestAnimationFrame(async () => {
        await waitForLCP(LCP_BLOCKS);
        resolve();
      });
    });
  }