Skip to main content

Adobe

Create an RSS Feed using HTL

Istock 1317277259

Did you know you can create an RSS feed in AEM (Adobe Experience Manager) for external applications like Eloqua? While AEM provides out-of-the-box functionality for RSS feeds, customizing them may require additional steps. Below you’ll find several options for creating RSS feeds in AEM along with steps for creating one using HTL.  

3 Options to Create an RSS Feed in AEM  

  1. Override Default JSP Functionality (JSP Approach) 
    • Customize the JSP code to tailor the RSS feed according to your requirements 
    • This approach requires writing backend logic in Java and JSP
  2. Create a Servlet for RSS Feed
    • Implement the logic within the servlet to fetch and format the necessary data into RSS feed XML
    • Configure the servlet to respond to specific requests for the RSS feed endpoint
    • This approach allows more control and flexibility over the RSS feed generation process
  3. Use HTL with Sling Model (HTL Approach)
    • Write HTL templates combined with a Sling Model to generate the RSS feed
    • Leverage Sling Models to retrieve data from AEM and format it within the HTL template
    • This approach utilizes AEM’s modern templating language and component models
    • HTL is preferred for templating tasks due to its simplicity and readability

Expected RSS Feed 

Below is the feed response for an external source to integrate and send emails accordingly. Here the feed results can be filtered by category tag names (category) using query parameters in the feed URL. 

  • https://www.demoproject.com/products/aem.rss 
  • https://www.demoproject.com/products/aem.rss?category=web
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <atom:link rel="self" href="https://www.demoproject.com/products/aem" />
        <link>https://www.demoproject.com/products/aem</link>
        <title>AEM</title>
        <description />
        <pubDate>Fri, 29 Sep 2023 02:08:26 +0000</pubDate>
        <item>
            <guid>https://www.demoproject.com/products/aem/one.rss.xml</guid>
            <atom:link rel="self" href="https://www.demoproject.com/products/aem/sites" />
            <link>https://www.demoproject.com/products/aem/sites</link>
            <title>Aem Sites</title>
            <description><![CDATA[AEM Sites is the content management system within Adobe Experience Manager that gives you one place to create, manage and deliver remarkable digital experiences across websites, mobile sites and on-site screens.]]></description>
            <pubDate>Tue, 31 Oct 2023 02:23:04 +0000</pubDate>
        </item>
        <item>
            <guid>https://www.demoproject.com/products/aem/two.rss.xml</guid>
            <atom:link rel="self" href="https://www.demoproject.com/products/aem/assets" />
            <link>https://www.demoproject.com/products/aem/assets</link>
            <title>Aem Assets</title>
            <description><![CDATA[Adobe Experience Manager (AEM) Assets is a digital asset management system (DAM) that is built into AEM. It stores and delivers a variety of assets (including images, videos, and documents) with their connected metadata in one secure location.]]></description>
            <pubDate>Thu, 26 Oct 2023 02:21:19 +0000</pubDate>
            <category>pdf,doc,image,web</category>
        </item>
    </channel>
</rss>

Steps for Creating RSS Feed Using HTL 

  • Create a HTML file under the page component 
  • Create a PageFeed Sling Model that returns data for the RSS feed 
  • Add a rewrite rule in the dispatcher rewrite rules file 
  • Update the ignoreUrlParams for the required params 

Page Component – RSS html  

Create an HTML file with the name “rss.xml.html” under page component. Both ‘rss.html’ or ‘rss.xml.html’ work fine for this. Here, ‘rss.xml.html’ naming convention indicates that it is generating XML data. PageFeedModel provides the page JSON data for the expected feed.  

  • Category tag is rendered only when page properties are authored with tag values
  • CDATA (character data) is a section of element content to render as only character data instead of markup
<?xml version="1.0" encoding="UTF-8"?>
<sly data-sly-use.model="com.demoproject.aem.core.models.PageFeedModel" />
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <atom:link rel="self" href="${model.link}"/>
        ${model.linkTag @ context='unsafe'}
        <title>${model.title}</title>
        <description>${model.subTitle}</description>
        <pubDate>${model.publishedDate}</pubDate>
        <sly data-sly-list.childPage="${model.entries}">
            <item>
                <guid>${childPage.feedUrl}</guid>
                <atom:link rel="self" href="${childPage.link}"/>
                ${childPage.linkTag @ context='unsafe'}
                <title>${childPage.title}</title>
                <description><![CDATA[${childPage.description}]]></description>
                <pubDate>${childPage.publishedDate}</pubDate>
                <sly data-sly-test="${childPage.tags}">
                    <category>${childPage.tags}</category>
                </sly>
            </item>
        </sly>
    </channel>
</rss>  

Page Feed Model

