Find more on AEM Content Fragments
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.):
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
|
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);
}
}
}
|
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?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>
|
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:
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
|
<?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"/>
|
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:
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>
|
And a corresponding process
com.wcm.site.workflow.CFDamUpdateGateKeeperProcess:
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
|
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());
}
}
}
|
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