AEM Experience Fragments: Rollout ConfigurationAEM Experience Fragments: Rollout Configuration

Tech Insights

14 min read

Table of Contents

Tags

#AEM

#Digital Marketing Technology

#Experience Fragments

Share

Our site pages are stored under /content.

All the Experience fragments are stored under /content/experience-fragments. When we were designing the structure for experience fragment (XF) pages we wanted them to correlate to our existing site pages.

Unlike ordinary AEM pages, XF pages cannot be created one under another. In order to mimic the structure of our main site, or just to group fragments logically in a tree structure, we can create folders/subfolders.

Our site has the following structure:
/site-com - blueprint
/site-com-live - live copies
      de_de
      en_us
      it_it
      fr_fr
In order to have different language versions for an XF, we can create XF variations. The difference between XF and the ordinary pages is that the live XF version will be stored under the same node as the blueprint one.

So, the structure of a single XF will be like this:
/content
       /experience-fragments
              /site-com
                     /xf-test
                          xf-test - main variation - "blueprint" version
                          en_us
                          it_it
                          de_de
                          fr_fr
In order to be able to rollout XFs we need to create a blueprint configuration for XFs:
/apps/msm/site-com/blueprintconfigs/xf-blueprint:
<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primarytype="cq:Page">
   <jcr:content cq:template="/libs/wcm/msm/templates/blueprint" jcr:primarytype="nt:unstructured" jcr:title="XF Blueprint" sling:resourcetype="wcm/msm/components/blueprint" sitepath="/content/experience-fragments/site-com" thumbnailrotate="0">
       <dialog></dialog>
   </jcr:content>
</jcr:root>
But this is not enough.

Rewriting XF links

When we rollout a page containing an XF e.g. to fr_fr, and there is french version of that XF, we would expect to automatically see the french XF variation on the french site page. To achieve this we need to create a custom rollout action.

@Component(immediate = true,
       service = LiveActionFactory.class,
       property = {
               LiveActionFactory.LIVE_ACTION_NAME + "=" + XFReferencesUpdateActionFactory.LIVE_ACTION_CLASS_NAME,
               LiveActionFactory.LIVE_ACTION_NAME + "=" + XFReferencesUpdateActionFactory.LIVE_ACTION_NAME
       })
public class XFReferencesUpdateActionFactory extends FilteredActionFactoryBase<xfreferencesupdateactionfactory.xfreferencesupdateaction> {
   public static final String LIVE_ACTION_CLASS_NAME = "XFReferencesUpdateAction";
   public static final String LIVE_ACTION_NAME = "referencesUpdateXF";
   @Reference
   private RolloutManager rolloutManager;
 
   @Reference
   private LiveRelationshipManager relationshipManager;
 
   @Activate
   @Modified
   protected void configure(ComponentContext context) {
       setupFilter(context, this.rolloutManager);
   }
 
   @Override
   protected XFReferencesUpdateAction newActionInstance(ValueMap valueMap) throws WCMException {
       return new XFReferencesUpdateAction(valueMap, this.getPagePropertyFilter(), this.getComponentFilter(), this);
   }
 
   @Override
   public String createsAction() {
       return LIVE_ACTION_NAME;
   }
 
   class XFReferencesUpdateAction extends FilteredAction {
 
       protected XFReferencesUpdateAction(ValueMap configuration, ItemFilterImpl pageItemFilter, ItemFilterImpl componentItemFilter, BaseActionFactory<!--? extends FilteredAction--> factory) {
           super(configuration, pageItemFilter, componentItemFilter, factory);
       }
 
       @Override
       protected boolean doHandle(Resource source, Resource target, LiveRelationship relation, boolean resetRollout)
               throws RepositoryException, WCMException {
           return (resourceHasNode(source)) && (resourceHasNode(target) &&
                   (source.isResourceType(ExperienceFragmentsConstants.RT_EXPERIENCE_FRAGMENT_COMPONENT)));
       }
 
       @Override
       protected void doExecute(Resource source, Resource target, LiveRelationship relation, boolean resetRollout)
               throws RepositoryException, WCMException {
           String fragmentPath = source.getValueMap().get(ExperienceFragmentsConstants.PN_FRAGMENT_PATH, StringUtils.EMPTY);
           ResourceResolver resolver = target.getResourceResolver();
           PageManager pageManager = resolver.adaptTo(PageManager.class);
           Resource fragment = resolver.getResource(fragmentPath);
 
           Page targetPage = pageManager.getPage(relation.getLiveCopy().getPath());
 
           Page targetLanguageRoot = targetPage.getAbsoluteParent(2);
 
           String targetLanguage = targetLanguageRoot.getName();
           adjustReferences(pageManager, fragment, target, targetLanguage);
       }
 