This is a component model that takes the currentPage as the root and retrieves a list of its child pages. Subsequently, it dynamically constructs properties such as publish date and categories based on the page’s tag field. These properties enable filtering of results based on query parameters. Once implemented, you can seamlessly integrate this model into your component to render the RSS feed.

  • Using currentPage get the current page properties as a value map 
  • Retrieve title, description, pubDate, link for current page 
  • Retrieve title, description, pubDate, link, tags (categories) for child pages 
  • Filter the child pages list based on the query param value (category)
//PageFeedModel sample code 
package com.demoproject.aem.core.models;

import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageFilter;
import com.demoproject.aem.core.utility.RssFeedUtils;
import lombok.Getter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.PostConstruct;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

@Model(adaptables = {
    Resource.class,
    SlingHttpServletRequest.class
}, resourceType = PageFeedModel.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class PageFeedModel {

    protected static final String RESOURCE_TYPE = "demoproject/components/page";
    private static final Logger logger = LoggerFactory.getLogger(PageFeedModel.class);
    @SlingObject
    ResourceResolver resourceResolver;
    @SlingObject
    SlingHttpServletRequest request;
    @Inject
    private Page currentPage;
    @Getter
    private String title;
    @Getter
    private String link;
    @Getter
    private String linkTag;
    @Getter
    private String description;
    @Getter
    private List < ChildPageModel > entries;
    @Inject
    private Externalizer externalizer;
    @Getter
    private String feedUrl;
    @Getter
    private String publishedDate;


    @PostConstruct
    protected void init() {
        try {
            ValueMap properties = currentPage.getContentResource().adaptTo(ValueMap.class);
            title = StringEscapeUtils.escapeXml(null != currentPage.getTitle() ? currentPage.getTitle() : properties.get(JcrConstants.JCR_TITLE, String.class));
            description = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_DESCRIPTION, String.class));

            link = RssFeedUtils.getExternaliseUrl(currentPage.getPath(), externalizer, resourceResolver);
            feedUrl = link + ".rss.xml";
            linkTag = RssFeedUtils.setLinkElements(link);

            String category = request.getParameter("category") != null ? request.getParameter("category").toLowerCase().replaceAll("\\s", "") : StringUtils.EMPTY;
            entries = new ArrayList < > ();
            Iterator < Page > childPages = currentPage.listChildren(new PageFilter(false, false));
            while (childPages.hasNext()) {
                Page childPage = childPages.next();
                ChildPageModel childPageModel = resourceResolver.getResource(childPage.getPath()).adaptTo(ChildPageModel.class);
                if (null != childPageModel) {
                    if (StringUtils.isBlank(category)) entries.add(childPageModel);
                    else {
                        String tags = childPageModel.getTags();
                        if (StringUtils.isNotBlank(tags)) {
                            tags = tags.toLowerCase().replaceAll("\\s", "");
                            List tagsList = Arrays.asList(tags.split(","));
                            String[] categoryList = category.split(",");
                            boolean flag = true;
                            for (String categoryStr: categoryList) {
                                if (tagsList.contains(StringEscapeUtils.escapeXml(categoryStr)) && flag) {
                                    entries.add(childPageModel);
                                    flag = false;
                                }
                            }
                        }
                    }
                }
            }
            publishedDate = RssFeedUtils.getPublishedDate(properties);

        } catch (SlingException e) {
            logger.error("Repository Exception {}", e);
        }
    }
}
//ChildPageModel 
package com.demoproject.aem.core.models;

import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.demoproject.aem.core.utility.RssFeedUtils;
import lombok.Getter;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

@Model(adaptables = {
    Resource.class
}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ChildPageModel {
    private static final Logger logger = LoggerFactory.getLogger(ChildPageModel.class);

    @SlingObject
    Resource resource;

    @Getter
    private String title;

    @Getter
    private String link;

    @Getter
    private String linkTag;

    @Getter
    private String feedUrl;

    @Getter
    private String description;

    @Getter
    private String publishedDate;

    @Getter
    private String tags;

    @Inject
    private Externalizer externalizer;

    @PostConstruct
    protected void init() {
        try {
            if (null != resource) {
                String url = resource.getPath();

                ResourceResolver resourceResolver = resource.getResourceResolver();
                link = RssFeedUtils.getExternaliseUrl(url, externalizer, resourceResolver);
                feedUrl = link + ".rss.xml";
                linkTag = RssFeedUtils.setLinkElements(link);

                ValueMap properties = resource.getChild(JcrConstants.JCR_CONTENT).adaptTo(ValueMap.class);
                title = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_TITLE, String.class));
                description = StringEscapeUtils.escapeXml(properties.get(JcrConstants.JCR_DESCRIPTION, String.class));
                publishedDate = RssFeedUtils.getPublishedDate(properties);
                tags = StringEscapeUtils.escapeXml(RssFeedUtils.getPageTags(properties, resourceResolver));

            }
        } catch (SlingException e) {
            logger.error("Error: " + e.getMessage());
        }
    }
}
//RSS Feed Utils 

