AEM Content Fragments in Depth. Custom Schemas. Part 2AEM Content Fragments in Depth. Custom Schemas. Part 2

Tech Tips

10 min read

AEM Content Fragments in Depth. Custom Schemas. Part 2

Tags

#AEM

#Content Fragments

#Digital Experience

#Digital Marketing Technology

Share

In our previous article, we introduced you to content fragments. Today, we let you roll up your sleeves and hard code your way through the intricacies of AEM. We’ll start by defining the Video and Event schema models described in the previous article and then explore brightcove AEM video sync.

Video Schema Model

conf > site-com > settings > dam > cfm > models > video:

<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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:Template" allowedpaths="[/content/entities(/.*)?]" ranking="{Long}100">
    <jcr:content cq:scaffolding="/conf/site-com/settings/dam/cfm/models/video/jcr:content/model" cq:templatetype="/libs/settings/dam/cfm/model-types/fragment" jcr:primarytype="cq:PageContent" jcr:title="Video" sling:resourcesupertype="dam/cfm/models/console/components/data/entity" sling:resourcetype="dam/cfm/models/console/components/data/entity/default">
        <model cq:targetpath="/content/entities" jcr:primarytype="cq:PageContent" sling:resourcetype="wcm/scaffolding/components/scaffolding" datatypesconfig="/mnt/overlay/settings/dam/cfm/models/formbuilderconfig/datatypes" maxgeneratedorder="20">
            <cq:dialog jcr:primarytype="nt:unstructured" 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" maxgeneratedorder="20">
                        <brc_title jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="Name" listorder="1" maxlength="255" required="on" metatype="text-single" name="brc_title" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <brc_poster jcr:primarytype="nt:unstructured" sling:resourcetype="dam/cfm/models/editor/components/contentreference" fieldlabel="Poster" filter="hierarchy" listorder="8" metatype="reference" name="brc_poster" namesuffix="contentReference" renderreadonly="false" rootpath="/content/dam" showemptyinreadonly="true" validation="cfm.validation.contenttype.image" valuetype="string/reference">                      
                        <brc_duration jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" disabled="{Boolean}true" fieldlabel="Duration" listorder="1" maxlength="255" metatype="text-single" name="brc_duration" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <brc_created_at jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" disabled="{Boolean}true" displayedformat="YYYY-MM-DD HH:mm" emptytext="YYYY-MM-DD HH:mm" fieldlabel="Created" listorder="5" metatype="date" name="brc_created_at" renderreadonly="false" showemptyinreadonly="true" type="datetime" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></brc_created_at>
                        <brc_id jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/hidden" fieldlabel="Id" listorder="1" maxlength="255" metatype="text-single" name="brc_id" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                    </brc_id></brc_duration></brc_poster></brc_title></items>
                </content>
            </cq:dialog>
        </model>
    </jcr:content>
</jcr:root>

Event Schema Model

This schema will serve both for upcoming Event and On-Demand webinars conf > site-com > settings > dam > cfm > models > event:

<!--?xml version="1.0" encoding="UTF-8"?-->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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:Template" allowedpaths="[/content/entities(/.*)?]" ranking="{Long}100">
    <jcr:content cq:scaffolding="/conf/site-com/settings/dam/cfm/models/event/jcr:content/model" cq:templatetype="/libs/settings/dam/cfm/model-types/fragment" jcr:primarytype="cq:PageContent" jcr:title="Event" sling:resourcesupertype="dam/cfm/models/console/components/data/entity" sling:resourcetype="dam/cfm/models/console/components/data/entity/default">
        <model cq:targetpath="/content/entities" jcr:primarytype="cq:PageContent" sling:resourcetype="wcm/scaffolding/components/scaffolding" datatypesconfig="/mnt/overlay/settings/dam/cfm/models/formbuilderconfig/datatypes" maxgeneratedorder="20">
            <cq:dialog jcr:primarytype="nt:unstructured" 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" maxgeneratedorder="20">
                        <name jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="Event Name" listorder="1" maxlength="255" metatype="text-single" name="eventName" renderreadonly="false" required="on" showemptyinreadonly="true" valuetype="string">
                        <start_date jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" displayedformat="YYYY-MM-DD HH:mm" displaytimezonemessage="{Boolean}true" emptytext="YYYY-MM-DD HH:mm" fieldlabel="Start Date" listorder="5" metatype="date" name="startDate" renderreadonly="false" required="on" showemptyinreadonly="true" type="[datetime,datetime]" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar/datetime">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></start_date>
                        <end_date jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/datepicker" displayedformat="YYYY-MM-DD HH:mm" displaytimezonemessage="{Boolean}true" emptytext="YYYY-MM-DD HH:mm" fieldlabel="”End" date"="" listorder="5" metatype="date" name="endDate" renderreadonly="false" showemptyinreadonly="true" type="[datetime,datetime]" valueformat="YYYY-MM-DD[T]HH:mm:ss.000Z" valuetype="calendar/datetime">
                            <granite:data jcr:primarytype="nt:unstructured" typehint="Date">
                        </granite:data></end_date>
                        <duration jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fielddescription="Please provide event duration in minutes." fieldlabel="Duration, minutes" listorder="1" maxlength="255" metatype="text-single" name="duration" renderreadonly="false" showemptyinreadonly="true" valuetype="string">
                        <form_id jcr:primarytype="nt:unstructured" sling:resourcetype="granite/ui/components/coral/foundation/form/textfield" fieldlabel="FORM - ID" listorder="1" maxlength="255" metatype="text-single" name="formID" renderreadonly="false" showemptyinreadonly="true" valuetype="string">      
                        <webinarvideoreference jcr:primarytype="nt:unstructured" sling:resourcetype="dam/cfm/models/editor/components/fragmentreference" allownew="{Boolean}true" fieldlabel="Webinar Video" filter="hierarchy" fragmentmodelreference="/conf/site-com/settings/dam/cfm/models/video" listorder="21" metatype="fragment-reference" name="webinarVideoReference" namesuffix="contentReference" renderreadonly="false" rootpath="/content/dam/site-com/content-fragments/${psCFRegion}/webinars" showemptyinreadonly="true" subtype="content-fragment" valuetype="string/content-fragment">
                            <field jcr:primarytype="nt:unstructured" rootpath="/content/dam/site-com/content-fragments/${psCFRegion}/webinars">
                            <granite:data jcr:primarytype="nt:unstructured">
                        </granite:data></field></webinarvideoreference>
                    </form_id></duration></name></items>
                </content>
            </cq:dialog>
        </model>
    </jcr:content>
