Exporting AEM Experience Fragment/Page Content for A/B Testing and External Systems without HTML Tags

Problem Statement:

How to export experience fragment or page content from author to:

  1. Adobe Target or any other application similar to Target without HTML Head/Body tags just component content for A/B Testing or targeting etc.
  2. Salesforce Marketing Cloud or Adobe Campaign system without HTML Head/Body tags
  3. Non-AEM sister sites are to be used as an iframe content.

Introduction:

If you are using an A/B Testing tool or when you are using SFMC for an email campaigning system in your project and you want to export Experience fragment or Page content to the external system using HTTP Post request.

Adding the entire HTML content (which includes the head, body etc.) on the existing page would negatively impact the accessibility score and page performance.

Usually with the above use case people follow exporting HTML content from AEM involves sending an HTTP request to the AEM server and receiving a response that contains the HTML content. This can be achieved using various tools and techniques, but in this article, we will focus on using the SlingRequestProcessor OSGi service.

Advantages of using are:

  1. Process an HTTP request through the Sling request processing engine for example passing any selectors or params or suffixes etc.
  2. Request parameter – Usually a “synthetic” request, i.e., not supplied by the servlet container.
  3. Response parameter – Usually a “synthetic” response, i.e., not supplied by the servlet container.
  4. ResourceResolver parameter – The ResourceResolver is used for the Sling request processing.

Step 1: Add the following maven dependency into your Core POM.XML

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <scope>provided</scope>
</dependency>
Include JSOUP jar from lib in BND plugin

Step 2: Create an Export service as shown below:

package com.chatgpt.core.services;
public interface ExportHtmlService {
    public String getExportHTMLContent(String xfPath);
}

Step 3: Create an Implementation class as shown below:

package com.chatgpt.core.services.impl;

import com.chatgpt.core.services.ExportHtmlService;
import com.day.cq.contentsync.handler.util.RequestResponseFactory;
import com.day.cq.wcm.api.WCMMode;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.engine.SlingRequestProcessor;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@Component(service = ExportHtmlService.class, immediate = true, name = "HTML Content Export service")
public class ExportHtmlServiceImpl implements ExportHtmlService{

    @Reference
    ResourceResolverFactory resolverFactory;

    @Reference
    private SlingRequestProcessor slingProcessor;

    @Reference
    private RequestResponseFactory requestResponseFactory;

    Map<String, Object> wgServiceParam = Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "service-user-name");

    @Override
    public String getExportHTMLContent(String xfPath) {
        if (StringUtils.isNotEmpty(xfPath)) {
            try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(wgServiceParam)) {
                if (StringUtils.isNotEmpty(xfPath)) {
                    retrieveContent(xfPath, resolver);
                    //End point details : you can also use http client builder factory
                    String url = "https://example.com/api";
                    Map<String, String> headers = new HashMap<>();
                    headers.put("Content-Type", "application/json");
                    Document document = Jsoup.parse(retrieveContent(xfPath, resolver));
                    JSONObject jsonObject = new JSONObject();
                    Gson gson = new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
                    jsonObject.put("from", "aem");
                    jsonObject.put("html", gson.toJson(document.body().html()));
                    return sendPost(url, headers, gson.toJson(jsonObject));
                }
            } catch (LoginException | ServletException | IOException | JSONException e) {
                log.error("An error occurred while proccessing the request {} ", e.getMessage());
            }
        }
        return StringUtils.EMPTY;
    }

    protected String retrieveContent(String requestUri, ResourceResolver resourceResolver)
            throws ServletException, IOException {
        HttpServletRequest req = requestResponseFactory.createRequest("GET", requestUri + ".nocloudconfigs.html");
        WCMMode.DISABLED.toRequest(req);
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        HttpServletResponse res = requestResponseFactory.createResponse(out);
        slingProcessor.processRequest(req, res, resourceResolver);
        return out.toString();
    }

    public static String sendPost(String urlString, Map<String, String> headers, String body) throws IOException {
        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);
        conn.setDoInput(true);

        // Set headers
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            conn.setRequestProperty(entry.getKey(), entry.getValue());
        }

        // Write body to output stream
        try (OutputStream os = conn.getOutputStream()) {
            byte[] input = body.getBytes("utf-8");
            os.write(input, 0, input.length);
        }

        // Read response from input stream
        try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"))) {
            StringBuilder response = new StringBuilder();
            String responseLine = null;
            while ((responseLine = br.readLine()) != null) {
                response.append(responseLine.trim());
            }
            return response.toString();
        }
    }
}

As you can see in the above code WCMMode API to set the request mode has been disabled as we are making the request from the author to avoid all the OOTB editor tags.

Here we are using JSOUP to remove unnecessary spaces and new line characters and also, we are using GsonBuilder for escaping double quotes.

The OOTB experience fragment page component comes with a nocloudconfigs custom selector to support Adobe target export functionality, which removes all the unnecessary HTML tags like head, body, etc.

Adobe Target export using nocloudconfigs selector

Step 4: Copy {selector}.html to your custom page component

You can also copy the same HTML files from the experience fragment html tag to your page component if your exporting complete page has iframe content to external systems or sister sites.

Step 5: Copy {selector}.html to your custom component

You can also hide or change component behaviour before exporting by creating {selector}.html (nocloudconfigs.html) inside the component.

Component level selector HTML file

For future enhancements:

Use HTTP Client factory which uses Apache Fluent with optimum performance and OSGi configurable service, please refer to the link for more details:

  1. https://kiransg.com/2021/11/08/aem-rest-service-using-http-client-factory/
  2. https://kiransg.com/2021/11/08/aem-invoke-api-how-to-use-http-client-factory/

You can also enhance special character escaping using the GSON Type registry.

Please refer the below link for working code: https://github.com/kiransg89/chatgpt

2 thoughts on “Exporting AEM Experience Fragment/Page Content for A/B Testing and External Systems without HTML Tags

  1. Hi Kiran,

    We have a integration from AEM to Target through Adobe IO directly.
    We have implemented like below.
    1). Author can login to Production Author
    2). create a experience fragment and export to Adobe Target by clicking on export to Target.
    3). Here there is no authoriser to review the experience fragment.

    Now we have a requirement
    1). Author can login to Production Author
    2). Author should update or create experience Fragment
    3). Author should trigger workflow
    4). Authoriser should review the Experience Fragment
    5). Authoriser should complete the workflow
    6). Experience Fragment should export to Adobe Target internally after complting the workflow.

    Can you please suggest me how to Achieve the new requirement.

    Thanks In Advance.
    Veera

    Like

    1. Hi Veera
      Sorry I am not sure how to achieve above request OOTB can you please reach out to Adobe support for any OOTB workflow which can be triggered

      But I would say enable to the export to Target option once the workflow approved to the user who is trying to export

      Like

Leave a comment