package com.demoproject.aem.core.utility;

import com.day.cq.commons.Externalizer;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.tagging.Tag;
import com.day.cq.tagging.TagManager;
import com.day.cq.wcm.api.NameConstants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/** 
 * @desc RSS Feed Utils 
 */
@Slf4j
public class RssFeedUtils {

    public static final String FORMAT_DATE = "E, dd MMM yyyy hh:mm:ss Z";
    public static final String CONTENT_PATH = "/content/demoproject/us/en";

    public static String getPublishedDate(ValueMap pageProperties) {
        String publishedDate = StringUtils.EMPTY;
        SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_DATE);
        Date updatedDateVal = pageProperties.get(JcrConstants.JCR_LASTMODIFIED, pageProperties.get(JcrConstants.JCR_CREATED, Date.class));
        if (null != updatedDateVal) {
            Date replicatedDate = pageProperties.get(NameConstants.PN_PAGE_LAST_REPLICATED, updatedDateVal);
            publishedDate = dateFormat.format(replicatedDate);
        }
        return publishedDate;
    }

    public static String getExternaliseUrl(String pagePath, Externalizer externalizer, ResourceResolver resourceResolver) {
        String url = StringUtils.EMPTY;
        if (StringUtils.isNotBlank(pagePath) && null != externalizer && null != resourceResolver)
            url = externalizer.publishLink(resourceResolver, resourceResolver.map(pagePath)).replace(CONTENT_PATH, "");

        return url;
    }

    public static String setLinkElements(String link) {
        String url = StringUtils.EMPTY;
        if (StringUtils.isNotBlank(link)) {
            url = "<link>" + link + "</link>";
        }
        return url;
    }

    public static String getPageTags(ValueMap properties, ResourceResolver resourceResolver) {
        String tags = StringUtils.EMPTY;
        String[] pageTags = properties.get(NameConstants.PN_TAGS, String[].class);
        if (pageTags != null) {
            List < String > tagList = new ArrayList < > ();
            TagManager tagManager = resourceResolver.adaptTo(TagManager.class);
            for (String tagStr: pageTags) {
                Tag tag = tagManager.resolve(tagStr);
                if (tag != null) {
                    tagList.add(tag.getName());
                }
            }
            if (!tagList.isEmpty()) tags = String.join(",", tagList);
        }
        return tags;
    }
}

Dispatcher Changes  

demoproject_rewrites.rules 

In the client project rewrites.rules (/src/conf.d/rewrites) file add a rewrite rule for .rss extension. This rewrite rule takes a URL ending with .rss and rewrites it to point to a corresponding rss.xml file in the page component, effectively changing the file extension from .rss to .rss.xml

#feed rewrite rule
RewriteRule ^/(.*).rss$ /content/demoproject/us/en/$1.rss.xml [PT,L]

100_demoproject_dispatcher_farm.any  

Set the URL parameters that should not be cached for the rss feed. It is recommended that you configure the ignoreUrlParams setting in an allowlist manner. As such, all query parameters are ignored and only known or expected query parameters are exempt (denied) from being ignored.

When a parameter is ignored for a page, the page is cached upon its initial request. As a result, the system subsequently serves requests for the page using the cached version, irrespective of the parameter’s value in the request. Here, we add URL parameters below to serve the content live as required by an external application.

/ignoreUrlParams {
    /0001 { /glob "*" /type "allow" }
    /0002 { /glob "category" /type "deny" }
    /0003 { /glob "pubdate_gt" /type "deny" }
    /0004 { /glob "pubdate_lt" /type "deny" }
}

 

Why is HTL Better?  

We can utilize this approach to produce any XML feed, extending beyond RSS feeds. We have the flexibility to add custom properties to tailor the feed to our specific needs. Plus, we can easily apply filters using query parameters.

 

Big thanks to my director, Grace Siwicki, for her invaluable assistance in brainstorming the implementation and completing this blog work.

Thoughts on “Create an RSS Feed using HTL”

  1. “A few days ago, I was searching about RSS feeds, and I found your article. It’s really informative” and this help me a lot. keep posting more about RSS feed.

    Thanking You

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Raviteja Gunda

Raviteja Gunda is an Adobe Experience Manager technical lead at Perficient. With over 11 years of experience in CMS and related technologies, he demonstrates exceptional competence in his work. Known for devising innovative solutions, he collaborates effectively with his team. He always sees the big picture and consistently drives positive outcomes for clients.

More from this Author

Categories
Follow Us