AEM Content Fragments in the Wild

One of the many features of Adobe Experience Manager are content fragments. With these you can create schemas, author headless content, and distribute that content to various channels. When implementing functionality with content fragments there are four main approaches you can take:

  1. Content fragment -> Core component -> Add style options
  2. Content fragment -> Access schema from sling model -> Implement requirements as needed in custom component
  3. Content fragment -> Create API by access content fragment from a servlet -> Use custom API in another channel

In this first blog post we will dive into the first two approaches, providing a use case, demonstrating an example implementation, and explaining the pros and cons of both. In a follow up blog posts we will demonstrate how to create the servlet needed for the third approach. In a final blog post we will look into utilizing what we learned from the second blog post in a React application.

Content fragment with core component

Content fragment -> Core component -> Add style options

User story

As an end user I want to see a simple overview of a movie including the title, description, and release date.

Implementation

In our first example we will simply setup AEM to use content fragments and then enable the core components. This will let us use the Content Fragment and Content Fragment List core components for implementing the user story stated above. The first step is to setup AEM as a headless CMS.

Note: Any time you see the phrase “AEM Start” that implies navigating to the root url of your AEM author server such as http://localhost:4502.

Create the fragmentexamples configuration

The first step is to create the configuration for our sample project, fragmentexamples. AEM configurations allow you to do many things such as editable templates, contextual site configurations, and content fragment configurations.

  1. Go to AEM Start > Tools > General > Configuration Browser > Create
  2. Enter “fragmentexamples” for the title
  3. Check the “Content Fragment Models” checkbox
  4. Click create
Create the fragmentexamples DAM Folder

Content fragments are stored in the AEM DAM and are simply treated as an asset like any image or pdf. We will want to create a DAM folder for storing all of our content fragments.

  1. Go to AEM Start > Assets > Files > Create > Folder
  2. Enter the title “fragmentexamples”
  3. Click create
Apply the fragmentexamples configuration to the fragmentexamples DAM folder

Next we need to apply our fragmentexamples configuration to our fragmentexamples DAM folder so that we can create content fragments with custom schemas within this folder.

  1. Go to AEM Start > Assets > Files
  2. Edit the properties of the fragmentexamples folder
  3. Go to the “Cloud Services” tab
  4. In the “Cloud Configuration” field select “fragmentexamples”
Create a “Movie” content fragment model

Now we are ready to create out model. In order to fulfill the requirements of the user story we need to display a list of movies.

  1. Go to AEM Start > Tools > Assets > Content Fragment Models > fragmentexamples > Create
  2. Set the title to “Movie” and then click “Open”.
  3. Add a single line text field with a field label of “Title” and a field name of “title”
  4. Add a single line text field with a field label of “Description and a field name of “description”
  5. Add a date and time field with a label of “Release Date” and a field name of “releaseDate”
  6. Add a content reference field with the label “Hero Image” and a field name of “heroImage”
Create a Movie content fragment

Now that we have configured the example project and created a movie model we can go ahead and create a movie content fragment. This will be the first entry in our headless CMS and will be reused in each of the four samples of this blog post.

  1. Go to AEM Start > Assets > Files > fragmentexamples > Create > Content Fragment
  2. Enter a title for the movie
  3. Click create
  4. Fill in the rest of the fields as desired
Enable the core components

To begin we will create a new project called “fragmentexamples” using Adobe’s project archetype. Below is maven archetype generate command to use. Make sure to pick the version of AEM that you are using. This will setup the boiler plate project for us and install the latest version of Core Components.

mvn archetype:generate \
 -DarchetypeGroupId=com.adobe.granite.archetypes \
 -DarchetypeArtifactId=aem-project-archetype \
 -DgroupId=fragmentexamples \
 -DappsFolderName=fragmentexamples \
 -DarchetypeVersion=19 \
 -DoptionAemVersion=6.4.0 \
 -DinteractiveMode=false

cd fragmentexamples.fragmentexamples

mvn clean install -PautoInstallPackage

Look at the following two files from the ui.apps sub project. These were automatically created for us by Adobe’s project archetype. They simply extend the content fragment and content fragment list core components.

  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/contentfragment/.content.xml
<?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"
    jcr:primaryType="cq:Component"
    jcr:title="Content Fragment"
    sling:resourceSuperType="core/wcm/components/contentfragment/v1/contentfragment"
    componentGroup="fragmentexamples"/>
  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/contentfragmentlist/.content.xml
<?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"
    jcr:primaryType="cq:Component"
    jcr:title="Content Fragment List"
    sling:resourceSuperType="core/wcm/components/contentfragmentlist/v1/contentfragmentlist"
    componentGroup="fragmentexamples"/>

Now lets go ahead and use these components on a page.

  1. Go to AEM Start > Sites > fragmentexamples > English > Create > Content Page
  2. Enter “Example” into the title field
  3. Click “Create”
  4. Click “Open”
  5. Click on the parsys
  6. Click the plus icon
  7. Select Content Fragment
  8. Edit the content fragment
  9. Select the movie content fragment that we created earlier
  10. Save the dialog

