Expand my Community achievements bar.

Don’t miss the AEM Skill Exchange in SF on Nov 14—hear from industry leaders, learn best practices, and enhance your AEM strategy with practical tips.
SOLVED

How to create dynamic pages in AEM?

Avatar

Level 5

Hi, what happens is that I have a servlet that brings information from an endpoint, this is a Json of products, and I already have a component that renders all the products with their values.

 

But what I need to do now as there are many products, is to make pages to each one but dynamically, is there any way to do that without using AIO or CIF?

Topics

Topics help categorize Community content and increase your ability to discover relevant content.

1 Accepted Solution

Avatar

Correct answer by
Level 4

@Dipti_Chauhan suggested good solution. Below you find find example of code to solve a problem:
1. Servlet to Fetch Products and Save as Content Fragments or Resources
This servlet will make an HTTP request to a REST API, fetch product information in JSON format, and save each product as a Content Fragment or a resource with a payloadJSON property. Note: this approach is just an example to store your product info.

 

package com.example.core.servlets;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.http.util.EntityUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import java.io.IOException;
import java.util.Iterator;

@Component(service = Servlet.class,
           property = {
                   "sling.servlet.paths=/bin/fetch-products",
                   "sling.servlet.methods=POST"
           })
@ServiceDescription("Fetch Products and Save as Resources")
public class FetchProductsServlet extends SlingAllMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(FetchProductsServlet.class);

    private static final String PRODUCTS_API_URL = "https://api.example.com/products";
    private static final String RESOURCE_PATH = "/content/project/products/";

    @Reference
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        ResourceResolver resourceResolver = request.getResourceResolver();
        ObjectMapper objectMapper = new ObjectMapper();

        try (CloseableHttpClient httpClient = httpClientBuilderFactory.newBuilder().build()) {
            // Create HTTP GET request
            HttpGet httpGet = new HttpGet(PRODUCTS_API_URL);
            HttpResponse httpResponse = httpClient.execute(httpGet);

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode != 200) {
                log.error("Failed to fetch products, HTTP error code: {}", statusCode);
                response.setStatus(SlingHttpServletResponse.SC_BAD_REQUEST);
                return;
            }

            // Parse JSON response
            HttpEntity entity = httpResponse.getEntity();
            String jsonString = EntityUtils.toString(entity);
            JsonNode products = objectMapper.readTree(jsonString);

            // Iterate over products and save each one
            Iterator<JsonNode> elements = products.elements();
            while (elements.hasNext()) {
                JsonNode product = elements.next();
                String productCode = product.get("code").asText();
                String productJson = objectMapper.writeValueAsString(product);

                // Save product as a resource with payloadJSON property
                Resource productResource = resourceResolver.getResource(RESOURCE_PATH + productCode);
                if (productResource == null) {
                    productResource = ResourceUtil.getOrCreateResource(resourceResolver, RESOURCE_PATH + productCode, (String) null, null, true);
                }

                ModifiableValueMap properties = productResource.adaptTo(ModifiableValueMap.class);
                properties.put("payloadJSON", productJson);

                resourceResolver.commit();
            }

            response.setStatus(SlingHttpServletResponse.SC_OK);
            response.getWriter().write("Products fetched and saved successfully.");
        } catch (Exception e) {
            log.error("Error fetching and saving products", e);
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
}

 

2. Servlet to Handle Product Details Page Requests
This servlet will handle requests to a product details page, extract the product code from the URL, retrieve the corresponding product resource, and set it as a request attribute for further using in Sling models. Example of URL: /content/project/product-details.product.P123.html

 

package com.example.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service = Servlet.class,
           property = {
                   "sling.servlet.resourceTypes=/content/project/product-details",
                   "sling.servlet.extensions=html",
                   "sling.servlet.selectors=product"
           })