</jcr:root>

Video Sync Process: Using AEM Brightcove Connector

We’re using Brightcove AEM for videos, so now we’ll set a video sync-up using Adobe AEM Brightcove Connector (the Adobe-AEM-Brightcove Connector allows you to manage Brightcove Video Cloud videos and players within AEM and easily embed videos in AEM pages).

We’ll start by setting up the sync into ”/content/dam/brightcove/en” and then create a process that will create video content fragments based on synced videos.

First of all, we add a pom.xml configuration:

<dependency>
  <groupid>com.coresecure.brightcove.cq5</groupid>
  <artifactid>brightcove_connector</artifactid>
  <type>zip</type>
  <version>5.6</version>
</dependency>

And the sync config apps > site-com > osgiconfig > config.author > com.coresecure.brightcove.wrapper.sling.BrcServiceImpl~b1d15603-3b49-459c-844d-a15120ddebd8.cfg.json:

{
  "client_secret": "{Enter your Brightcove client Secret API}",
  "accountAlias": "{Enter a name for the account to be displayed in the Connector}",
  "client_id": "{Enter your Brightcove client ID from the Brightcove API Authentication page }",
  "allowed_groups": [
    "{Specify the group that will access the Connector (required) be sure that it is a group you are included in}"
  ],
  "key": "{Default Video Player Key}",
  "asset_integration_path": "/content/dam/brightcove/en"
}

And apps > site-com > osgiconfig > config.author > com.coresecure.brightcove.wrapper.webservices.BrcReplicationHandler.cfg.json:

{ 
  "target_directory":"/content/dam/brightcove/en"
}

The Brightcove Import Listener to create or update a video content fragment based on video assets from Brightcove AEM (a simplified code with all non-null checks skipped):

.....
    @Activate
    public void activate() {
        try {
            session = repository.loginService(SERVICE_NAME, null);
 
            JackrabbitEventFilter ef = new JackrabbitEventFilter()
                    .setAbsPath("/content/dam/brightcove/en")
                    .setNodeTypes(new String[]{JcrConstants.NT_UNSTRUCTURED})
                    .setEventTypes(Event.PROPERTY_CHANGED | Event.PROPERTY_ADDED)
                    .setIsDeep(true)
                    .setNoExternal(true);
            JackrabbitObservationManager om = (JackrabbitObservationManager) session.getWorkspace().getObservationManager();
            om.addEventListener (this, ef);
 
        } catch (RepositoryException e) {
            LOG.error("Unable to register session", e);
        }
    }