       private void adjustReferences(PageManager pageManager, Resource fragment, Resource target, String targetLanguage) throws RepositoryException, WCMException {
           RangeIterator relationshipsIterator = relationshipManager.getLiveRelationships(fragment, null, null);
 
           while (relationshipsIterator.hasNext()) {
               LiveRelationship relationship = (LiveRelationship) relationshipsIterator.next();
               Page fragmentLiveCopyPage = pageManager.getPage(relationship.getLiveCopy().getPath());
 
               if (isSubjectForReferencesAdjustment(fragmentLiveCopyPage, targetLanguage)) {
                   new ReferenceSearch().adjustReferences(target.adaptTo(Node.class), relationship
                           .getSourcePath(), relationship
                           .getTargetPath(), true, Collections.emptySet());
               }
           }
       }
 
       private boolean isSubjectForReferencesAdjustment(Page fragmentLiveCopyPage, String targetLanguage) {
           return fragmentLiveCopyPage != null && fragmentLiveCopyPage.getPath().endsWith(targetLanguage);
       }
   }
}
</xfreferencesupdateactionfactory.xfreferencesupdateaction>
And a separate rollout config for the created action:
/apps/msm/wcm/rolloutconfigs/site-com/updateXFReferences
<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primarytype="cq:Page">
   <jcr:content cq:defaultview="html" cq:rolloutconfigid53="cq:trigger=rollout#updateContent/status=true####" cq:template="/libs/wcm/msm/templates/rolloutconfig" cq:trigger="rollout" jcr:description="Updating experience fragment references for regional sites on rollout trigger" jcr:primarytype="nt:unstructured" jcr:title="Update XF references config" sling:resourcetype="wcm/msm/components/rolloutconfig">
       <referencesupdatexf jcr:primarytype="cq:LiveSyncAction"></referencesupdatexf>
   </jcr:content>
</jcr:root>
Now, for each locale we should add the config (Page Properties -> Live Copy tab):

Rewriting site links inside XFs

When we roll out an XF page we expect all the site links to be rewritten according to the locale we are rolling it out to. We will create another rollout action to take care of the site link inside an XF.

@Component(immediate = true,
       service = LiveActionFactory.class,
       property = {
               LiveActionFactory.LIVE_ACTION_NAME + "=" + SiteReferencesUpdateActionFactory.LIVE_ACTION_CLASS_NAME,
               LiveActionFactory.LIVE_ACTION_NAME + "=" + SiteReferencesUpdateActionFactory.LIVE_ACTION_NAME
       })
public class SiteReferencesUpdateActionFactory extends FilteredActionFactoryBase<sitereferencesupdateactionfactory.sitereferencesupdateaction> {
   public static final String LIVE_ACTION_CLASS_NAME = "SiteReferencesUpdateAction";
   public static final String LIVE_ACTION_NAME = "referencesUpdateSite";
 
   private static final String CONTENT_PATH_REGEXP = "/content/(site-com)[w-/]*";
   private static final Pattern CONTENT_PATH_PATTERN = Pattern.compile(CONTENT_PATH_REGEXP);
 
   @Reference
   private RolloutManager rolloutManager;
 
   @Reference
   private LiveRelationshipManager relationshipManager;
 
   @Activate
   @Modified
   protected void configure(ComponentContext context) {
       setupFilter(context, this.rolloutManager);
   }
 
   @Override
   protected SiteReferencesUpdateAction newActionInstance(ValueMap valueMap) throws WCMException {
       return new SiteReferencesUpdateAction(valueMap, this.getPagePropertyFilter(), this.getComponentFilter(), this);
   }
 
   @Override
   public String createsAction() {
       return LIVE_ACTION_NAME;
   }
 
   class SiteReferencesUpdateAction extends FilteredAction {
 
       protected SiteReferencesUpdateAction(ValueMap configuration, ItemFilterImpl pageItemFilter, ItemFilterImpl componentItemFilter, BaseActionFactory<!--? extends FilteredAction--> factory) {
           super(configuration, pageItemFilter, componentItemFilter, factory);
       }
 
       @Override
       protected boolean doHandle(Resource source, Resource target, LiveRelationship relation, boolean resetRollout)
               throws RepositoryException, WCMException {
           return resourceHasNode(source) && resourceHasNode(target);
       }
 
       @Override
       protected void doExecute(Resource source, Resource target, LiveRelationship relation, boolean resetRollout)
               throws RepositoryException, WCMException {
           ResourceResolver resolver = target.getResourceResolver();
 
           PageManager pageManager = resolver.adaptTo(PageManager.class);
 
           Page targetPage = pageManager.getPage(relation.getLiveCopy().getPath());
 
           String targetLanguage = targetPage.getName();
 
           Node sourceNode = source.adaptTo(Node.class);
 
           PropertyIterator pi = sourceNode.getProperties();
 
           while (pi.hasNext()) {
               Property property = pi.nextProperty();
               if(property.isMultiple()) {
                   for(Value value : property.getValues()) {
                       processSingleValue(value, resolver, target, pageManager, targetLanguage);
                   }
               } else {
                   processSingleValue(property.getValue(), resolver, target, pageManager, targetLanguage);
               }
           }
       }
 
