
Find more on AEM Experience Fragments
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:
1 2 3 4 5 6 |
/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:
1 2 3 4 5 6 7 8 9 |
/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:
1 2 3 4 5 6 7 |
/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.
Want to join our team and work on open source projects?
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
@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:
1 2 3 4 5 6 7 |
/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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
@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:
1 2 3 4 5 6 |
<!--?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:
1 2 |
<!--?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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
(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