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:
- 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
- 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
- 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
- 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:
- Start by following the steps to Use Adobe Target and Web SDK for personalization, and skip all steps related to actual instrumentation at the code level
- Make sure to note down the Adobe IMS Organization Identifier (
orgId
) as well as the Adobe Experience Platform Datastream Id (datastreamId
, formallyedgeConfigId
) you want to use. You can get those following the WebSDK configuration documentation (orgId, edgeConfigId). - In your GitHub repository for the website, add the
alloy.js
file. - Then edit your
scripts.js
file and add the following code somewhere above theloadEager
method definition:
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());
}
- Adjust the path to the library and set the correct values for your
datastreamId
, formallyedgeConfigId
, andorgId
as per step 2. - Then edit the
loadEager
method to:
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();
});
});
}
- Commit and push your code
- Setup up an experiment in Adobe Target and preview the page
- Add the
Target
metadata property to your page to trigger the instrumentation, or adjust thegetMetadata
condition in the code above to your needs. You can typically importgetMetadata
fromaem.js
or equivalent in your project if it isn’t yet available in yourscripts.js
- If the instrumentation is properly done, you should see a call to
https://edge.adobedc.net/ee/v1/interact
in your browser’s Network tab when you load the page. Whether the page is actually modified or not will depend on the configuration you set in Adobe Target - You are all done!
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:
- Start by reading the Adobe Target at.js implementation documentation, and skip all steps related to actual instrumentation at the code level
- Go to https://experience.adobe.com/#/target/setup/implementation and note down your Client Code and IMS Organization Id
- In your GitHub repository for the website, add the
at.js
file. We have an optimized version for AEM Edge Delivery Service that you can fetch athttps://atjs--wknd--hlxsites.hlx.live/scripts/at.fix.min.js
until it is made publicly available - Then Edit your
scripts.js
file and add the following code somewhere above theloadEager
method definition:
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());
}
- Adjust the path to the library and set the correct values for the
clientCode
andimsOrgId
as per step 2, and edit theserverDomain
so the first part matches your client code. - Then edit the
loadEager
method to:
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();
});
});
}
- Commit your code
- Setup up an experiment in Adobe Target and preview the page
- Add the
Target
metadata property to your page to trigger the instrumentation, or adjust thegetMetadata
condition in the code above to your needs. You can typically importgetMetadata
fromaem.js
or equivalent in your project if it isn’t yet available in yourscripts.js
- If the instrumentation is properly done, you should see a call to
https://<client-code>.tt.omtrdc.net/rest/v1/delivery
in your browser’s Network tab when you load the page. Whether the page is actually modified or not will depend on the configuration you set in Adobe Target - You are all done!