Tech Insights

AEM Content Fragments: Content Structure

In this article we will show you how to prepare CFs’ structure and translation configuration.

Content Structure

There are no restrictrictions where you can store content fragments in AEM. Originally, you would store them in /content/dam/site-com’ (the We Retail site has this type of structure, for example). This might be not handy if your site folder has a lot of subfolders, and having several folders for every language would create a mess.

To create the language root, you create a folder and use an ISO language code as a name. The language code must be in one of the following formats:

  • <language-code> The supported language code is a two-letter code as defined by ISO-639-1 (like en).
  • <language-code>_<country-code> or <language-code>-<country-code> The supported country code is a lower-case or upper-case two-letter code as defined by ISO 3166, such as en_US, en_us, en_GB, en-gb.

AEM will determine where the language root is automatically. You can have a separate folder for CFs just for your comfort. This approach might be particularly handy if you want to configure quick access to the CFs folder from Navigation (this will be explained below).

Our content fragments are stored under ‘/content/dam/site-com/content-fragments’.

Here’s the content structure for them:

/en serves as a blueprint.

We use on-deploy scripts so we don’t have to create it manually (this feature is not AEM as a Cloud Service compatible; starting with the 4.6.2 release, this feature is disabled on this platform, including the SDK.):

package com.wcm.site.util.ondeploy.scripts;
import com.adobe.acs.commons.ondeploy.scripts.OnDeployScriptBase;
import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.dam.commons.util.DamLanguageUtil;
import com.wcm.site.localization.Locale;
import com.wcm.site.localization.PathContext;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.jcr.resource.JcrResourceConstants;
 
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
 
import static com.day.cq.commons.jcr.JcrConstants.*;
import static com.wcm.site.util.LinkUtils.joinAsPath;
import static org.apache.sling.jcr.resource.api.JcrResourceConstants.NT_SLING_FOLDER;
 
public class CreateFoldersForCFScript extends OnDeployScriptBase {
  private static final String SITE_CF_SCHEMA = "/conf/global/settings/dam/adminui-extension/metadataschema/site-contentfragment";
  private static final String METADATA_SCHEMA_PN = "metadataSchema";
 
   @Override
   protected void execute() throws PersistenceException {
     ResourceUtil.getOrCreateResource(getResourceResolver(),
        joinAsPath(PathContext.Site.getCfPath(), JCR_CONTENT),
        NT_UNSTRUCTURED, NT_SLING_FOLDER, true);
 
     Arrays.stream(PathContext.Site.values()).forEach(site -> Optional.ofNullable(
     getResourceResolver().getResource(joinAsPath(site.getCfPath(), JCR_CONTENT)))
       .map(resource -> resource.adaptTo(ModifiableValueMap.class)).ifPresent(vm -> {
          vm.put(METADATA_SCHEMA_PN, SITE_CF_SCHEMA);
          vm.put("cq:conf", "/conf/site-com");
       })
     );
 
     for(Locale locale: Locale.values()) {
       String folderName = locale.name();
       this.createContentFragmentFolders(PathContext.Site.getCfPath(), folderName);
     }
 
     this.createContentFragmentFolders(PathContext.Site.getCfPath(), "en");
  }
 
  protected final void createContentFragmentFolders(String contentRoot, String folderName) throws PersistenceException {
    String targetPath = joinAsPath(contentRoot, folderName);
    if (getResourceResolver().getResource(targetPath) == null) {
      Map<String, Object> properties = new HashMap<>();
      properties.put(JcrConstants.JCR_PRIMARYTYPE, JcrResourceConstants.NT_SLING_FOLDER);
 
      Resource folder = getResourceResolver().create(getResourceResolver().getResource(contentRoot),folderName, properties);
      getResourceResolver().commit();
      properties.clear();
 
      properties.put(JcrConstants.JCR_PRIMARYTYPE, NT_UNSTRUCTURED);
      properties.put(JCR_TITLE, folderName);
 
      getResourceResolver().create(folder, JCR_CONTENT, properties);
    }
  }
}
See more See less

