AEM Experience Fragments: Rollout Configuration

Exadel Digital Experience Team Tech Insights July 24, 2020 14 min read

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
See more See less

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
See more See less

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>
See more See less

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>
See more See less

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>
See more See less

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>
See more See less

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>
See more See less

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>
See more See less

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));
See more See less

Author: Iryna Ason

Was this article useful for you?

Get in the know with our publications, including the latest expert blogs