AEM Cloud Service - Create Adobe Target Offers as AEM Experience Fragments for Non AEM Sites

Goal

Adobe Experience Manager 2021.6.5586.20210628T210726Z-210600 (June 28, 2021)

Use AEM as Offer Server where the offers (with text and images) are created by marketing team as XFs (Experience Fragments) in Authoring and Published to AEM Publish. XF editor provides an easy to use interface for creating html fragments. For personalizing the offers we use Adobe Target Activities and XF Offers. Finally render the Personalized offer in a Third-party website (not running in AEM Publish). The offer html and necessary images are accessed by the third party website from AEM Publish

Demo | Package Install | Github



Setup IMS in AEM for Target Integration

                    https://experienceleague.adobe.com/docs/experience-manager-65/administering/integration/integration-ims-adobe-io.html

                    Tools > Security > Adobe IMS Configurations


                    1. Create a Certificate in AEM...




                    2. Upload Certificate in https://console.adobe.io




                    3. Continue the IMS integration in AEM...



 


                    4. Provide the necessary permissions for Integration in Admin Console. Without this you might see the following error when exporting XFs to Target...

Caused by: com.day.cq.analytics.testandtarget.impl.service.WebServiceException: Unexpected response status code [403] for request [https://mc.adobe.io/ags959/target/offers/content?includeMarketingCloudMetadata=true].{"httpStatus":403,"requestId":"SKeC0FKYUiWHkDxi2D47KS33xMkRKpdy","requestTime":"2021-06-15T17:12:51.320578Z","errors":[{"errorCode":"Forbidden.Resource","message":"Access denied. To perform this operation, all of the following privileges are required \"[editor]\".","meta":{}}]}at com.day.cq.analytics.testandtarget.impl.service.WebServiceImpl.request(WebServiceImpl.java:610) [com.adobe.cq.cq-target-integration:1.4.30]... 163 common frames omitted




Create Target Integration using IMS

                    Tools > Cloud Services > Legacy Cloud Services > Adobe Target



Create XF Components in AEM

1) Create a Text component in AEM extending Core Text Component (sling:resourceSuperType="core/wcm/components/text/v2/text")

2) Create an Image component in AEM extending Core Image Component (sling:resourceSuperType="core/wcm/components/image/v2/image"), add the following render script in /apps/eaem-cs-at-json-offer/components/image/image.html

<div data-sly-use.image="apps.experienceaem.assets.core.filters.ImageModel"
     data-sly-test="${image.src}"
     style="text-align:center">
        <img src="${image.src}" style="margin-left: auto; margin-right: auto;"/>
</div>

<div    data-sly-use.image="apps.experienceaem.assets.core.filters.ImageModel"
        style="text-align: center; padding: 30px"
        data-sly-test="${!image.src && wcmmode.edit}">
    Image not set
</div>


3) Create the model apps.experienceaem.assets.core.filters.ImageModel with following code for the Image component render script (this is for generating the full image link using Externalizer)

package apps.experienceaem.assets.core.filters;

import com.day.cq.commons.Externalizer;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Optional;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

@Model(
        adaptables = {SlingHttpServletRequest.class}
)
public class ImageModel {
    private static Logger log = LoggerFactory.getLogger(ImageModel.class);

    @Inject
    SlingHttpServletRequest request;

    @Inject
    private SlingSettingsService slingSettingsService;

    @ValueMapValue
    @Optional
    private String fileReference;

    private String src = "";

    @PostConstruct
    protected void init() {
        if(StringUtils.isEmpty(fileReference)){
            return;
        }

        SlingHttpServletRequest slingRequest = (SlingHttpServletRequest)request;
        ResourceResolver resolver = slingRequest.getResourceResolver();

        Externalizer externalizer = resolver.adaptTo(Externalizer.class);
        boolean isAuthor = slingSettingsService.getRunModes().contains(Externalizer.AUTHOR);

        src = !isAuthor ? externalizer.publishLink(resolver, fileReference) : externalizer.authorLink(resolver, fileReference);
    }

    public String getSrc(){
        return src;
    }
}