       private void processSingleValue(Value value, ResourceResolver resolver, Resource target, PageManager pageManager, String targetLanguage) throws RepositoryException, WCMException {
           if(value.getType() != PropertyType.STRING) {
               return;
           }
           String ctaPath = value.getString();
           if(ctaPath == null ||!ctaPath.contains("/content/site-com")) {
               return;
           }
           Matcher pathMatcher = CONTENT_PATH_PATTERN.matcher(ctaPath);
           while (pathMatcher.find()) {
               Resource cta = resolver.getResource(pathMatcher.group());
               adjustReferences(pageManager, cta, target, targetLanguage);
           }
       }
 
       private void adjustReferences(PageManager pageManager, Resource cta, Resource target, String targetLanguage) throws RepositoryException, WCMException {
           RangeIterator relationshipsIterator = relationshipManager.getLiveRelationships(cta, null, null);
 
           while (relationshipsIterator.hasNext()) {
               LiveRelationship relationship = (LiveRelationship) relationshipsIterator.next();
               Page ctaLiveCopyPage = pageManager.getPage(relationship.getLiveCopy().getPath());
 
               if (isSubjectForReferencesAdjustment(ctaLiveCopyPage, targetLanguage)) {
                   new ReferenceSearch().adjustReferences(target.adaptTo(Node.class),
                           relationship.getSourcePath(), relationship.getTargetPath(), true, Collections.emptySet());
               }
           }
       }
 
       private boolean isSubjectForReferencesAdjustment(Page ctaLiveCopyPage, String targetLanguage) {
           return ctaLiveCopyPage != null && ctaLiveCopyPage.getPath().endsWith(targetLanguage);
       }
   }
}
</sitereferencesupdateactionfactory.sitereferencesupdateaction>
And the new rollout config: /apps/msm/wcm/rolloutconfigs/site-com/updateSiteReferences:
<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primarytype="cq:Page">
   <jcr:content cq:defaultview="html" cq:rolloutconfigid53="cq:trigger=rollout#updateContent/status=true####" cq:template="/libs/wcm/msm/templates/rolloutconfig" cq:trigger="rollout" jcr:description="Updating CTA and other references for XF on rollout trigger" jcr:primarytype="nt:unstructured" jcr:title="Update Site references in XF config" sling:resourcetype="wcm/msm/components/rolloutconfig">
       <referencesupdateps jcr:primarytype="cq:LiveSyncAction"></referencesupdateps>
   </jcr:content>
</jcr:root>
We won’t be able to set this config at a blueprint level for XF, so we need to select it by default when we create a new variation:

We will create a new client lib “xf-rollout” .content.xml:
<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primarytype="cq:ClientLibraryFolder" categories="[cq.experiencefragments.authoring]" dependencies="[granite.jquery]"></jcr:root>
xf-live-copy.js:
(function ($, $document) {
   var ROLLOUT_CONFIGS_SELECT = "coral-select[name='cq:rolloutConfigs']";
   $(document).on("dialog-ready", dlgReadyHandler);
   function dlgReadyHandler() {
       if (_.isEmpty($("[value='createLiveCopy'][name='cmd']"))) {
           return;
       }
       var srcPath = $("input[name='srcPath']").attr("value");
 
       if(!srcPath || !isSiteComXF(srcPath)) {
           return;
       }
 
       selectItems(ROLLOUT_CONFIGS_SELECT, [
               "/etc/msm/rolloutconfigs/default",
               "/etc/msm/rolloutconfigs/updateSiteReferences"], true);
   }
 
   function isSiteComXF(srcPath) {
       var result = false;
       $.ajax(
           {
               url: srcPath.replace(".html", ".infinity.json"),
               type: 'GET',
               async: false,
               success: function (data) {
                   var content = data["jcr:content"];
                   if (!content || !content["cq:template"]) {
                       return;
                   }
                   result = (content["cq:template"] === "/conf/site-com/settings/wcm/templates/experience-fragment");
               }
           }
       );
       return result;
   }
 
   function selectItems(selector, values, action) {
       var sel = $(selector);
       sel.each(function(idx, select){
           select.items.getAll().forEach(function(item, idx){
               if(values.includes(item.value)){
                   item.selected = action;
               }
           });
       });
   }
})($, $(document));
Author: Iryna Ason

Resource Hub

Our Latest Stories & Industry Insights

View Resource Hub

Perfection Doesn’t Ship: Lessons from Carlos Macedo’s Engineering Journey

6 min read

April 21, 2026

Valentina Panova on career paths, confidence, and work-life balance

6 min

April 13, 2026

Women@Exadel

The Overlooked Tool That Streamlines AEM as a Cloud Service Migrations

3 min read

April 2, 2026

AEM

Digital Experience

Adobe Cloud Migration

Rethinking Configuration Management in AEM Cloud

3 min read

April 2, 2026

AEM

Digital Experience

Adobe Cloud Migration

Be bold and keep moving - Talk with Karen Hutchison, VP and Global Head of Talent

6 min read

March 29, 2026

#Exadel People

#Women at Exadel

Exadel Women in Tech. Lidana Taborda on Curiosity, Leadership, and Building Better Software Together

5 min read

February 26, 2026
Two people sitting at a table with a laptop.

Let’s make your next project faster, safer, smarter.

Get In Touch