See that every field of the content is displayed with both the field name and the field value. Go ahead and edit the component again and in the elements field select “Title”, “Description”, and “Release Date”. Now you should have something that looks like the below image. Notice that the styling is subpar at best and the field names are showing up. Now we will create a style option for this component to tidy up the display.

Add a style option to the out of the box content fragment component

To style the component we will add a style option that template authors can enable on the component. Then page authors can select that style for the component. The first step is to actually create the style. To do this we will create a client lib as explained below. Make sure to add the “fragmentexamples.contentfragment” category to the embed list in ui.apps/src/main/content/jcr_root/apps/fragmentexamples/clientlibs/clientlib-base/.content.xml. After adding these files build the changes with “mvn clean install -PautoInstallPackage”. Also make sure to

  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/contentfragment/clientlib/.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="[fragmentexamples.contentfragment]"/>
  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/contentfragment/clientlib/clean.css
.fragmentexamples-clean .cmp-contentfragment__element-value {
  margin: 1rem 0 0 0;
}

.fragmentexamples-clean .cmp-contentfragment__element--title {
  font-size: 1.5rem;
}

.fragmentexamples-clean .cmp-contentfragment__element--description {
  font-style: italic;
  color: grey;
}

.fragmentexamples-clean .cmp-contentfragment__element-title {
  display: none;
}

.fragmentexamples-clean .cmp-contentfragment__title {
  display: none;
}
  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/contentfragment/clientlib/css.txt
clean.css

Now we will need to enable this style on the content fragment component.

  1. Go to AEM Start > Tools > Templates > fragmentexamples > Content Page > Edit
  2. In the Layout Container edit the policy of the content fragment component.
  3. Add a policy title of “Content Fragment”
  4. In the right hand column go to the styles tab.
  5. Add a style with the class “fragmentexamples-clean” and name “Clean”
  6. Click the check mark that is in the top right of the page.
  7. Now refresh the example page and in the edit bar of the content fragment component click the paint brush icon and select the “Clean” option.

Now your movie information should be styled as we defined in our css. See the below image to see what it should not look like. Our ability to customize the component in this format is limited, but the only code we had to write was css that targets the core components implementation.

Pros

  1. Reuse the core component functionality
  2. Development work could be as simple as writing a .content.xml in order to extend the core component
  3. The display of the component can be authored and further customized with additional style options

Cons

  1. Non semantic markup
  2. Forces css to conform to a one size fits all markup structure
  3. Limited ability to fulfill non trivial business use cases
  4. Overriding too much functionality might be more difficult than directly implementing a custom component
  5. It is not possible to customize the tag logic of the Content Fragment List component

Content fragment with custom component

Content fragment -> Access schema from sling model -> Implement requirements as needed in custom component

User story

As an end user I want to see a list of movie titles, each of which is a link to that movies IMDB profile.

Implementation

Update the movie content fragment
  1. Go to AEM Start > Tools > Assets > Content Fragment Models > fragmentexamples > Movie > Edit
  2. Add a text field with a field label of “IMDB Profile”, a field name of “imdbProfile”, and a description field with the following text:
    • “This should be a full url to an IMDB movie profile including the ‘https’.”
  3. Next edit the movie you created earlier with a link to the corresponding movie profile on IMDB.
Create the custom component

Now that we have our data created we will go ahead and create a custom component that implements the user story. This will involve creating the component definition, dialog, HTL template, and two sling models. Create the following files and then build these code changes with “mvn clean install -PautoInstallPackage”.

  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/movielist/.content.xml
<?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"
    jcr:primaryType="cq:Component"
    jcr:title="Movie List"
    componentGroup="fragmentexamples"/>
  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/movielist/_cq_dialog/.content.xml
<?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="nt:unstructured"
    jcr:title="Properties"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns">
        <items jcr:primaryType="nt:unstructured">
            <column
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/container">
                <items jcr:primaryType="nt:unstructured">
                    <rootPath
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
                        fieldLabel="Parent Path"
                        rootPath="/content/dam"
                        name="./parentPath" />
                    <tags
                        jcr:primaryType="nt:unstructured"
                        sling:resourceType="cq/gui/components/common/tagspicker"
                        fieldLabel="Tags"
                        name="./cq:tags"/>
                </items>
            </column>
        </items>
    </content>
</jcr:root>
  • ui.apps/src/main/content/jcr_root/apps/fragmentexamples/components/content/movielist/movielist.html
<sly data-sly-use.movieList="fragmentexamples.core.models.MovieListModel" />

<h3>Your list of movies:</h3>
<ul data-sly-list.movie="${movieList.movies}">
  <li>
    <a href="${movie.imdbProfile}">${movie.title}</a>
  </li>
</ul>

