Content Fragments
15 – Content Fragments
Target: 3–4 years experienced AEM Developer Syllabus points covered (24 partly, 25 in full): Point 24 — "CF and XF, and the difference between them." (Content Fragments here; Experience Fragments and the full comparison in file 16.) Point 25 — "How do you read a CF in a model class?" Project domain: a global energy technology company's marketing site.
Before we start — the sentence that separates CF from XF
Your syllabus asks for the difference between Content Fragments and Experience Fragments, and there is one sentence that gets you most of the way:
A Content Fragment is content without presentation. An Experience Fragment is presentation with content.
Everything else follows. A Content Fragment holds what you want to say — a product name, a specification, a description — with no opinion about how it looks. An Experience Fragment holds a piece of a page — actual components, laid out and styled.
Which means they answer different questions:
- "I need this same information on the website, in the mobile app, and in a partner's system." → Content Fragment
- "I need this same banner on twenty pages." → Experience Fragment
Get that distinction crisp and the rest of both files is detail.
One thing worth flagging early, because it surprises people and comes up: Content Fragments are stored in the DAM, under /content/dam, as assets. They are not pages and they are not under /content. That trips candidates up, and knowing it is a small proof you have actually worked with them.
1. Introduction
1.1 The problem Content Fragments solve
Our energy site has a product page for each transformer, with technical specifications — voltage range, cooling type, standards compliance, weight.
Now the mobile app needs the same specifications. And a partner integration wants them as JSON. And the specifications also appear in a comparison table on a different page.
Without Content Fragments, those specifications live inside a component on the product page — as dialog values on a jcr:content node somewhere. Which means they are presentation-bound. The mobile app cannot easily consume them, because they are entangled with the page that renders them.
A Content Fragment separates the content from anywhere it happens to be displayed. The specification exists as a piece of structured content in its own right. The web page renders it. The mobile app fetches it as JSON. The comparison table references it. One source, many consumers.
That is what "channel-agnostic" and "headless" actually mean in practice, and it is the framing to use.
1.2 Where they live, and why that matters
Content Fragments are assets in the DAM. Not pages, not under /content/<site>.
Why that's the right design: a fragment is a piece of content that belongs to no particular page. Putting it under a site's content tree would imply it belongs to that site. The DAM is where reusable, non-page content lives — alongside images and documents, which have exactly the same property.
Two practical consequences:
DAM permissions apply. Authors who can create fragments need DAM access, not just site access (file 13). And on publish, anonymous needs read on the fragment path or a component renders nothing.
They're activated like assets. Publishing a page that references a fragment does not publish the fragment. That is a genuinely common "works on author, blank on publish" cause, and it's the same family of problem as file 13's.
1.3 A real project example to adapt
"We use Content Fragments for product specifications and technical articles — anything that needs to exist independently of a page. The specifications for a transformer are a fragment, modelled with typed fields for voltage range, cooling type and standards compliance, and they're consumed three ways: rendered on the product page by a component, pulled into a comparison table on a different page, and delivered to the mobile app as JSON through GraphQL persisted queries.
The reason for persisted queries rather than ad-hoc GraphQL is caching — a persisted query is a GET on a stable path, so the dispatcher and CDN cache it like any other response. An ad-hoc POST query isn't cacheable at all, which for an app hitting it on every launch would have meant every request reaching publish."
That covers the use case, the model, the three consumers, and the caching decision — which is the architecturally interesting part.
2. Core Concepts
2.1 Content Fragment Models
A model defines the structure. It is the schema — what fields a fragment of this kind has, and what type each one is.
Where models live:
Note that's /conf — the same place as templates and policies from file 03. Models are configuration, per-site, and they ship in ui.content as mutable content.
The field types available:
Two worth calling out:
Fragment reference is what lets you compose fragments — a product fragment referencing a manufacturer fragment, for instance. That's how you avoid duplicating shared data.
Multi line text has a content type, which matters for rendering. It can be plain text, rich text (HTML) or markdown, and your code has to know which — section 2.6.
A model must be enabled before fragments can be created from it. A disabled model simply doesn't appear in the create dialog, which is the same shape as the status="draft" problem with templates in file 03.
2.2 The repository structure of a fragment
Worth knowing, because it explains both the API and a shortcut people take.
Three things to notice:
cq:model points at the Content Fragment Model. That's the link between fragment and schema.
master is a variation, not a special container — it's simply the default one. Other variations sit beside it.
Element values live under a variation, which is why the API has both element and variation concepts.
2.3 Variations
A variation is an alternative version of the fragment's content, for a different channel or context.
The canonical example: a description that's three paragraphs on the website, one sentence for a social media card, and a two-line summary for a listing. Same fragment, same underlying content, different renderings of the text itself — not different styling.
That last distinction matters. A variation is not "the same content styled differently" — that would be presentation, and presentation isn't a Content Fragment's job. A variation is genuinely different wording for a different context.
master is the default. If you ask for a variation that doesn't exist, you fall back to master.
A caution worth raising: variations are easy to over-use. Every variation is content someone has to write and maintain, and they drift. If the difference is purely how it's displayed, that belongs in the rendering, not in a variation.
2.4 Reading a Content Fragment in a Sling Model (syllabus point 25)
Your syllabus asks this directly. Here's the proper answer.
The API is com.adobe.cq.dam.cfm.ContentFragment, and you get there by adapting the fragment's resource:
Note you adapt the asset node itself — /content/dam/.../transformer-tx-4000 — not its jcr:content.
And as always, adaptTo can return null (file 05). If the path is wrong, or the resource isn't a Content Fragment, you get null rather than an exception.
The three classes to know:
Reading a field:
Reading it typed, which matters for numbers, booleans and dates:
Reading a variation:
Iterating the elements, when you don't know the model in advance:
Fragment metadata:
The full worked example is in section 8, but the interview answer is:
"I get the fragment's resource by path and adapt it to
ContentFragment— and I adapt the asset node itself, not itsjcr:content. ThengetElement(name)gives me aContentElement, andgetContent()gives me the value, orgetValue()gives me aFragmentDataif I need the type. Variations come fromelement.getVariation(name), falling back to master when it doesn't exist.Two things I'm careful about.
adaptToreturns null if the path is wrong or the resource isn't a fragment, so it's always null-checked — and I'd do that work in@PostConstructrather than in a getter, because HTL can call getters repeatedly.And for multi-line text I check the content type, because a field can be plain text, rich text or markdown. If it's
text/htmlI render it withcontext='html'in HTL; if I use the default escaping the author sees literal tags on the page."
2.5 The shortcut, and why to avoid it
Because the repository structure is knowable, you can read a fragment with plain Sling Model injections:
Why not:
It's fragile. You're depending on the internal storage layout, which the API exists to insulate you from.
You lose the type information. No content type, so you can't tell rich text from plain.
Variations become manual. You'd hand-code the fallback to master.
And it breaks if the model changes shape.
Being able to say "you can, but I wouldn't, and here's why" is a better answer than either just knowing the API or just knowing the shortcut.
2.6 Content types on multi-line text — the rendering trap
A multi-line text element carries a content type:
Why this matters connects directly to file 08. If the element is text/html and you render it with HTL's default escaping, the visitor sees literal <p> and <strong> tags. If it's text/plain and you render it with context='html', you've created an injection point.
So the model should expose the content type, and the HTL should branch on it — or the model should normalise it. Section 8 shows both.
This is the same bug as story 1 in file 11, arriving from a different direction, which is worth noticing: rich text is rich text wherever it comes from.
2.7 GraphQL — the headless delivery story
This is what makes Content Fragments architecturally interesting, and it's where the interview usually goes next.
Content Fragment Models automatically generate a GraphQL schema. Create a product model and you get query types for it — fetch one by path, or list them with filters. You write no schema code.
Two ways to query, and the difference is the whole point:
Why persisted queries are the production answer — and this is the same reasoning as files 01, 04 and 07:
A POST to a single endpoint isn't cacheable. Every request reaches publish. A persisted query is a GET on a stable, path-based URL, so the dispatcher caches it as a file and the CDN caches it at the edge.
For a mobile app fetching product data on every launch, that's the difference between every launch hitting publish and almost none of them doing so.
A second benefit worth mentioning: persisted queries mean the client can't send arbitrary queries. An ad-hoc GraphQL endpoint lets a caller construct expensive or over-broad queries; persisted queries constrain them to ones you've approved. That's a security and performance property, not just a caching one.
The interview answer:
"Content Fragment Models generate a GraphQL schema automatically, so you get queries per model without writing any schema code.
The important decision is persisted queries versus ad-hoc. An ad-hoc query is a POST to the endpoint, which isn't cacheable — every request goes through to publish. A persisted query is stored server-side and called with a GET on a stable path, so the dispatcher and CDN cache it like any other response. For an app hitting it on every launch that's the difference between all the traffic reaching publish and almost none of it.
There's a second reason too: persisted queries mean callers can only run queries we've approved, rather than constructing arbitrary ones that might be expensive or expose more than intended."
2.8 When to use a Content Fragment — and when not to
Use one when:
- The content is consumed by more than one channel — web, app, partner system.
- It's structured — distinct typed fields, not free-form layout.
- It has its own lifecycle — created, reviewed and updated independently of any page.
- The same content appears in several different renderings.
Don't use one when:
- The content only ever appears in one place, in one way. A component dialog is simpler and authors find it easier.
- What you actually want is layout reuse — that's an Experience Fragment (file 16).
- You'd be putting HTML markup into a single multi-line field, which defeats the purpose.
That last one is worth stating, because it's a genuine anti-pattern: a fragment with one rich-text field containing a whole page's worth of markup is not structured content. It's a page in the wrong place, and none of the headless benefits apply because a consumer can't do anything with an opaque blob of HTML.
3. Internal Working
3.1 How a fragment reaches a page
graph TD A["Content Fragment Model<br/>/conf/.../dam/cfm/models/product"] -->|"defines the structure"| B["Content Fragment<br/>/content/dam/.../transformer-tx-4000"] B -->|"referenced by path"| C["A component on the page"] C -->|"adaptTo(ContentFragment.class)"| D["Sling Model reads elements"] D --> E["HTL renders it"] B -->|"GraphQL persisted query"| F["Mobile app / partner<br/>gets JSON"]
The key structural point: the fragment is referenced by path, from a component. It has no knowledge of where it's used, which is exactly what makes it reusable — and also why deleting or moving one can silently break pages that reference it.
3.2 Why a fragment renders on author but not publish
A recurring theme, arriving here from a new direction:
graph TD A["Fragment renders on author,<br/>blank on publish"] --> B{"Was the FRAGMENT activated?"} B -->|No| C["Publishing the PAGE does NOT<br/>publish the fragment.<br/>They are separate assets."] B -->|Yes| D{"Can anonymous read<br/>/content/dam/...?"} D -->|No| E["DAM permissions — file 13"] D -->|Yes| F{"Is the reference path correct<br/>on this environment?"} F -->|No| G["adaptTo returned null,<br/>and the model rendered nothing"]
The first branch is the common one and it's worth stating clearly: a Content Fragment is a separate asset with its own publication state. Activating the page that references it does not activate it. That's not a bug — the fragment might be referenced by fifty pages — but it surprises people every time.
3.3 Model changes and existing fragments
A practical concern that gets asked:
The point to make: a Content Fragment Model is much harder to change than a component dialog, because fragments are content that already exists and there's no dialog-save to migrate it. So model design deserves more care up front than a dialog does.
8. Coding Examples
8.1 Reading a fragment properly
The six decisions to be able to defend:
Everything in @PostConstruct — each element read is a repository operation, and HTL calls getters repeatedly.
Adapt the asset node, not jcr:content.
Null-check adaptTo, and log when the fragment is missing — because silent absence is correct behaviour and useless for debugging.
Variation with a fallback to master, treated as normal rather than an error.
The content type is read and exposed, so the HTL can escape correctly rather than guessing.
Getters never return null (file 05), and isReady() guards the render.
8.2 The HTL
8.3 The dialog
8.4 Iterating elements when the model isn't known in advance
Useful for a generic renderer — a specifications table that shows whatever fields the fragment has.
Note the bound. Same rule as file 05 — anything building a list gets a ceiling, so a misconfigured or unexpectedly large fragment degrades rather than causing a problem.
And element.getTitle() gives the human-readable label from the model, which is what you want in a table header rather than the field name.
8.5 A persisted GraphQL query
The query, stored server-side:
Called by the app as a GET:
Why this is the production answer:
The variables are part of the path, so different products produce different cacheable URLs — the same principle as selectors and suffixes from file 01.
Note also description { html plaintext } — GraphQL exposes both renderings of a multi-line field, so the consumer picks what it needs. That's the structured-content benefit in practice: the app can take plaintext for a summary and html where it renders markup.
9. Common Mistakes
10. Best Practices
On modelling. Design the model carefully up front, because it's much harder to change than a dialog — fragments are content that already exists. Use typed fields rather than free text. Never model an entire article as one rich-text field.
On reading. Use the ContentFragment API, not raw property injection. All the work in @PostConstruct. Null-check everything and log when a referenced fragment is missing. Read and expose the content type rather than assuming.
On rendering. Prefer the Core Components Content Fragment component, proxied, unless you genuinely need a different rendering. Constrain the fragment pathfield with rootPath. Guard the render with isReady().
On headless. Persisted queries in production, always. Plan invalidation at the same time — cached query responses go stale when fragments change. Keep the ad-hoc endpoint off on publish.
On operations. Remember fragments publish separately from pages. Check the References panel before changing a widely-used fragment. Grant anonymous read on fragment paths for publish.
11. Debugging Tips
"The fragment renders on author but not publish" — check in this order:
- Was the fragment activated? Publishing the page doesn't publish it. This is the most common cause by a wide margin.
- Can
anonymousread the DAM path? File 13. - Is the path the same on both environments? If not,
adaptToreturned null and the model rendered nothing.
"The component renders nothing and there's no error." That's the model working as designed — adaptTo returned null and isReady() is false. Which is exactly why the warn-level log line in section 8.1 matters: it turns silence into a signal.
"Rich text shows literal tags." The element is text/html and the HTL is using default escaping. Check getContentType() and branch (file 08).
"An element returns empty." Either the name doesn't match the model, or the value genuinely isn't set. Look at the fragment in CRXDE under jcr:content/data/master — that tells you immediately whether the data is absent or the read is wrong. Same split-the-problem-in-half move as everywhere else.
"A GraphQL query returns nothing on publish." Usually permissions — anonymous can't read the fragment paths — or the fragments aren't published.
12. Performance Notes
Read once, in @PostConstruct. Each element access is a repository operation, and HTL calls getters repeatedly.
Bound anything iterative. Element iteration, fragment lists, reference chains — a ceiling constant, as everywhere.
Fragment references compound. A fragment referencing another which references another means several reads per render, and in GraphQL a nested query can fan out considerably. That's a model-design decision as much as a code one.
Persisted queries are the headless performance story. Ad-hoc POST queries guarantee every request reaches publish; persisted queries are cached at the dispatcher and CDN. For an app fetching on launch, that's the whole difference.
And plan invalidation with the caching. Cached query responses derived from fragments go stale when fragments change, which is the same problem as the derived listing in file 02.
15. Comparison Tables
Content Fragment vs Experience Fragment (full treatment in file 16)
Field types
Ad-hoc vs persisted GraphQL
Reading a fragment: API vs raw injection
Model changes
Next topic
16-Experience-Fragments — the other half of your syllabus point 24: what an Experience Fragment is, how it differs from a Content Fragment in practice, the plain HTML export for third-party channels, and when to use each.
Topic 15 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.