4) Use the Web Variation template /conf/eaem-cs-at-json-offer/settings/wcm/templates/xf-web-variation created by maven archetype command...

mvn -B archetype:generate -D archetypeGroupId=com.adobe.aem -D archetypeArtifactId=aem-project-archetype -D archetypeVersion=24 
-D aemVersion=cloud -D appTitle="Experience AEM Target JSON Offer" -D appId="eaem-cs-at-json-offer" -D groupId="apps.experienceaem.assets" 
-D frontendModule=none -D includeExamples=n -D includeDispatcherConfig=y


5) Create a filter apps.experienceaem.assets.core.filters.ExperienceFragmentJSONOfferFilter with the following code to modify the product model.json response, so it just provides the publish url of XF...

eg. https://publish-p10961-e90064.adobeaemcloud.com/content/experience-fragments/eaem-cs-at-json-offer/us/en/site/eaem-cs-three/master.model.json

{

}

package apps.experienceaem.assets.core.filters;

import com.day.cq.commons.Externalizer;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.commons.json.JSONObject;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import java.io.IOException;

@Component(
        service = Filter.class,
        immediate = true,
        name = "Experience AEM - Change offer JSON exported to Target",
        property = {
                Constants.SERVICE_RANKING + ":Integer=-99",
                "sling.filter.scope=COMPONENT",
                "sling.filter.pattern=(/content/experience-fragments/.*.model.json)",
        }
)
public class ExperienceFragmentJSONOfferFilter implements Filter {
    private static final Logger log = LoggerFactory.getLogger(ExperienceFragmentJSONOfferFilter.class);

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;

        try {
            String uri = slingRequest.getRequestURI();

            if(!uri.endsWith(".model.json")){
                chain.doFilter(request, response);
                return;
            }

            JSONObject model = new JSONObject();

            String masterXFPath = uri.substring(0,uri.lastIndexOf(".model.json"));

            masterXFPath = masterXFPath + ".html";

            ResourceResolver resolver = slingRequest.getResourceResolver();
            Externalizer externalizer = resolver.adaptTo(Externalizer.class);

            model.put("xfHtmlPath", externalizer.publishLink(resolver, masterXFPath));

            response.getWriter().print(model);
        } catch (Exception e) {
            log.error("Error getting json offer response : " + slingRequest.getRequestURI());
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}


6) Set the Adobe Target configuration and Export Format on the XF folder eg. https://author-p10961-e90064.adobeaemcloud.com/mnt/overlay/cq/experience-fragments/content/experience-fragments/folderproperties.html/content/experience-fragments/eaem-cs-at-json-offer/us/en/site



CORS and Dispatcher Changes

1) Browsers do not allow cross origin requests unless the Access-Control-Allow-Origin header is present in response headers. So for the third party website (for this post its a local file C:\.....\eaem-cloud-service\eaem-cs-at-json-offer\scripts\show-xf-offer\show-aem-xf-json-offer.html) to make a CORS call to AEM publish (eg. https://publish-p10961-e90064.adobeaemcloud.com) for loading the XF offer html, some configuration changes are required. For more info check adobe documentation. When Access-Control-Allow-Origin header is NOT present in the response you might see the following error...

Access to XMLHttpRequest at 'http://localhost:8080/content/experience-fragments/eaem-cs-at-json-offer/us/en/site/test/master.html' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.


2) Add the CORS configuration file ui.config\src\main\content\jcr_root\apps\eaem-cs-at-json-offer\osgiconfig\config\com.adobe.granite.cors.impl.CORSPolicyImpl~eaem-cs-at-json-offer.cfg.json with following settings (adding it in config.publish folder should be fine if the XF html is delivered only by publish instance or just adding the header it Dispatcher, discussed in next section, should be fine... however during debugging when Dispatcher has to be bypassed you might find the need for this configuration in AEM Author and Publish instances)

