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

Women in Exadel: Let's Talk — Stories of Growth, Leadership, and Resilience

7 min read

July 2, 2026

AEM Cloud Migration using Agentic AI: Beyond Generic Tooling

7 min read

June 30, 2026

AI Readiness Assessment for Financial Services: Benchmarks, Challenges, and How to Move Forward

15 min read

June 29, 2026

How to Run an AI Readiness Assessment: A Step-by-Step Framework

14 min read

June 26, 2026

The 5 Stages of AI Readiness: Where Does Your Organization Stand?

9 min read

June 12, 2026

Whitepaper: Token Cost Optimization for Enterprise AI. A Practical Framework to Reduce LLM Costs by 50–80%

28 min read

June 9, 2026
Two people sitting at a table with a laptop.

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

Get In Touch