Notice that on line 30 of the below file the double use of the getParent method. This is because the cq:tags property of the content fragment tags field places the property on the <content-fragment>/jcr:content/metadata node. In order to adapt the content fragment you have to go two nodes upwards. This class will be what contains most of our business logic which provides a list of movies to our HTL above.

  • core/src/main/java/fragmentexamples/core/models/MovieListModel.java
package fragmentexamples.core.models;

import com.day.cq.tagging.TagManager;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import java.util.ArrayList;
import java.util.List;
import org.apache.sling.models.annotations.Default;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MovieListModel {
    @SlingObject
    private Resource currentResource;

    @SlingObject
    private ResourceResolver resourceResolver;

    @Inject @Named("cq:tags")
    private String[] cqTags;

    @Inject @Default(values = "/content/dam")
    private String parentPath;

    private final List<ContentFragmentMovie> movies = new ArrayList<>();

    private String message;

    @PostConstruct
    protected void init() {
        final TagManager tagManager = resourceResolver.adaptTo(TagManager.class);

        if (cqTags != null) {
            tagManager.find(parentPath, cqTags, true).forEachRemaining(resource -> {
                final ContentFragmentMovie cfMovie = resource
                    .getParent().getParent().adaptTo(ContentFragmentMovie.class);
                if (cfMovie != null &&
                    ContentFragmentMovie.MODEL_TITLE.equals(cfMovie.getModelTitle())) {
                    movies.add(cfMovie);
                }
            });
        }
    }

    public List<ContentFragmentMovie> getMovies() {
        return movies;
    }
}

The next file is another sling model that retrieves the needed data from the content fragment. It uses the com.adobe.cq.dam.cfm API in order to interact with the content fragment resources instead of directly accessing the CRX.

  • core/src/main/java/fragmentexamples/core/models/ContentFragmentMovie.java
package fragmentexamples.core.models;

import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.ContentElement;
import com.adobe.cq.dam.cfm.FragmentTemplate;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.commons.lang.StringUtils;
import java.util.Optional;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;
import javax.annotation.PostConstruct;
import javax.inject.Inject;

@Model(
    adaptables = Resource.class,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class ContentFragmentMovie {
    public static final String MODEL_TITLE = "Movie";

    @Inject @Self
    private Resource resource;

    private Optional<ContentFragment> contentFragment;

    @PostConstruct
    public void init() {
        contentFragment = Optional.ofNullable(resource.adaptTo(ContentFragment.class));
    }

    public String getModel() {
        return contentFragment
            .map(ContentFragment::getTemplate)
            .map(FragmentTemplate::getTitle)
            .orElse(StringUtils.EMPTY);
    }

    public String getTitle() {
        return contentFragment
            .map(cf -> cf.getElement("title"))
            .map(ContentElement::getContent)
            .orElse(StringUtils.EMPTY);
    }

    public String getDescription() {
        return contentFragment
            .map(cf -> cf.getElement("description"))
            .map(ContentElement::getContent)
            .orElse(StringUtils.EMPTY);
    }

    public Calendar getReleaseDate() {
        return ((Calendar) contentFragment
            .map(cf -> cf.getElement("releaseDate"))
            .map(ContentElement::getValue)
            .map(FragmentData::getValue)
            .orElse(StringUtils.EMPTY));
    }

    public String getImdbProfile() {
        return contentFragment
            .map(cf -> cf.getElement("imdbProfile"))
            .map(ContentElement::getContent)
            .orElse(StringUtils.EMPTY);
    }

    public String getImage() {
        return contentFragment
            .map(cf -> cf.getElement("heroImage"))
            .map(ContentElement::getContent)
            .orElse(StringUtils.EMPTY);
    }
}

We finally have all the code changes that we need. Go ahead and build these changes. Before we author the component we need to add a tag to a content fragment.

  1. Go to AEM Start > Tools > Tagging > Create > Create Namespace
  2. Enter “fragmentexamples” for the name and title and click create
  3. Click on the newly created “fragmentexamples” tag and click create tag
  4. Enter “best-movies-ever” for the name and “Best Movies Ever” for the title
  5. Click submit

Now edit your movie content fragment, go to the properties screen, and add this tag to the tags field. With our tagged content fragment ready, go to your page and add the movie list component to the page. Author the parent path to be “/content/dam/fragmentexamples” and select the “Best Movies Ever” tag. Now you should see your movie listed with a link to it’s IMDB profile. Go ahead and create some more movies with this tag and you will see them added to the list.

Pros

  1. Total flexibility over the implementation and functionality
  2. Control over the HTL template in order to write semantic HTML
  3. Extending the core component can hook you into third party functionality such as GlobalLink translation
  4. Clean Java API’s such as TagManager, ContentFragment, and ContentElement make implementing business logic easy

Cons

  1. All the code is custom just like any other custom component would be

Conclusion

AEM content fragments provide powerful and flexible content for use in page level AEM authoring or as an API as we shall see in future blog posts. The core component provides some quick functionality with almost no coding required. Then the content fragment Java API’s allow for easy to implement components driven by content fragments.

Leave a Reply

avatar
  Subscribe  
Notify of