Reference - AEM
Framework: Adobe Experience Manager — Sling · JCR · OSGi
Checked: 2026-08-16
A content repository with a REST framework on top. Learn four things — the repository, the resource, resolution, and the OSGi service model — and the rest of AEM is vocabulary. The platform’s shape is in Adobe Experience Manager; this is how you build in it.
§0 The one idea
Everything is a resource, and the framework’s whole job is turning a URL into a resource and a resource into a renderer.
URL → Resource → resourceType → script
Three layers, each an Apache project, each doing one thing:
AEM Adobe's product on top
─────────────────────────────────────────
Sling URL → resource → script
the web framework
─────────────────────────────────────────
OSGi (Felix) modules, services, config
the runtime
─────────────────────────────────────────
JCR (Oak) the content repository
everything is a node
vs a normal MVC framework: there is no route table, no controller registry, no ORM. Content declares its own type; the type selects the script. Adding a page type means adding a folder in /apps, not editing a central file — Routing and Resolution.
vs a normal deployment: code and content live in the same repository, as nodes. A component is a node. A template is a node. This is why AEM deployments are content packages rather than binaries alone.
1. The repository
Everything — content, code, config, users — is a node in one tree.
/content pages · assets · fragments
/apps YOUR components and templates
/libs Adobe's — never modify
/conf templates, policies, config
/var runtime, workflows
/home users and groups
/apps overlays /libs. Put a node at the matching path under /apps and yours wins. That’s the extension model for the entire product, and it’s why customisation survives upgrades.
Nodes and properties
A node has a primary type, optional mixins, child nodes, and properties.
node types you'll meet
nt:unstructured anything goes — the default
for component instances
cq:Page a page
cq:PageContent a page's content node
cq:Component a component definition
cq:Template a template
dam:Asset an asset
nt:file a file
On disk
Nodes serialise to XML. This is what a page actually is:
<!-- /content/site/en/products/.content.xml -->
<jcr:root jcr:primaryType="cq:Page">
<jcr:content
jcr:primaryType="cq:PageContent"
jcr:title="Products"
sling:resourceType="mysite/components/page">
<root sling:resourceType="…/responsivegrid">
<hero
sling:resourceType="mysite/components/hero"
title="Autumn range"/>
</root>
</jcr:content>
</jcr:root>The hero an author dragged on is a child node. What they typed is a property on it. Nothing else is happening.
2. Resources
A resource is Sling’s wrapper over a node — a path, a type, and a map of values.
Resource r = resolver.getResource("/content/site/en");
String type = r.getResourceType();
ValueMap vals = r.getValueMap();
String title = vals.get("jcr:title", String.class);
for (Resource child : r.getChildren()) { … }adaptTo — the conversion mechanism
The single most-used call in AEM code:
Page page = resource.adaptTo(Page.class);
Node node = resource.adaptTo(Node.class);
ValueMap vm = resource.adaptTo(ValueMap.class);
Hero hero = resource.adaptTo(Hero.class);One object, many views. adaptTo returns null when the adaptation isn’t available — the most common source of a null pointer in AEM code, and it is never a thrown exception.
3. Resolution — URL to renderer
The URL decomposes
/content/site/en/page.print.a4.html/extra?x=1
└──────── path ────────┘└ selectors ┘└ex┘└suf┘
path the resource
selectors print, a4 — free-form modifiers
extension html — picks the script
suffix /extra — extra path info,
available to the script
Selectors are the mechanism people miss. Same resource, different script, no new route:
page.html → page.html
page.print.html → print.html
page.model.json → the JSON export
Then the lookup
1 resolve the path to a resource
2 read sling:resourceType
"mysite/components/page"
3 look for a script under
/apps/mysite/components/page/
4 match on selectors + method + extension
print.html · page.html · GET.html
5 not found? fall back to the
sling:resourceSuperType chain
sling:resourceSuperType gives you inheritance. A component declaring a supertype inherits every script it doesn’t override — which is how you extend a Core Component without copying it.
4. HTL — the templating layer
AEM’s template language. Not a general-purpose language — its vocabulary is the Sling data model in attribute form, and it can’t express logic by design.
The smallest working form
<h1>${properties.title}</h1>Complete. Reads title off the current resource, escapes it for HTML text context, outputs it.
Expressions
${properties.subtitle}
${properties['jcr:title']} <!-- brackets when
the key has a
colon -->Globals
properties the current resource's props
resource the resource itself
currentPage the containing page
pageProperties that page's properties
wcmmode author or publish?
request the Sling request
component the component definition
Operators
${properties.count > 3}
${a && b}
${!hidden}
${a || 'fallback'}
${x ? 'yes' : 'no'}No arithmetic and no concatenation. If you want price * qty, that’s a getter on the model. The restriction is the design.
data-sly-use — bind
<!-- a Sling Model -->
<div data-sly-use.hero="com.mysite.models.Hero">
<h1>${hero.title}</h1>
</div>
<!-- another HTL file, for templates -->
<sly data-sly-use.lib="cards.html">
<!-- with parameters -->
<div data-sly-use.nav="${'com.mysite.Nav'
@ depth=3}">data-sly-test — conditionals
<p data-sly-test="${properties.subtitle}">
${properties.subtitle}
</p>Element and children removed when falsy. There is no else — store the result and negate:
<div data-sly-test.featured="${properties.featured}">
Featured
</div>
<div data-sly-test="${!featured}">Standard</div>data-sly-list — loops
<ul data-sly-list.item="${hero.links}">
<li>${item.label}</li>
</ul>Every list creates a companion object, <var>List:
itemList.index 0-based
itemList.count 1-based
itemList.first boolean
itemList.middle boolean
itemList.last boolean
itemList.odd boolean
itemList.even boolean
list versus repeat — the difference that catches everyone:
<ul data-sly-list.i="${items}"><li>${i}</li></ul>
→ <ul><li>a</li><li>b</li></ul>
<li data-sly-repeat.i="${items}">${i}</li>
→ <li>a</li><li>b</li>list repeats the children. repeat repeats the element.
<sly> — the element that disappears
<sly data-sly-test="${hero.hasLinks}">
<a href="#">One</a>
</sly>Removed from output, children kept. Use it whenever you need an HTL attribute but not a wrapper. data-sly-unwrap on a real element is equivalent; <sly> is preferred because the intent is visible.
Manipulating the element
<h1 data-sly-element="${model.headingTag}">…</h1>
<a data-sly-attribute.href="${hero.link}">Go</a>
<div data-sly-attribute="${hero.attrs}"></div>
<p data-sly-text="${hero.body}">placeholder</p>data-sly-text and ${} inside the element are equivalent — use data-sly-text when you want visible placeholder content while editing the file. data-sly-element is restricted to a safe tag list, so content can’t inject arbitrary elements.
Including
<!-- render a CONTENT node -->
<div data-sly-resource="${'header'}"></div>
<div data-sly-resource="${'par' @
resourceType='wcm/foundation/components/
responsivegrid'}"></div>
<!-- pull in a FILE -->
<div data-sly-include="partial.html"></div>data-sly-resource a content node → goes
through resolution, picks
its own renderer
data-sly-include a file → same resource,
more markup
data-sly-resource is how a page renders the components an author placed on it.
Templates
<template data-sly-template.card="${@ title, body}">
<div class="card">
<h3>${title}</h3>
<p>${body}</p>
</div>
</template>
<sly data-sly-call="${card @ title='Hi',
body='There'}"></sly>From another file:
<sly data-sly-use.lib="cards.html"
data-sly-call="${lib.card @ title='Hi'}"></sly>data-sly-set
<sly data-sly-set.name="${hero.firstName}"></sly>
<p>${name}</p>Avoids repeating a long expression. Not a computation — there’s nothing to compute with.
Display contexts — the escaping system
Context is chosen automatically by position:
<p>${text}</p> → text
<a href="${url}"> → uri
<div class="${cls}"> → attribute
<script>var x='${s}';</script> → scriptStringOverride with @ context:
<div>${richText @ context='html'}</div>
<div>${raw @ context='unsafe'}</div>text escape everything
html allow a safe HTML subset
attribute escape for an attribute value
uri validate + escape as a URL
number must be numeric
scriptString escape inside a JS string
styleString escape inside CSS
unsafe NO escaping
context='unsafe' is the only way to introduce XSS in HTL. Auditing is a grep — Common Vulnerabilities.
Expression options
${'Add to basket' @ i18n}
${'{0} of {1}' @ format=[shown, total]}
${date @ format='dd MMMM yyyy'}
${tags @ join=', '}
${url @ context='uri', prefix='https://'}5. Sling Models
Where logic lives. Annotation-driven POJOs that map repository data onto Java fields.
@Model(adaptables = Resource.class,
defaultInjectionStrategy = OPTIONAL)
public class Hero {
@ValueMapValue
private String title; // ← property
// "title"
@ValueMapValue
@Named("jcr:description")
private String description;
@ChildResource
private Resource links; // ← child node
@Self
private Resource resource;
@SlingObject
private ResourceResolver resolver;
@OSGiService
private ProductService products;
@PostConstruct
protected void init() { … }
public String getTitle() { return title; }
}@ValueMapValue a property on this node
@ChildResource a child node
@Self the adaptable itself
@SlingObject resolver, request, response
@OSGiService an injected service
@Named when the field name and the
property name differ
@PostConstruct runs after injection
defaultInjectionStrategy = OPTIONAL should almost always be set. The default is REQUIRED, and a single missing property then makes the whole model adapt to null — which surfaces as a blank component with no error.
Adaptables
// adapt from a Resource — no request context
@Model(adaptables = Resource.class)
// adapt from a request — needed for
// request-scoped things
@Model(adaptables = SlingHttpServletRequest.class)
// both
@Model(adaptables = {Resource.class,
SlingHttpServletRequest.class})Exporting a model as JSON
@Model(adaptables = SlingHttpServletRequest.class,
adapters = {Hero.class,
ComponentExporter.class},
resourceType = "mysite/components/hero")
@Exporter(name = "jackson", extension = "json")
public class HeroImpl implements Hero { … }This is what makes page.model.json work — the same component, serialised instead of rendered, which is how AEM’s hybrid headless mode works.
6. OSGi
The runtime. Everything is a bundle (a JAR with metadata), and bundles publish services.
@Component(service = ProductService.class,
immediate = true)
public class ProductServiceImpl
implements ProductService {
@Reference
private ResourceResolverFactory factory;
@Activate
protected void activate(Config config) {
this.endpoint = config.endpoint();
}
}Configuration
@ObjectClassDefinition(name = "My Product Service")
public @interface Config {
@AttributeDefinition(name = "API endpoint")
String endpoint() default "https://api…";
@AttributeDefinition(name = "Timeout (ms)")
int timeout() default 5000;
}@Component(service = ProductService.class)
@Designate(ocd = Config.class)
public class ProductServiceImpl { … }Config lives in the repository, per run mode:
/apps/mysite/osgiconfig/
config/ all environments
config.author/ author only
config.publish/ publish only
config.publish.prod/ publish, production
Run modes are how one codebase serves every environment. The most specific matching folder wins.
7. Servlets
For endpoints that aren’t pages.
// bind to a resource type — PREFERRED
@Component(service = Servlet.class)
@SlingServletResourceTypes(
resourceTypes = "mysite/components/hero",
selectors = "data",
extensions = "json",
methods = "GET")
public class HeroDataServlet
extends SlingSafeMethodsServlet {
@Override
protected void doGet(SlingHttpServletRequest req,
SlingHttpServletResponse res) { … }
}// bind to a path — AVOID
@SlingServletPaths("/bin/mysite/products")Prefer resource types over paths. A path-bound servlet bypasses resolution and access control, which is why /bin/ endpoints are a recurring finding on AEM security reviews.
The default POST servlet
Sling handles form posts with no code at all:
<form method="POST" action="/content/site/en/page">
<input name="./jcr:title" value="New title">
<input name="./count" value="5"
type="hidden">
<input name="./count@TypeHint" value="Long"
type="hidden">
</form>./property writes straight to the node. Powerful, and exactly why the Dispatcher must block POSTs to content paths on publish.
8. Dialogs
Granite UI, defined as nodes. Verbose, and the verbosity is representative.
<!-- _cq_dialog/.content.xml, trimmed -->
<items jcr:primaryType="nt:unstructured">
<title
sling:resourceType="granite/ui/components/
coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"/>
<body
sling:resourceType="granite/ui/components/
coral/foundation/form/textarea"
fieldLabel="Body"
name="./body"/>
<link
sling:resourceType="granite/ui/components/
coral/foundation/form/pathfield"
fieldLabel="Link"
rootPath="/content"
name="./link"/>
</items>name="./title" is the whole contract — it writes to the property that @ValueMapValue private String title reads.
Field types worth knowing: textfield, textarea, pathfield, select, checkbox, numberfield, datepicker, richtext, multifield.
9. Templates and policies
Editable templates live in /conf and are configured in the UI rather than in code.
/conf/mysite/settings/wcm/
templates/
landing-page/
structure/ locked, always present
initial/ copied into a new page
policies/ which components allowed
policies/ the policy definitions
STRUCTURE authors cannot remove it
INITIAL the starting content of a new page
POLICY which components may go in this
container, and their settings
The policy is the governance mechanism — it’s how a large authoring team gets consistency without a developer per page.
10. The Dispatcher
Apache module. Cache and security filter, and not optional.
# filter — default deny, then allow
/0001 { /type "deny" /url "*" }
/0100 { /type "allow" /url "/content/*" }
/0102 { /type "allow" /url "*.html" }
# block the obvious
/0craft { /type "deny" /url "/crx/*" }
/0syst { /type "deny" /url "/system/*" }
/0bin { /type "deny" /url "/bin/*" }
# cache
/rules {
/0000 { /glob "*" /type "allow" }
}
/statfileslevel "3"
statfileslevel controls how far up the tree an invalidation reaches. Too low and every publish flushes everything; too high and stale content survives. It’s the main tuning decision — Caching Strategies.
11. Project structure
The Maven archetype generates this, and knowing the modules tells you where anything lives:
core/ Java — models, servlets, services
ui.apps/ /apps — components, HTL, dialogs
ui.content/ /content — sample content, /conf
ui.config/ OSGi configuration
ui.frontend/ webpack — the CSS and JS build
dispatcher/ Dispatcher config
all/ the deployable package
ui.frontend is a normal front-end project — npm, webpack, whatever you like — that compiles into a clientlib in ui.apps. Front-end work in AEM is much more ordinary than its reputation suggests.
Clientlibs
<jcr:root jcr:primaryType="cq:ClientLibraryFolder"
categories="[mysite.site]"
embed="[mysite.dependencies]"
allowProxy="{Boolean}true"/><sly data-sly-use.clientlib=
"core/wcm/components/commons/v1/templates/
clientlib.html"
data-sly-call="${clientlib.css @
categories='mysite.site'}"/>Required versus optional
| Thing | Required? |
|---|---|
sling:resourceType on content | Yes — no type, no renderer |
A .content.xml for a component | Yes — to be a component |
| A dialog | No — a component can have no fields |
| A Sling Model | No — properties works alone |
defaultInjectionStrategy=OPTIONAL | No, but always set it |
sling:resourceSuperType | No — only for inheritance |
| A servlet | No — pages need none |
Explicit @ context in HTL | No — automatic by position |
| Dispatcher | Yes in any real deployment |
ui.frontend | No — but the alternative is worse |
The kitchen sink
Labelled as such — do not write templates like this. It exists to show the attributes coexisting:
<sly data-sly-use.hero="${'com.mysite.Hero'
@ depth=2}"
data-sly-use.lib="cards.html"
data-sly-set.heading="${hero.title ||
'Untitled'}">
<section data-sly-test.hasItems="${hero.items}"
data-sly-attribute.class="${hero.css}">
<h2 data-sly-element="${hero.headingTag}">
${heading}
</h2>
<div>${hero.body @ context='html'}</div>
<ul data-sly-list.item="${hero.items}">
<li class="${itemList.last ? 'last' : ''}">
<a data-sly-attribute.href="${item.url}">
${item.label}
</a>
</li>
</ul>
<sly data-sly-repeat.card="${hero.cards}"
data-sly-call="${lib.card
@ title=card.title}"></sly>
</section>
<p data-sly-test="${!hasItems}">
${'No items' @ i18n}
</p>
<div data-sly-resource="${'footer'}"></div>
</sly>A complete component, top to bottom
/apps/mysite/components/hero/.content.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
jcr:primaryType="cq:Component"
jcr:title="Hero"
componentGroup="My Site"/>_cq_dialog/.content.xml — trimmed
<items jcr:primaryType="nt:unstructured">
<title
sling:resourceType="granite/ui/components/
coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"/>
<body
sling:resourceType="granite/ui/components/
coral/foundation/form/textarea"
fieldLabel="Body"
name="./body"/>
</items>core/…/models/Hero.java
@Model(adaptables = Resource.class,
defaultInjectionStrategy = OPTIONAL)
public class Hero {
@ValueMapValue
private String title;
@ValueMapValue
private String body;
public String getTitle() { return title; }
public String getBody() { return body; }
public boolean isEmpty() {
return title == null && body == null;
}
}hero.html
<sly data-sly-use.hero="com.mysite.models.Hero"/>
<div class="hero" data-sly-test="${!hero.empty}">
<h2>${hero.title}</h2>
<div class="hero__body">
${hero.body @ context='html'}
</div>
</div>
<sly data-sly-test="${hero.empty && wcmmode.edit}">
<p>Configure this hero.</p>
</sly>The last block matters. wcmmode.edit shows placeholder text to authors only — a component that vanishes entirely when unconfigured is invisible in the editor, which is the standard authoring complaint.
The round trip
The whole model in eight lines:
DIALOG name="./title"
↓ author types "Autumn range"
JCR NODE title="Autumn range"
↓ @ValueMapValue
SLING MODEL String title
↓ ${hero.title}
HTL <h1>Autumn range</h1>
Related
- Adobe Experience Manager — the platform’s shape, deployment flavours, Edge Delivery
- Routing and Resolution — what Sling’s resolution is an instance of
- Caching Strategies — the Dispatcher, generalised
- Common Vulnerabilities — what HTL’s escaping and the Dispatcher filter defend
- Reference - Liquid — the templating comparison, for contrast