{
  "supportscredentials": false,
  "exposedheaders": [
    "*"
  ],
  "supportedmethods": [
    "GET",
    "HEAD",
    "POST"
  ],
  "alloworigin": [
    ""
  ],
  "maxage:Integer": 1800,
  "alloworiginregexp": [
    ".*"
  ],
  "allowedpaths": [
    ".*"
  ],
  "supportedheaders": [
    "Origin",
    "Accept",
    "X-Requested-With",
    "Content-Type",
    "Access-Control-Request-Method",
    "Access-Control-Request-Headers",
    "Authorization"
  ]
}


3) In the above step alloworiginregexp was set to ".*". This will set the access-control-allow-origin to wildcard * in response headers allowing any cross origin request (in real scenarios you may want to set it to the third party website domain name). If your third party website is a local file, chrome sends null origin header so setting alloworigin to null or alloworiginregexp to .* works...




4) The next step is configuring Dispatcher to allow this header in response (Publish instance sends the header but unless cleared via configuration, Dispatcher blocks it)


5) Create a copy of eaem-cs-at-json-offer\dispatcher\src\conf.d\available_vhosts\default.vhost and name it eaem-cs-at-json-offer\dispatcher\src\conf.d\available_vhosts\eaem.vhost. Add the access control allow origin header configuration...

               <IfModule mod_headers.c>
Header add X-Vhost "publish"
Header add Access-Control-Allow-Origin "*"
</IfModule>


6) For caching the Access-Control-Allow-Origin response header, create a copy of eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\available_farms\default.farm and name it eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\available_farms\eaem.farm. Add the access control allow origin header cache configuration...

              /headers {
"Cache-Control"
"Content-Disposition"
"Content-Type"
"Expires"
"Last-Modified"
"X-Content-Type-Options"
"Access-Control-Allow-Origin"
}


7) You need to create SYMLINK eaem-cs-at-json-offer\dispatcher\src\conf.d\enabled_vhosts\eaem.vhost and eaem-cs-at-json-offer\dispatcher\src\conf.dispatcher.d\enabled_farms\eaem.farm pointing to the respective eaem.vhost and eaem.farms created in steps above. This is tricky in Windows OS, use command prompt, run as administrator and create them using following commands... (for more details on various ways to create the symlinks check this post)

> git config --global core.symlinks true

> cd C:\dev\projects\eaem-cloud-service\code\dispatcher\src\conf.d\enabled_vhosts

> mklink  eaem.vhost "../available_vhosts/eaem.vhost"

> cd C:\dev\projects\eaem-cloud-service\code\dispatcher\src\conf.dispatcher.d\enabled_farms

> mklink  eaem.farm "../available_farms/eaem.farm"




8) You can optionally test these dispatcher changes using the dispatcher tools locally (check documentation). Testing your dispatcher changes locally first is productive, as the CS build pipelines take time and any errors in dispatcher config fail the pipeline. For checking cached files, open the container terminal (Open in Terminal and folder /mnt/var/www/html) in docker desktop, also on windows, you can find the cached files in \\wsl$\docker-desktop-data\version-pack-data\community\docker\overlay2\366651e523edf46526559bd894ce73e3aa9146158f8efd48bcfcab88d5f3dee6\diff\mnt\var\www

9) After unzipping the Dispatcher tools eg. aem-sdk-dispatcher-tools-x.x.x-windows.zip , run the following commands to validate dispatcher config changes and test it using a Docker container (assuming you have Docker Desktop installed and the Publish instance is started on 4503)

> dispatcher-tools\bin\validator full -f -d out src

> dispatcher-tools\bin\docker_run out host.docker.internal:4503 8080


10) If everything goes well the XF is available for viewing on...





11) Sometimes when starting Docker you might see the following error. To workaround open Windows Services (Run > services.msc) and restart the service named Hyper-V Virtual Machine Management, restart Docker...

System.InvalidOperationException: Failed to deploy distro docker-desktop to C:\Users\nalabotu\AppData\Local\Docker\wsl\distro: exit code: -1  stdout: Logon failure: the user has not been granted the requested logon type at this computer.




12) For testing CORS and loading the page directly from AEM cloud services Publish, you can use the following code...

<div id="aem-offer" style="text-align: center; margin-top: 30px">
    Default content before HTML; Loading the XF offer...
