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
- 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
- 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
- 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.
“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