Now, we can create a language copy. This will just copy the content; translation will be set up later.

Let’s say we have a content fragment in a blueprint folder: ‘/content/dam/site-com/content-fragments/en/myfirstcf/test-cf’. Select this CF and click References on the left:

As you can see, only one language copy exists now. To create a French version, click ‘Create and Translate:’

After that you’ll see that a French CF was created:

CF Quick Access

We will start by setting up quick access to our Content Fragments in AEM by adding them to Navigation. This is a quick fix; just add a new item under /apps/cq/core/content/nav.

.content.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
  xmlns:jcr="http://www.jcp.org/jcr/1.0"
  xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
  jcr:description="Content Fragments"
  jcr:mixinTypes="[sling:Redirect]"
  jcr:primaryType="nt:unstructured"
  jcr:title="Site Content Fragments"
  sling:resourceType="crx/core/components/welcome/tool"
  sling:target="/assets.html/content/dam/site-com/content-fragments"
  href="/assets.html/content/dam/site-com/content-fragments"
  icon="documentFragmentGroup"
  id="site-cf"
  order="1110">
</jcr:root>
See more See less

A new item is available in Navigation:

Global Link Translation Setup

Adobe recommends using the Translation Integration Framework to manage the localization of your data. The Translation Integration Framework integrates with third-party translation services to orchestrate the translation of AEM content. To use it:

  • Connect to your translation service provider
  • Create a Translation Integration Framework configuration
  • Associate the cloud configurations with your pages/assets

We are using Global Link (v5.9.8) as a translation service provider. This version does not provide an implementation of the new API for translation projects, so we have to use the Global Link admin console and set up configuration for GL itself.

To set up a translation in GL, we need to create a new repository for it.

Adding the .xml config is enough:

/libs/globallink-adaptor/config/repositories/content/dam/site-com/content-fragments/.content.xml:

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
  jcr:primaryType="sling:Folder"
  allowedResourceTypes="[jcr:title@*,text@*]"
  alwaysCopyTargetValue="false"
  autoSubmissionFriValue="false"
  autoSubmissionLastRun="0"
  autoSubmissionMonValue="false"
  autoSubmissionSatValue="false"
  autoSubmissionScheduleType="sch_time"
  autoSubmissionSunValue="false"
  autoSubmissionThuValue="false"
  autoSubmissionTueValue="false"
  autoSubmissionWedValue="false"
  binaryTranslationEnable="false" blockedResourceTypes="[sling:resourceType@*,sling:resourceSuperType@*,cq:template@*,jcr:primaryType@*,cq:lastModified*@*,jcr:lastModified*@*,jcr:created*@*,jcr:mixinTypes*@*,textIsRich@*,]"
  copyTagsToTarget="false"
  customLinkRewritingEnabled="false"
  damAssetProperties="[title,dc:title,description]"
  damAssetsTranslationEnable="true"
  defaultSplitSub="false"
  defaultSubmissionStatus="NOT_READY"
  deleteArrayMembers="false"
  eMailOnError="false"
  eMailOnSuccess="false"
  eMailToInitiator="false"
  enableCopyToTarget="true"
  enableIgnore="false"
  enableLinkRewriting="true"
  enablePublishers="false"
  enableReviewers="false"
  enableSynchronizeDeletions="false"
  enableWorkflowTargetLanguages="false"
  includeAssetOriginalNodeEnable="false" languageMapping="[English@en@en-US,German@de_de@de-DE,French@fr_fr@fr-FR,Japan@ja_jp@ja-JP,Korea@ko_kr@ko-KR,Brazil@pt_br@pt-BR,Latam@es_la@es-LA,Dutch@nl_nl@nl-NL,Taiwan@zh_tw@zh-TW,Russian@ru_ru@ru-RU,Italian@it_it@it-IT]"
  pdClassifier="AEMP"
  pdProjectShortCode="TEST000031"
  propertyBinaryTranslationEnable="false"
  referenceChildrenDepth="1"
  referenceTranslationEnable="false"
  repositoryEnable="true"
  repositoryPath="/content/dam/site-com/content-fragments"
  rewriteLinksCopyProperties="false"
  secondaryBinaryClassifiers="image/png@Non-Parsable"
  sortAlphabetically="false"
  sortByDate="false"
  submissionNamePatern="."
  tagTranslationEnable="false"
  targetRules="[$PATH(@replace($SRC_LANG\,$TGT_LANG))]"
  tmUpdateEnabled="false"
  useSlingKey="false"
  workflowOnDelivery="GlobalLink On Delivery Workflow"/>