@ServiceDescription("Product Details Servlet")
public class ProductDetailsServlet extends SlingSafeMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(ProductDetailsServlet.class);
    private static final String RESOURCE_PATH = "/content/project/products/";

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        String[] selectors = request.getRequestPathInfo().getSelectors();

        if (selectors == null || selectors.length == 0) {
            response.sendError(SlingHttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String productCode = selectors[1];
        String productPath = RESOURCE_PATH + productCode;
        Resource productResource = request.getResourceResolver().getResource(productPath);

        if (productResource == null) {
            response.sendError(SlingHttpServletResponse.SC_NOT_FOUND, "Product not found");
            return;
        }

        request.setAttribute("productResourcePath", productPath);
        // or you can adapt resourc to any Sling Model to to any DTO and pass as a request param
        request.getRequestDispatcher("/content/project/product-details.html").forward(request, response);
    }
}

 

3. Example of Sling Model that shows how to reuse request attribute.

 

@Model(adaptables = Resource.class)
public class ProductModel {

    private static final Logger log = LoggerFactory.getLogger(ProductModel.class);

    @Self
    private ResourceResolver resourceResolver;

    @RequestAttribute
    @Optional
    private String productResourcePath;

    private String title;

    @PostConstruct
    protected void init() {
        if (productResourcePath != null) {
            Resource productResource = resourceResolver.getResource(productResourcePath);
            if (productResource != null) {
                String payloadJSON = productResource.getValueMap().get("payloadJSON", String.class);
                if (payloadJSON != null) {
                    try {
                        ObjectMapper objectMapper = new ObjectMapper();
                        JsonNode jsonNode = objectMapper.readTree(payloadJSON);
                        JsonNode titleNode = jsonNode.get("title");

                        if (titleNode != null) {
                            title = titleNode.asText();
                        } else {
                            log.warn("Title property not found in JSON for resource: {}", productResourcePath);
                        }
                    } catch (Exception e) {
                        log.error("Error parsing JSON for resource: {}", productResourcePath, e);
                    }
                } else {
                    log.warn("No payloadJSON property found for resource: {}", productResourcePath);
                }
            } else {
                log.warn("No resource found at path: {}", productResourcePath);
            }
        } else {
            log.warn("No productResourcePath found in request attributes.");
        }
    }

    public String getTitle() {
        return title;
    }

    public String getResourcePath() {
        return productResourcePath;
    }
}

 

SEO impact

In this case your sitemap will contain only /content/project/product-details.html page. You need to extend sitemap generation to include all product specific URLs (like /content/project/product-details.product.P123.html, /content/project/product-details.product.P124.html) to it. Read more: https://github.com/apache/sling-org-apache-sling-sitemap#readme . However I would suggest to implement vanity urls for product details pages.

 

View solution in original post

7 Replies

Avatar

Level 4

Hi @Aaron_Dempwolff ,

 

I suppose that you store products information somewhere in the repository, but I am all listing possible solutions below. 

 

Based on your requirements I see the following options to solve it:

Option 1: Product as an asset

You can create Content Fragment model that will map product model from the JSON object that you receive. Every time when you receive JSON of products in the servlet, you can create Content Fragment. After creating the CF for product, you can create a page by product template and add CF component to the page with a link to your product CF.

Option 2: Page per each product

