Content Fragments
Content Fragments are channel-neutral, structured content in AEM. Unlike page components that mix content with presentation, Content Fragments separate content from layout, making the same content reusable across websites, mobile apps, SPAs, email, and any other channel via GraphQL, REST, or direct Java API access.
:::info Official documentation
- Content Fragments -- overview (Experience League)
- Managing Content Fragments
- Content Fragment Models
- AEM GraphQL API for Content Fragments
- Headless delivery with Content Fragments & GraphQL :::
Content Fragment Models
A Content Fragment Model defines the schema (fields and their types) for a fragment. Models are
created in the AEM configuration space (/conf) and must be enabled per site configuration.
Enabling CF Models for a site
- Navigate to Tools > General > Configuration Browser
- Select your site configuration (e.g.,
myproject) - Enable Content Fragment Models
- Models are stored at
/conf/myproject/settings/dam/cfm/models/
Available field types
| Field Type | JCR Storage | Java Type | Use case |
|---|---|---|---|
| Single-line text | String | String | Titles, labels, short text |
| Multi-line text | String | String | Rich text (HTML), Markdown, or plain text |
| Number | Long / Double | Long / Double | Quantities, prices, ratings |
| Boolean | Boolean | Boolean | Toggles, flags |
| Date and Time | Calendar | Calendar | Publish dates, event dates |
| Enumeration | String | String | Predefined choices (dropdown) |
| Tags | String[] | String[] | AEM tag references |
| Content Reference | String | String | Path to a page or asset |
| Fragment Reference | String[] | String[] | References to other Content Fragments |
| JSON Object | String (JSON) | String | Arbitrary structured data |
| Tab Placeholder | - | - | Visual grouping in the editor (no data) |
Model JCR structure
/conf/myproject/settings/dam/cfm/models/article
├── jcr:content
│ ├── jcr:title = "Article"
│ ├── jcr:description = "An article with title, body, and author"
│ └── model
│ ├── cq:fields
│ │ ├── title (fieldType: "text-single", required: true)
│ │ ├── body (fieldType: "text-multi", mimeType: "text/html")
│ │ ├── publishDate (fieldType: "calendar")
│ │ ├── category (fieldType: "enumeration", options: [...])
│ │ ├── featuredImage (fieldType: "content-reference")
│ │ ├── author (fieldType: "fragment-reference", modelPath: "/conf/.../author")
│ │ └── relatedArticles (fieldType: "fragment-reference", multiple: true)
Model field validation
Models support built-in validation rules:
| Validation | Applies to | Effect |
|---|---|---|
| Required | All types | Field must have a value |
| Min/Max length | Text fields | Character count limits |
| Min/Max value | Number fields | Numeric range |
| Unique | Text fields | Value must be unique across fragments of this model |
| Pattern (Regex) | Text fields | Value must match the regex |
| Accept (for references) | Content/Fragment references | Limits selectable models or paths |
Authoring in the Content Fragments Console
Most teams create models and fragments through the UI, not code. On AEM as a Cloud Service the dedicated Content Fragments Console is the primary entry point; on AEM 6.5 you use the Assets console plus Tools > General > Content Fragment Models.
Create a model
- Open the Content Fragments Console (AEMaaCS) or Tools > General > Content Fragment Models (6.5).
- Select the configuration folder for your site and choose Create.
- Add fields by dragging data types from the right rail, set each field's Property Name (this becomes the GraphQL field name), and mark required/translatable as needed.
- Enable the model. Only enabled models can back new fragments and generate a GraphQL schema.
Create a fragment
- In the console, choose Create > Content Fragment, pick the model, the target DAM folder, and a name/title.
- Author values in the editor; switch Variations in the left rail to author channel-specific versions.
- Use Associated Content to attach related DAM assets (collections) to the fragment.
:::tip Allow models on a folder A model only appears in the Create dialog for folders where it is permitted. Configure this on the DAM folder's Properties > Policies / Cloud Configuration (or via the model's Allowed Content Fragment Models policy). See Allowing Content Fragment Models on your Assets Folder. :::
Reference: Managing Content Fragment Models and Managing Content Fragments.
Models as code
Models you build in the console live under /conf in the JCR. Like templates and policies, they
should be exported and committed so they deploy identically to every environment -- otherwise your
GraphQL schema differs between Dev, Stage, and Prod.
The model is stored as a cq:Template-style structure; export it via FileVault by adding a filter and
shipping it in a content package (e.g. ui.content or a dedicated ui.conf):
<filter root="/conf/myproject/settings/dam/cfm/models"/>
The exported .content.xml for a model field carries the field type and validation, for example:
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="dam/cfm/models/editor/components/datatypes/multifield"
fieldType="text-single"
name="title"
required="{Boolean}true"
valueType="string"/>
<body
jcr:primaryType="nt:unstructured"
sling:resourceType="dam/cfm/models/editor/components/datatypes/multifield"
fieldType="text-multi"
name="body"
valueType="string"
mimeType="text/html"/>
:::tip Treat model changes like schema migrations Because the GraphQL schema is generated from the model, adding a field is backwards-compatible but renaming or removing one is a breaking change for every headless consumer. Version models deliberately and coordinate changes with client teams. :::
Content Fragment Structure in the JCR
Content Fragments are stored as dam:Asset nodes under /content/dam/. Understanding the JCR
structure is essential for programmatic access.
/content/dam/myproject/articles/my-article
├── jcr:content
│ ├── jcr:primaryType = "dam:AssetContent"
│ ├── data
│ │ ├── cq:model = "/conf/myproject/settings/dam/cfm/models/article"
│ │ ├── title = "My Article Title"
│ │ ├── body = "<p>The article body in HTML...</p>"
│ │ ├── publishDate = "2025-06-15T10:00:00.000+02:00"
│ │ ├── category = "technology"
│ │ ├── featuredImage = "/content/dam/myproject/images/hero.jpg"
│ │ └── author = ["/content/dam/myproject/authors/john-doe"]
│ └── metadata
│ ├── dc:title = "My Article Title"
│ └── dc:description = "..."
Content Fragment data lives under jcr:content/data. The element names match the field names
defined in the model. Fragment references are stored as String arrays of paths.
Variations
Content Fragments support variations - alternative versions of the same content (e.g., a short summary, a social media version). Each variation stores its own set of field values:
/content/dam/myproject/articles/my-article
├── jcr:content
│ ├── data
│ │ ├── master (default variation)
│ │ │ ├── title = "My Article Title"
│ │ │ └── body = "<p>Full article body...</p>"
│ │ └── summary (custom variation)
│ │ ├── title = "My Article"
│ │ └── body = "<p>Short summary...</p>"
Reading Content Fragments (Java API)
Adapting a resource to ContentFragment
The primary API is com.adobe.cq.dam.cfm.ContentFragment:
@Model(
adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class ArticleModel {
@ValueMapValue
private String fragmentPath;
@SlingObject
private ResourceResolver resolver;
private ContentFragment fragment;
@PostConstruct
protected void init() {
if (fragmentPath != null) {
Resource cfResource = resolver.getResource(fragmentPath);
if (cfResource != null) {
fragment = cfResource.adaptTo(ContentFragment.class);
}
}
}
public String getTitle() {
if (fragment == null) return null;
return fragment.getElement("title").getContent();
}
public String getBody() {
if (fragment == null) return null;
ContentElement bodyElement = fragment.getElement("body");
// getContent() returns the raw value; for rich text this is HTML
return bodyElement != null ? bodyElement.getContent() : null;
}
public Calendar getPublishDate() {
if (fragment == null) return null;
FragmentData data = fragment.getElement("publishDate").getValue();
return data != null ? data.getValue(Calendar.class) : null;
}
}
ContentFragment API reference
| Method | Returns | Description |
|---|---|---|
getTitle() | String | Fragment title |
getDescription() | String | Fragment description |
getName() | String | Node name (URL-safe) |
getElement(name) | ContentElement | Access a specific element by field name |
getElements() | Iterator<ContentElement> | Iterate over all elements |
getVariations() | Iterator<ContentVariation> | List available variations |
hasElement(name) | boolean | Check if an element exists |
getAssociatedContent() | Iterator<Resource> | Get associated content (collections) |
adaptTo(Resource.class) | Resource | Get the underlying Sling resource |
ContentElement API reference
| Method | Returns | Description |
|---|---|---|
getName() | String | Element/field name |
getTitle() | String | Display title |
getContent() | String | String value (for text fields) |
setContent(content, mimeType) | void | Set content with MIME type |
getValue() | FragmentData | Typed value container |
setValue(FragmentData) | void | Set typed value |
getContentType() | String | MIME type (text/plain, text/html, etc.) |
getVariation(name) | ContentVariation | Get a specific variation of this element |
getVariations() | Iterator<ContentVariation> | List variations |
Reading typed values
The FragmentData object provides type-safe access to field values:
// String
String title = fragment.getElement("title").getContent();
// Number
FragmentData numberData = fragment.getElement("rating").getValue();
Long rating = numberData.getValue(Long.class);
Double price = fragment.getElement("price").getValue().getValue(Double.class);
// Boolean
Boolean featured = fragment.getElement("featured").getValue().getValue(Boolean.class);
// Date
Calendar date = fragment.getElement("publishDate").getValue().getValue(Calendar.class);
// Fragment references (stored as String array of paths)
FragmentData refData = fragment.getElement("relatedArticles").getValue();
String[] refPaths = refData.getValue(String[].class);
// Content reference (single path)
String imagePath = fragment.getElement("featuredImage").getContent();
Reading variations
// Read the "summary" variation of the body element
ContentElement bodyElement = fragment.getElement("body");
ContentVariation summaryVariation = bodyElement.getVariation("summary");
if (summaryVariation != null) {
String summaryBody = summaryVariation.getContent();
}
// List all available variations
Iterator<ContentVariation> variations = bodyElement.getVariations();
while (variations.hasNext()) {
ContentVariation variation = variations.next();
LOG.info("Variation: {} ({})", variation.getTitle(), variation.getName());
}
Creating Content Fragments Programmatically
Creating a single fragment
package com.myproject.core.services.impl;
import com.adobe.cq.dam.cfm.ContentFragment;
import com.adobe.cq.dam.cfm.ContentFragmentException;
import com.adobe.cq.dam.cfm.FragmentTemplate;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public ContentFragment createFragment(
ResourceResolver resolver,
String modelPath,
String folderPath,
String title) throws ContentFragmentException {
// 1. Resolve the CF model
Resource modelResource = resolver.getResource(modelPath);
if (modelResource == null) {
throw new ContentFragmentException("Model not found: " + modelPath);
}
// 2. Adapt to FragmentTemplate
FragmentTemplate template = modelResource.adaptTo(FragmentTemplate.class);
if (template == null) {
throw new ContentFragmentException("Cannot adapt model to FragmentTemplate: " + modelPath);
}
// 3. Resolve the target folder
Resource folder = resolver.getResource(folderPath);
if (folder == null) {
throw new ContentFragmentException("Folder not found: " + folderPath);
}
// 4. Generate a unique node name from the title
String nodeName = ResourceUtil.createUniqueChildName(folder, title);
// 5. Create the fragment
ContentFragment fragment = template.createFragment(folder, nodeName, title);
LOG.info("Created Content Fragment: {} ({})", title, fragment.adaptTo(Resource.class).getPath());
return fragment;
}
Setting field values
/**
* Set a text field (single-line or multi-line).
*/
public void setText(ContentFragment fragment, String fieldName, String value, String mimeType) {
fragment.getElement(fieldName).setContent(value, mimeType);
}
/**
* Set a typed value (number, boolean, date, etc.).
*/
public void setTypedValue(ContentFragment fragment, String fieldName, Object value) {
FragmentData data = fragment.getElement(fieldName).getValue();
data.setValue(value);
fragment.getElement(fieldName).setValue(data);
}
// Usage:
setText(fragment, "title", "My Article", "text/plain");
setText(fragment, "body", "<p>Rich text content</p>", "text/html");
setTypedValue(fragment, "rating", 5L);
setTypedValue(fragment, "featured", true);
setTypedValue(fragment, "publishDate", Calendar.getInstance());
Setting fragment references
/**
* Set a fragment reference field (single or multi-valued).
*/
public void setFragmentReferences(ContentFragment fragment, String fieldName,
List<ContentFragment> references) {
List<ContentFragment> validRefs = references.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (validRefs.isEmpty()) {
return;
}
String[] paths = validRefs.stream()
.map(ref -> ref.adaptTo(Resource.class).getPath())
.toArray(String[]::new);
FragmentData data = fragment.getElement(fieldName).getValue();
data.setValue(paths);
fragment.getElement(fieldName).setValue(data);
}
Complete creation example
public void createArticleWithRelated(ResourceResolver resolver) throws Exception {
String modelPath = "/conf/myproject/settings/dam/cfm/models/article";
String authorModelPath = "/conf/myproject/settings/dam/cfm/models/author";
String folderPath = "/content/dam/myproject/articles";
// 1. Create the author fragment
ContentFragment author = createFragment(resolver, authorModelPath,
"/content/dam/myproject/authors", "Jane Doe");
setText(author, "name", "Jane Doe", "text/plain");
setText(author, "bio", "Senior tech writer with 10 years of experience.", "text/plain");
// 2. Create the main article
ContentFragment article = createFragment(resolver, modelPath,
folderPath, "Getting Started with Content Fragments");
setText(article, "title", "Getting Started with Content Fragments", "text/plain");
setText(article, "body",
"<p>Content Fragments are a powerful way to manage structured content...</p>",
"text/html");
setTypedValue(article, "publishDate", Calendar.getInstance());
setTypedValue(article, "featured", true);
// 3. Link author to article
setFragmentReferences(article, "author", List.of(author));
// 4. Create related articles
List<ContentFragment> relatedArticles = new ArrayList<>();
for (String relatedTitle : List.of("CF Models Deep Dive", "GraphQL Queries")) {
ContentFragment related = createFragment(resolver, modelPath,
folderPath, relatedTitle);
setText(related, "title", relatedTitle, "text/plain");
relatedArticles.add(related);
}
// 5. Link related articles
setFragmentReferences(article, "relatedArticles", relatedArticles);
// 6. Commit all changes
resolver.commit();
}
Modifying Existing Fragments
Updating field values
Resource cfResource = resolver.getResource("/content/dam/myproject/articles/my-article");
ContentFragment fragment = cfResource.adaptTo(ContentFragment.class);
// Update a text field
fragment.getElement("title").setContent("Updated Title", "text/plain");
// Update a typed field
FragmentData ratingData = fragment.getElement("rating").getValue();
ratingData.setValue(4L);
fragment.getElement("rating").setValue(ratingData);
// Don't forget to commit
resolver.commit();
Creating and editing variations
ContentElement bodyElement = fragment.getElement("body");
// Create a new variation
ContentVariation socialVariation = bodyElement.createVariation("social", "Social Media", "Short version for social");
// Set variation content
socialVariation.setContent("Check out our latest article on Content Fragments!", "text/plain");
// Update an existing variation
ContentVariation existingVariation = bodyElement.getVariation("summary");
if (existingVariation != null) {
existingVariation.setContent("Updated summary text", "text/plain");
}
resolver.commit();
Deleting fragments
Resource cfResource = resolver.getResource("/content/dam/myproject/articles/old-article");
if (cfResource != null) {
resolver.delete(cfResource);
resolver.commit();
}
Content Fragment in HTL Components
You can render Content Fragments in HTL components using either the built-in Content Fragment component or a custom Sling Model.
Using the Core Component
The AEM Core Content Fragment component renders Content Fragments out of the box. Add it to your template policy and configure via dialog.
Custom HTL rendering
@Model(
adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class ArticleCardModel {
@ValueMapValue
private String fragmentPath;
@SlingObject
private ResourceResolver resolver;
private ContentFragment fragment;
@PostConstruct
protected void init() {
if (fragmentPath != null) {
Resource cfResource = resolver.getResource(fragmentPath);
if (cfResource != null) {
fragment = cfResource.adaptTo(ContentFragment.class);
}
}
}
public String getTitle() {
return getElementContent("title");
}
public String getSummary() {
return getElementContent("summary");
}
public String getImagePath() {
return getElementContent("featuredImage");
}
public boolean isEmpty() {
return fragment == null;
}
private String getElementContent(String name) {
if (fragment == null || !fragment.hasElement(name)) {
return null;
}
return fragment.getElement(name).getContent();
}
}
<sly data-sly-use.model="com.myproject.core.models.ArticleCardModel"/>
<div data-sly-test="${!model.empty}" class="article-card">
<img data-sly-test="${model.imagePath}" src="${model.imagePath}" alt="${model.title}"/>
<h3>${model.title}</h3>
<p>${model.summary}</p>
</div>
<div data-sly-test="${model.empty}" class="cq-placeholder">
Select a Content Fragment
</div>
Querying Content Fragments
QueryBuilder
Find Content Fragments of a specific model:
Map<String, String> predicates = new HashMap<>();
predicates.put("path", "/content/dam/myproject");
predicates.put("type", "dam:Asset");
predicates.put("1_property", "jcr:content/data/cq:model");
predicates.put("1_property.value", "/conf/myproject/settings/dam/cfm/models/article");
predicates.put("2_property", "jcr:content/data/master/featured");
predicates.put("2_property.value", "true");
predicates.put("orderby", "@jcr:content/data/master/publishDate");
predicates.put("orderby.sort", "desc");
predicates.put("p.limit", "10");
Query query = queryBuilder.createQuery(PredicateGroup.create(predicates), session);
SearchResult result = query.getResult();
List<ContentFragment> fragments = new ArrayList<>();
for (Hit hit : result.getHits()) {
Resource resource = hit.getResource();
ContentFragment cf = resource.adaptTo(ContentFragment.class);
if (cf != null) {
fragments.add(cf);
}
}
JCR-SQL2
String sql2 = "SELECT * FROM [dam:Asset] AS a "
+ "WHERE ISDESCENDANTNODE(a, '/content/dam/myproject') "
+ "AND a.[jcr:content/data/cq:model] = '/conf/myproject/settings/dam/cfm/models/article' "
+ "AND CONTAINS(a.[jcr:content/data/master/title], 'AEM') "
+ "ORDER BY a.[jcr:content/data/master/publishDate] DESC";
Session session = resolver.adaptTo(Session.class);
QueryManager qm = session.getWorkspace().getQueryManager();
javax.jcr.query.Query query = qm.createQuery(sql2, javax.jcr.query.Query.JCR_SQL2);
query.setLimit(20);
QueryResult result = query.execute();
NodeIterator nodes = result.getNodes();
while (nodes.hasNext()) {
Node node = nodes.nextNode();
Resource resource = resolver.getResource(node.getPath());
ContentFragment cf = resource.adaptTo(ContentFragment.class);
// ... process fragment ...
}
GraphQL (for external consumers)
Content Fragments are the primary data source for AEM's built-in GraphQL API. See the dedicated Headless GraphQL page for endpoint setup, queries, and configuration.
Example persisted query:
# Persisted query: /content/cq:graphql/myproject/endpoint/articles
{
articleList(
filter: {
featured: { _expressions: [{ value: true }] }
}
_sort: "publishDate"
_limit: 10
) {
items {
_path
title
body { html }
publishDate
featuredImage { ... on ImageRef { _path } }
author { name bio }
}
}
}
Headless end-to-end: from model to frontend
This walks the full path a headless consumer takes: enable the model, save a persisted query, then fetch it from a JavaScript app. Persisted queries are strongly preferred over ad-hoc POST queries in production because they are cacheable by the Dispatcher and CDN and they keep the query server-side.
1. Enable the model and GraphQL endpoint
Enabling a model generates its GraphQL schema (e.g. an Article model creates the articleByPath and
articleList entry points). Create a project GraphQL endpoint so queries resolve at
/content/cq:graphql/myproject/endpoint.json (see
AEM GraphQL API).
2. Save a persisted query
Persist the query above with the AEM GraphQL CLI or a PUT to the persistence endpoint, giving it a
short name (featured-articles):
# Local SDK / dev only -- real environments authenticate with a scoped token, not admin:admin.
curl -u admin:admin -X PUT \
-H "Content-Type: application/json" \
--data-binary @featured-articles.json \
"http://localhost:4502/graphql/persist.json/myproject/featured-articles"
The persisted query is now served (and cached) at:
GET /graphql/execute.json/myproject/featured-articles
Parameterize with variables by appending ;name=value segments to the path
(.../featured-articles;limit=5).
3. Fetch it from a frontend
const AEM_HOST = process.env.AEM_PUBLISH_HOST; // e.g. https://publish-p123.adobeaemcloud.com
export async function getFeaturedArticles(limit = 10) {
const res = await fetch(
`${AEM_HOST}/graphql/execute.json/myproject/featured-articles;limit=${limit}`,
{ headers: { Accept: "application/json" } }
);
if (!res.ok) {
throw new Error(`AEM GraphQL request failed: ${res.status}`);
}
const json = await res.json();
return json.data.articleList.items;
}
import { useEffect, useState } from "react";
import { getFeaturedArticles } from "../lib/aem";
export function FeaturedArticles() {
const [articles, setArticles] = useState([]);
useEffect(() => {
getFeaturedArticles(5).then(setArticles).catch(console.error);
}, []);
return (
<ul>
{articles.map((a) => (
<li key={a._path}>
<h3>{a.title}</h3>
{/* body.html is sanitized AEM markup -- still review your XSS policy */}
<div dangerouslySetInnerHTML={{ __html: a.body.html }} />
</li>
))}
</ul>
);
}
4. CORS, referrer, and caching
A browser app on another origin needs AEM to allow it:
- CORS: configure the Cross-Origin Resource Sharing policy (
alloworigin/alloworiginregexp) for your frontend origin. - Referrer filter: allow the origin in the Apache Sling Referrer Filter for non-GET methods.
- Caching: persisted queries respond with cache headers and are cacheable at the Dispatcher/CDN; ad-hoc POST queries are not. Keep production reads on persisted queries.
See Headless GraphQL for the endpoint, CORS, and referrer configuration in detail.
Content Fragment Folder Structure
A well-organized DAM folder structure makes Content Fragments easier to manage, query, and maintain permissions on:
/content/dam/myproject/
├── articles/ # Article fragments
│ ├── 2025/ # Organised by year (optional)
│ │ ├── getting-started
│ │ └── advanced-topics
│ └── 2026/
├── authors/ # Author fragments
│ ├── jane-doe
│ └── john-smith
├── categories/ # Category fragments (if using CF-based taxonomy)
├── config/ # Configuration fragments (feature flags, settings)
└── shared/ # Shared fragments (CTAs, disclaimers, footers)
Creating folder structure programmatically
public Resource ensureFolder(ResourceResolver resolver, String path) throws PersistenceException {
Resource existing = resolver.getResource(path);
if (existing != null) {
return existing;
}
// Recursively create parent folders
String parentPath = path.substring(0, path.lastIndexOf('/'));
Resource parent = ensureFolder(resolver, parentPath);
// Create the folder as a sling:OrderedFolder (standard for DAM)
Map<String, Object> props = new HashMap<>();
props.put("jcr:primaryType", "sling:OrderedFolder");
props.put("jcr:title", path.substring(path.lastIndexOf('/') + 1));
return resolver.create(parent, path.substring(path.lastIndexOf('/') + 1), props);
}
Content Fragment Bulk Operations
Bulk update with Groovy Console
import com.adobe.cq.dam.cfm.ContentFragment
def modelPath = "/conf/myproject/settings/dam/cfm/models/article"
def targetFolder = "/content/dam/myproject/articles"
def count = 0
getResource(targetFolder).listChildren().each { child ->
def cf = child.adaptTo(ContentFragment.class)
if (cf != null) {
def model = child.getChild("jcr:content/data")?.valueMap?.get("cq:model", "")
if (model == modelPath) {
// Update a field
cf.getElement("category").setContent("technology", "text/plain")
count++
}
}
}
if (count > 0) {
resourceResolver.commit()
}
println "Updated ${count} fragments"
Bulk export to JSON
public List<Map<String, Object>> exportFragments(ResourceResolver resolver,
String folderPath, String modelPath) {
List<Map<String, Object>> results = new ArrayList<>();
Resource folder = resolver.getResource(folderPath);
if (folder == null) return results;
for (Resource child : folder.getChildren()) {
ContentFragment cf = child.adaptTo(ContentFragment.class);
if (cf == null) continue;
// Check model match
Resource dataNode = child.getChild("jcr:content/data");
if (dataNode == null) continue;
String cfModel = dataNode.getValueMap().get("cq:model", "");
if (!modelPath.equals(cfModel)) continue;
// Build export map
Map<String, Object> entry = new LinkedHashMap<>();
entry.put("path", child.getPath());
entry.put("title", cf.getTitle());
Iterator<ContentElement> elements = cf.getElements();
while (elements.hasNext()) {
ContentElement element = elements.next();
entry.put(element.getName(), element.getContent());
}
results.add(entry);
}
return results;
}
AEMaaCS Considerations
| Topic | Details |
|---|---|
| Content Fragment API | Same API as AEM 6.5; the com.adobe.cq.dam.cfm package is fully available |
| GraphQL | Built-in and enabled by default on AEMaaCS; requires Dispatcher configuration for caching |
| Persisted queries | Preferred over ad-hoc queries for production use; cacheable by Dispatcher/CDN |
| Assets HTTP API | REST API for CRUD operations on fragments without Java code |
| CF Console | AEMaaCS has a dedicated Content Fragments Console at /ui#/aem/cf/admin/ |
| OpenAPI | AEMaaCS supports the Content Fragments OpenAPI for headless delivery |
| Bulk imports | Use the Content Fragment Migration tool or the Assets HTTP API for large imports |
Assets HTTP API (REST)
For external systems that need to create or read fragments without Java. See the Content Fragments Support in the Assets HTTP API reference (AEMaaCS):
# Local SDK / dev only -- real environments use a scoped service account, not admin:admin.
# Read a fragment
curl -u admin:admin \
"http://localhost:4502/api/assets/myproject/articles/my-article.json"
# Create a fragment
curl -u admin:admin \
-X POST \
-H "Content-Type: application/json" \
-d '{
"properties": {
"cq:model": "/conf/myproject/settings/dam/cfm/models/article",
"elements": {
"title": { "value": "New Article" },
"body": { "value": "<p>Content here</p>", "contentType": "text/html" }
}
}
}' \
"http://localhost:4502/api/assets/myproject/articles/new-article"
Best Practices
Model design
- Keep models focused - one model per content type (Article, Author, FAQ). Avoid catch-all models with dozens of fields
- Use fragment references to link related content rather than duplicating data
- Name fields consistently - use camelCase, match your API contract (GraphQL field names derive from model field names)
- Add descriptions to fields - they appear as help text in the author UI
- Version your models carefully - adding fields is safe; renaming or removing fields breaks existing fragments
Authoring workflow
- Organise fragments in logical folders by type, year, or locale
- Use variations for channel-specific content (web, email, social) rather than creating separate fragments
- Tag fragments to enable cross-model discovery and filtering
- Use associated content to link DAM assets that belong to a fragment
Performance
- Query by model path (
jcr:content/data/cq:model) as the primary filter to limit results - Create Oak indexes for frequently queried fragment properties
- Cache fragment data in Sling Models with
@PostConstructrather than reading on every getter call - Use persisted GraphQL queries on Publish - they are cacheable by Dispatcher and CDN
The model reference (cq:model) lives on the jcr:content/data node -- a relative path two levels
deep. A type="property" index cannot index that (its propertyNames is a single property name,
and Oak only transforms the single jcr:content -> parent hop), so use a Lucene index with an
indexRule on dam:Asset, deployed as a content package
(ui.apps/.../_oak_index/cfModel-custom-1/.content.xml):
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
xmlns:dam="http://www.day.com/dam/1.0"
xmlns:oak="http://jackrabbit.apache.org/oak/ns/1.0"
jcr:primaryType="oak:QueryIndexDefinition"
type="lucene"
async="[async]"
compatVersion="{Long}2"
evaluatePathRestrictions="{Boolean}true">
<indexRules jcr:primaryType="nt:unstructured">
<dam:Asset jcr:primaryType="nt:unstructured">
<properties jcr:primaryType="nt:unstructured">
<cqModel
jcr:primaryType="nt:unstructured"
name="jcr:content/data/cq:model"
propertyIndex="{Boolean}true"/>
</properties>
</dam:Asset>
</indexRules>
</jcr:root>
For full-text search across fragment fields, add analyzed="{Boolean}true" properties to the same
index. See Adobe's Content Search and Indexing
documentation for AEMaaCS index deployment (/oak:index via ui.apps, using the -custom-N naming convention).
Common pitfalls
| Pitfall | Solution |
|---|---|
Fragment returns null when adapted | The resource must be a dam:Asset with a valid CF model reference |
getElement() returns null | The field name must match the model exactly (case-sensitive) |
| Rich text displays raw HTML | Use context='html' in HTL: ${model.body @ context='html'} |
| Fragment references not resolving | References are stored as paths; if the target is moved, the reference breaks |
| Slow fragment queries | Add an Oak index for the cq:model property and your custom filter properties |
| Variation content is empty | Variations inherit from master; only fields explicitly set on a variation are stored |
| CORS errors on GraphQL | Configure CORS and referrer filters; see Headless GraphQL |
| Infinite loop / stack overflow resolving references | Fragment A references B which references A. GraphQL limits nesting depth, but custom Java traversal must guard against cycles -- track visited paths in a Set and stop on revisit |
| GraphQL field missing after model change | The schema is regenerated when a model is created/updated/deleted; a disabled model produces no schema. Re-enable and republish the model |
See also
- Headless GraphQL - GraphQL endpoint setup, queries, CORS
- Modify and Query the JCR - QueryBuilder and JCR-SQL2
- JCR Node Operations - low-level JCR CRUD
- Replication and Activation - publishing fragments
- Multi-Site Manager - language copies and live copies
- Sling Models - reading fragment data in components
- HTL Templates - rendering fragment data
- Architecture - content model and resource resolution
- Component Dialogs - Content Fragment picker field
- Experience Fragments - XF vs CF comparison
- Tags and Taxonomies - tagging fragments
- AEM as a Cloud Service - Content Distribution and migration
- Groovy Console - bulk fragment operations