.......
 
    @Override
    public void onEvent(EventIterator events) {
        try (ResourceResolver resourceResolver = getResourceResolverFromSession(session, resourceResolverFactory)) {      
            while (events.hasNext()) {
                Event event = events.nextEvent();
                String path = event.getPath();
                if (!path.endsWith(ConnectorField.LAST_SYNC.getFieldName())) {
                    continue;
                }
                String metadataRelPath = joinAsPath(JCR_CONTENT, METADATA_FOLDER);
                String targetPath = StringUtils.substringBeforeLast(path, "/" + metadataRelPath);
 
                WorkflowSession workflowSession = resourceResolver.adaptTo(WorkflowSession.class);
                WorkflowModel workflowModel = workflowSession.getModel(“{brightcove sync metadata workflow path}”);
 
                Map<string, object=""> properties = new HashMap<>();
                properties.put(JOB_PARAMETER_PATH, targetPath);
                properties.put(JOB_PARAMETER_WF_ID, WF_MODEL_AUTHOR_ID);
                jobManager.addJob(TOPIC, properties);
            }
 
        } catch (WorkflowException | RepositoryException | RuntimeException ex) {
            LOG.error("Exception", ex);
        }
    }
</string,>

And a simple enum for Brightcove AEM field mapping:

package com.wcm.site.model.brightcove;
 
import com.adobe.cq.dam.cfm.ContentFragment;
import com.wcm.site.util.CFMUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.ValueMap;
 
import java.util.Optional;
 
public enum ConnectorField {
    ID("brc_id"),
    TITLE("brc_title"),
    TEXT_TRACKS("brc_text_tracks"),
    CUSTOM_FIELDS("brc_custom_fields"),
    BRC_TAGS("brc_tags"),
    TAGS("tags"),
    PAGE_NAME_HINT("pageNameHint"),
    DETAILS("details"),
    IS_AEM_EXPOSED("isaemexposed"),
    TYPE("type"),
    DESCRIPTION("brc_description"),
    LONG_DESCRIPTION("brc_long_description"),
    CREATED_AT("brc_created_at"),
    UPDATED_AT("brc_updated_at"),
    DURATION("brc_duration"),
    TEXT("text"),
    POSTER("brc_poster"),
    STAGE("stage"),
    LAST_SYNC("brc_lastsync"),
    THUMBNAIL("brc_thumbnail");
 
 
    private String fieldName;
 
    ConnectorField(String fieldName) {
        this.fieldName = fieldName;
    }
 
    public String getContent(Optional<contentfragment> contentFragment) {
        return CFMUtils.getContent(contentFragment, this.fieldName);
    }
 
    public <t> T getValue(Optional<contentfragment> contentFragment, Class<t> type) {
        return CFMUtils.getValue(contentFragment, this.fieldName, type);
    }
 
    public boolean getBooleanValue(Optional<contentfragment> contentFragment) {
        return BooleanUtils.toBoolean(CFMUtils.getValue(contentFragment, this.fieldName));
    }
 
    public <t> T getValue(ValueMap valueMap, Class<t> type) {
        return valueMap.get(this.getFieldName(), type);
    }
 
    public String getValue(Optional<contentfragment> contentFragment) {
        return Optional.ofNullable(getValue(contentFragment, String.class))
                .orElse(StringUtils.EMPTY);
    }
 
    public String getValue(ValueMap valueMap) {
        return Optional.ofNullable(getValue(valueMap, String.class))
                .orElse(StringUtils.EMPTY);
    }
 
    public boolean getBooleanValue(ValueMap valueMap) {
        return BooleanUtils.toBoolean(this.getValue(valueMap));
    }
 
    public String getFieldName() {
        return fieldName;
    }
}
</contentfragment></t></t></contentfragment></t></contentfragment></t></contentfragment>

Stay tuned for the next articles on this subject. We’ll be showing how to manage Event content fragments and corresponding pages next. Should you have any questions or need AEM development services, reach out to us. We work wonders with AEM!

Resource Hub

Our Latest Stories & Industry Insights

View Resource Hub

Tangent logo with text 'an Exadel AI company' separated by a vertical line on a light background.

Exadel Acquires UK-Based Consultancy Tangent to Elevate AI-Powered Digital Experiences and Expand Global Presence

3 min read

May 18, 2026
Exadel Colleague logo with a colorful symbol on a gradient background.

Exadel Launches Exadel Colleague, an Autonomous AI Delivery Peer Built to Solve Problems AI Coding Tools Leave Behind

3 min read

May 14, 2026
Digital transition from AI experiments with data to cloud enterprise platforms highlighting scalability and security.

From AI Experiments to Enterprise Platforms

14 min read

April 24, 2026
Holographic cloud with upload and download arrows above a tablet, symbolizing cloud computing and data transfer.

Migrating to AEM Cloud: The Unexpected Upside for Front-End Delivery

7 min read

April 24, 2026
Portrait of Carlos Macedo with quote about engineering solving real problems for real people.

Perfection Doesn’t Ship: Lessons from Carlos Macedo’s Engineering Journey

6 min read

April 21, 2026
Woman in sunglasses and striped shirt holding hand, with background of rooftops and grassy hill.

Valentina Panova on career paths, confidence, and work-life balance

6 min

April 13, 2026

Women@Exadel

Two people sitting at a table with a laptop.

Let’s make your next project faster, safer, smarter.

Get In Touch