Tags: web-dev reference

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>    → scriptString

Override 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

ThingRequired?
sling:resourceType on contentYes — no type, no renderer
A .content.xml for a componentYes — to be a component
A dialogNo — a component can have no fields
A Sling ModelNo — properties works alone
defaultInjectionStrategy=OPTIONALNo, but always set it
sling:resourceSuperTypeNo — only for inheritance
A servletNo — pages need none
Explicit @ context in HTLNo — automatic by position
DispatcherYes in any real deployment
ui.frontendNo — 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>