See more See less

The property that we’re interested in is ‘damAssetProperties=“[title,dc:title,description]”’, which is where we list the properties in the CF available for translation.

Update Asset Workflow Change

Since a content fragment is technically an asset (since it has type dam:Asset), we need to prevent ‘Update DAM Asset workflow’ from applying to them.

/conf/global/settings/workflow/models/dam/update_asset/.content.xml

We need to add a new process config in the beginning of the <flow> section:

<process_cf
 jcr:description="Checks if the workflow should terminate eg. when it's executed against a content fragment"
 jcr:lastModified="{Date}2021-01-27T22:02:26.155+03:00"
 jcr:lastModifiedBy="admin"
 jcr:primaryType="nt:unstructured"
 jcr:title="CF Continue updating?"
 sling:resourceType="cq/workflow/components/model/process">
 <metaData
  jcr:primaryType="nt:unstructured"
  PROCESS="com.wcm.site.workflow.CFDamUpdateGateKeeperProcess"
  PROCESS_AUTO_ADVANCE="true"/>
</process_cf>
1
2
3
4
5
6
7
8
9
10
11
12
<process_cf
jcr:description="Checks if the workflow should terminate eg. when it's executed against a content fragment"
jcr:lastModified="{Date}2021-01-27T22:02:26.155+03:00"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="CF Continue updating?"
sling:resourceType="cq/workflow/components/model/process">
<metaData
  jcr:primaryType="nt:unstructured"
  PROCESS="com.wcm.site.workflow.CFDamUpdateGateKeeperProcess"
  PROCESS_AUTO_ADVANCE="true"/>
</process_cf>
See more See less

And a corresponding process

com.wcm.site.workflow.CFDamUpdateGateKeeperProcess:

package com.wcm.site.workflow;
 
import com.adobe.acs.commons.util.WorkflowHelper;
import com.adobe.granite.workflow.WorkflowException;
import com.adobe.granite.workflow.WorkflowSession;
import com.adobe.granite.workflow.exec.WorkItem;
import com.adobe.granite.workflow.exec.WorkflowProcess;
import com.adobe.granite.workflow.metadata.MetaDataMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import static com.wcm.site.util.CFMUtils.isContentFragment;
import static com.wcm.site.workflow.WorkflowProcessBase.WORKFLOW_PROCESS_LABEL;
 
@Component(immediate = true, service = WorkflowProcess.class, property = { WORKFLOW_PROCESS_LABEL + "=CF Dam Update Gate Keeper" })
public class CFDamUpdateGateKeeperProcess extends WorkflowProcessBase implements WorkflowProcess {
  private static final Logger LOG = LoggerFactory.getLogger(CFDamUpdateGateKeeperProcess.class);
 
  @Reference
  private WorkflowHelper workflowHelper;
 
  @Override
  public void execute(WorkItem workItem, WorkflowSession workflowSession, MetaDataMap metaData) throws WorkflowException {
    ResourceResolver resourceResolver = getResourceResolver(workflowHelper, workflowSession);
    String sourcePath = getTargetPath(workItem, true);
    Resource source = resourceResolver.getResource(sourcePath);
    if (source == null || isContentFragment(resourceResolver.getResource(sourcePath))) {
     LOG.info("Terminating update asset workflow for content fragment (or null asset) {}", sourcePath);
     workflowSession.terminateWorkflow(workItem.getWorkflow());
    }
  }
}
See more See less

This quick look at CF content structure and initial set up will get you started. As our content fragment series continues, we’ll continue to take a deeper dive into working with CF.

Author: Iryna Ason