Every time when you receive JSON of products in the servlet, you can create page using PageManager API (https://developer.adobe.com/experience-manager/reference-materials/6-5/javadoc/index.html?com/day/cq...). Product JSON you can store either as nodes and properties or as just as a String property. 

 

I would suggest to expose some long-running operations like creating N pages to a Sling Job and don't block servlet thread. The Sling Job guarantees processing: https://sling.apache.org/documentation/bundles/apache-sling-eventing-and-job-handling.html

 

These solutions will work only for new products. However, if you want to create pages for existing products, you can create workflow or Groovy script that will process them in bulk.

 

 

Hello, no, I don't have saved the products in the repository, they are sent to call in a servlet, how can I do to store the result of the products that are called from the servlet?

And no, I don't want to create static pages, but dynamic ones, that is to say the pages that don't exist but are generated at the moment of accessing the url of these pages.

Avatar

Correct answer by
Level 4

@Dipti_Chauhan suggested good solution. Below you find find example of code to solve a problem:
1. Servlet to Fetch Products and Save as Content Fragments or Resources
This servlet will make an HTTP request to a REST API, fetch product information in JSON format, and save each product as a Content Fragment or a resource with a payloadJSON property. Note: this approach is just an example to store your product info.

 

package com.example.core.servlets;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.osgi.services.HttpClientBuilderFactory;
import org.apache.http.util.EntityUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import java.io.IOException;
import java.util.Iterator;

@Component(service = Servlet.class,
           property = {
                   "sling.servlet.paths=/bin/fetch-products",
                   "sling.servlet.methods=POST"
           })
@ServiceDescription("Fetch Products and Save as Resources")
public class FetchProductsServlet extends SlingAllMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(FetchProductsServlet.class);

    private static final String PRODUCTS_API_URL = "https://api.example.com/products";
    private static final String RESOURCE_PATH = "/content/project/products/";

    @Reference
    private HttpClientBuilderFactory httpClientBuilderFactory;

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        ResourceResolver resourceResolver = request.getResourceResolver();
        ObjectMapper objectMapper = new ObjectMapper();

        try (CloseableHttpClient httpClient = httpClientBuilderFactory.newBuilder().build()) {
            // Create HTTP GET request
            HttpGet httpGet = new HttpGet(PRODUCTS_API_URL);
            HttpResponse httpResponse = httpClient.execute(httpGet);

            int statusCode = httpResponse.getStatusLine().getStatusCode();
            if (statusCode != 200) {
                log.error("Failed to fetch products, HTTP error code: {}", statusCode);
                response.setStatus(SlingHttpServletResponse.SC_BAD_REQUEST);
                return;
            }

            // Parse JSON response
            HttpEntity entity = httpResponse.getEntity();
            String jsonString = EntityUtils.toString(entity);
            JsonNode products = objectMapper.readTree(jsonString);

            // Iterate over products and save each one
            Iterator<JsonNode> elements = products.elements();
            while (elements.hasNext()) {
                JsonNode product = elements.next();
                String productCode = product.get("code").asText();
                String productJson = objectMapper.writeValueAsString(product);

                // Save product as a resource with payloadJSON property
                Resource productResource = resourceResolver.getResource(RESOURCE_PATH + productCode);
                if (productResource == null) {
                    productResource = ResourceUtil.getOrCreateResource(resourceResolver, RESOURCE_PATH + productCode, (String) null, null, true);
                }

                ModifiableValueMap properties = productResource.adaptTo(ModifiableValueMap.class);
                properties.put("payloadJSON", productJson);

                resourceResolver.commit();
            }

            response.setStatus(SlingHttpServletResponse.SC_OK);
            response.getWriter().write("Products fetched and saved successfully.");
        } catch (Exception e) {
            log.error("Error fetching and saving products", e);
            response.setStatus(SlingHttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
}

 

2. Servlet to Handle Product Details Page Requests
This servlet will handle requests to a product details page, extract the product code from the URL, retrieve the corresponding product resource, and set it as a request attribute for further using in Sling models. Example of URL: /content/project/product-details.product.P123.html

 

package com.example.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.propertytypes.ServiceDescription;
import org.osgi.service.component.propertytypes.ServiceVendor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(service = Servlet.class,
           property = {
                   "sling.servlet.resourceTypes=/content/project/product-details",
                   "sling.servlet.extensions=html",
                   "sling.servlet.selectors=product"
           })
@ServiceDescription("Product Details Servlet")
public class ProductDetailsServlet extends SlingSafeMethodsServlet {

    private static final Logger log = LoggerFactory.getLogger(ProductDetailsServlet.class);
    private static final String RESOURCE_PATH = "/content/project/products/";

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
        String[] selectors = request.getRequestPathInfo().getSelectors();

        if (selectors == null || selectors.length == 0) {
            response.sendError(SlingHttpServletResponse.SC_NOT_FOUND);
            return;
        }

        String productCode = selectors[1];
        String productPath = RESOURCE_PATH + productCode;
        Resource productResource = request.getResourceResolver().getResource(productPath);

        if (productResource == null) {
            response.sendError(SlingHttpServletResponse.SC_NOT_FOUND, "Product not found");
            return;
        }

        request.setAttribute("productResourcePath", productPath);
        // or you can adapt resourc to any Sling Model to to any DTO and pass as a request param
        request.getRequestDispatcher("/content/project/product-details.html").forward(request, response);
    }
}

 

3. Example of Sling Model that shows how to reuse request attribute.

 

@Model(adaptables = Resource.class)
public class ProductModel {

    private static final Logger log = LoggerFactory.getLogger(ProductModel.class);

    @Self
    private ResourceResolver resourceResolver;

    @RequestAttribute
    @Optional
    private String productResourcePath;

    private String title;

    @PostConstruct
    protected void init() {
        if (productResourcePath != null) {
            Resource productResource = resourceResolver.getResource(productResourcePath);
            if (productResource != null) {
                String payloadJSON = productResource.getValueMap().get("payloadJSON", String.class);
                if (payloadJSON != null) {
                    try {
                        ObjectMapper objectMapper = new ObjectMapper();
                        JsonNode jsonNode = objectMapper.readTree(payloadJSON);
                        JsonNode titleNode = jsonNode.get("title");

                        if (titleNode != null) {
                            title = titleNode.asText();
                        } else {
                            log.warn("Title property not found in JSON for resource: {}", productResourcePath);
                        }
                    } catch (Exception e) {
                        log.error("Error parsing JSON for resource: {}", productResourcePath, e);
                    }
                } else {
                    log.warn("No payloadJSON property found for resource: {}", productResourcePath);
                }
            } else {
                log.warn("No resource found at path: {}", productResourcePath);
            }
        } else {
            log.warn("No productResourcePath found in request attributes.");
        }
    }

    public String getTitle() {
        return title;
    }

    public String getResourcePath() {
        return productResourcePath;
    }
}

 

SEO impact

In this case your sitemap will contain only /content/project/product-details.html page. You need to extend sitemap generation to include all product specific URLs (like /content/project/product-details.product.P123.html, /content/project/product-details.product.P124.html) to it. Read more: https://github.com/apache/sling-org-apache-sling-sitemap#readme . However I would suggest to implement vanity urls for product details pages.

 

Avatar

Community Advisor

@Aaron_Dempwolff why do you want to create pages for each product? Will it be possible to explore how OOTB CIF component architecture follows? Create single, generic page in AEM and use same page with a selector to identify productid, serve content based on your product information.

 

<domain>/us/en/category/products/product.12345.html mapped to /content/yourproject/us/en/category/products/product.html and page components and content components on the page rely on selector and retrieve product information.

 

Also importing large-scale products to AEM is not an ideal and recommended solution that is one of reasons why Adobe moved away from Hybris Product importer concept to Dynamic CIF framework which pulls same product info in your component logic from your backend system be it PIM, Ecommerce..

Avatar

Level 5

Hello, I don't want to create static pages, but dynamic ones, that is to say the pages that don't exist but are generated at the moment of accessing the url of these pages.

Avatar

Community Advisor

HI @Aaron_Dempwolff 

yes, you can create dynamic pages if you dont have any authoring content. Your the URL structure may look like "abc.[PRODUCT ID].html". This product id as selector approach will allow you to cache the content effectively, as the URL will be distinct for each page. But in repository you only need to create one page "abc".

Furthermore, by using the ID as a selector, you will be able to fetch the relevant data for each dynamic page. 

However, it's important to note that with this approach, you will need to consider the SEO implications separately. 

Avatar

Level 5

Do you have a more in depth guide or tutorial on how to do this?