</div>

<script>
    function loadXF() {
        var $aemOffer = $("#aem-offer");

        alert("loading directly from publish....")

        $.ajax("https://publish-p10961-e90064.adobeaemcloud.com/content/experience-fragments/eaem-cs-at-json-offer/us/en/site/eaem-cs-one/master.html").done(function (html) {
            $aemOffer.html(html);
        }).fail(function () {
            $aemOffer.html("Error loading Publish CS offer");
        });
    }

    loadXF();
</script>


Exporting to Adobe Target

1) AEM Cloud services is all setup up with the XF related code and configuration. Lets export the created XF to Target...


2) Login to https://experience.adobe.com using your Adobe ID (make sure you are in the right org) and click on Target icon...


3) Click on Offers icon and you should see the exported offers from AEM...



Create Target Activities 

1) Lets create a personalized A/B Test Activity to use the XF Offers exported from AEM...



2) Select the XF offer for Experience A. The mbox name here can be any eg. eaem-cs-test-3 and  the same name is used when loading the activity in third party website. Only the AEM Publish url of XF offer is exported from AEM, as you can see in the screenshot below...



3) Select the XF offer for Experience B



4) Personalization used here is a random 50:50 delivery of the experiences A and B...



5) Finish the Activity creation by setting the conversion goal as Viewed an mbox


6) Make the Activity Live...



Offer loading Script in Third Party

1) For this post we are using a local file (C:\.....\eaem-cloud-service\eaem-cs-at-json-offer\scripts\show-xf-offer\show-aem-xf-json-offer.html) as the third-party website showing XF offers. Integrating Target with Launch is the right way to do it (check documentation) however to keep things simple download the Target lib file at.js from your Adobe Target account (check documentation)



2) Add at.js and jquery-3.6.0.min.js in your thirdparty script to make a connection to target, get the personalized offer JSON with AEM Publish url and further get the html content of the XF from AEM Publish to show in a div...

<!--https://experienceleague.adobe.com/docs/target/using/implement-target/client-side/at-js-implementation/deploy-at-js/implementing-target-without-a-tag-manager.html?lang=en#task_E85D2F64FEB84201A594F2288FABF053-->
<!doctype html>
<html>

<head>
    <meta charset="utf-8">
    <title>Experience AEM XF Target Offer</title>

    <!--Preconnect and DNS-Prefetch to improve page load time-->
    <link rel="preconnect" href="//ags959.tt.omtrdc.net?lang=en">
    <link rel="dns-prefetch" href="//ags959.tt.omtrdc.net?lang=en">
    <!--/Preconnect and DNS-Prefetch-->

    <!--jQuery or other helper libraries should be implemented before at.js if you would like to use their methods in Target-->
    <script src="./js/jquery-3.6.0.min.js"></script>
    <!--/jQuery-->

    <!--Target's JavaScript SDK, at.js-->
    <script src="./js/at.js"></script>
    <!--/at.js-->
</head>

<body>
<div style="text-align: center; margin-top: 20px">
    This pages loads AEM Experience Fragment Offers (authored as JSON and rendered as HTML)
</div>

<div id="aem-offer" style="text-align: center; margin-top: 30px">
    Default content before HTML; Loading the XF offer...
</div>

<script>
    function loadOffer() {
        var $aemOffer = $("#aem-offer");

        adobe.target.getOffer({
            mbox: 'eaem-cs-test-3',
            success: function (offer) {
                var offerJSON = JSON.parse(offer[0].content);

                if (!offerJSON.xfHtmlPath) {
                    $aemOffer.html("Error loading offer");
                    return;
                }

                $.ajax(offerJSON.xfHtmlPath).done(function (html) {
                    $aemOffer.html(html);
                });

            }, error: function () {
                $aemOffer.html("Target Error loading offer");
            }
        })
    }

    setTimeout(loadOffer, 3000);
</script>
</body>

</html>


3) The offer rendered in a thirdparty page (chrome does not allow storing cookies needed for mbox when loaded via local file, so this was tested using firefox...)



No comments:

Post a Comment