---
name: aem-component-development
description: Build and review Adobe Experience Manager Sites components (AEM as a Cloud Service first, 6.5 noted) — HTL templates with correct XSS contexts, Sling Models and exporters, Core Components proxies and delegation, Granite UI dialogs, editable templates, policies and the Style System. Use this whenever the user writes or debugs an HTL .html file, a Sling Model or *Impl.java with @Model, @ValueMapValue, @ChildResource or @PostConstruct, a _cq_dialog/.content.xml or _cq_design_dialog, a multifield, cq:editConfig or _cq_editConfig.xml, a proxy component with sling:resourceSuperType, a .model.json exporter, component groups or allowed components, i18n dictionaries, WCMMode placeholders, or Content/Experience Fragment components; asks "how do I extend the Core Image/Teaser component" or "why is my field empty in author". Also apply it when reviewing AEM component code, ui.apps content packages or pull requests touching /apps components.
metadata:
  technology: AEM
  type: development
---

# AEM Component Development

A component is a contract between authors, templates and code: the dialog is the authoring API, the Sling Model is the logic, HTL is a thin, escaped view, and policies decide what is allowed where. Start from Core Components and extend; build from scratch only when no Core Component fits the content model.

## 1. Decide first

| Need | Use | Notes |
|---|---|---|
| Standard element (title, text, image, teaser, list, tabs, carousel, embed) | **Proxy of a Core Component** | Never reference `core/wcm/components/*` directly from content |
| Core behaviour + a few extra fields | Proxy + extra dialog fields + **delegating Sling Model** | Keep upstream upgrades cheap |
| Genuinely new content structure | Custom component, own model interface + impl | Still reuse Core templates (placeholder, clientlib) |
| Container of other components | Extend Core Container/Tabs/Accordion | Own-built containers need `cq:isContainer` and careful editConfig |
| Reusable authored fragment (header, footer, promo) | **Experience Fragment** component | Variations per locale/channel |
| Structured, channel-neutral content | **Content Fragment** component or headless GraphQL | Model the data in CF Models, not in dialogs |
| Look-and-feel variants of one component | **Style System** (policy style groups) | Not a new component, not a dialog dropdown |

## 2. Proxy components and versioning

```xml
<!-- ui.apps/.../components/title/.content.xml -->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
    xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="cq:Component"
    jcr:title="Title" componentGroup="MySite - Content"
    sling:resourceSuperType="core/wcm/components/title/v3/title"/>
```

- Pin a **specific Core version** (`v2`, `v3`); upgrade deliberately, one component at a time, because markup and model output can change between versions.
- On AEM as a Cloud Service Core Components ship with the platform and update automatically; do not embed them in your package. On 6.5 you install and upgrade the Core Components package yourself — check the compatibility matrix.
- Version your own components the same way (`/apps/mysite/components/hero/v1/hero`) only if other sites consume them; otherwise keep a single proxy-style path.
- `componentGroup` controls the side panel; `.hidden` hides helper components. Allowed components are set on the **layout container policy**, not hard-coded.

## 3. Sling Models

```java
@Model(adaptables = SlingHttpServletRequest.class,
       adapters = {Hero.class, ComponentExporter.class},
       resourceType = HeroImpl.RESOURCE_TYPE,
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class HeroImpl implements Hero {
    static final String RESOURCE_TYPE = "mysite/components/hero";

    @ValueMapValue private String title;
    @ChildResource private List<Resource> links;
    @ScriptVariable private Page currentPage;
    @OSGiService private LinkResolver linkResolver;   // your own service
    @SlingObject private Resource resource;

    private List<HeroLink> heroLinks;

    @PostConstruct
    protected void init() {
        heroLinks = links == null ? List.of()
            : links.stream().map(r -> new HeroLink(r, linkResolver)).collect(Collectors.toList());
    }

    @Override public String getTitle() { return title; }
    @Override public List<HeroLink> getLinks() { return heroLinks; }
    @Override @JsonIgnore public boolean isEmpty() { return StringUtils.isBlank(title); }
    @Override public String getExportedType() { return RESOURCE_TYPE; }
}
```

- **Adapt from the request** (`SlingHttpServletRequest`) for component models: you get `currentPage`, `currentStyle`, WCMMode, i18n and request attributes. Resource-only models suit reusable helpers and background jobs.
- Use **injector-specific annotations** (`@ValueMapValue`, `@ChildResource`, `@OSGiService`, `@ScriptVariable`, `@Self`, `@RequestAttribute`); avoid bare `@Inject`, which walks every injector and hides which source you meant.
- `DefaultInjectionStrategy.OPTIONAL` at class level, then handle nulls — a freshly dropped component has no properties, and a failing required injection makes adaptation return null and the component render nothing.
- Heavy work belongs in `@PostConstruct` or lazily in getters, never in HTL expressions. Keep models free of side effects: they run on every render and every `.model.json` call.
- Expose an **interface** (in a public package) and keep `*Impl` internal; HTL and exporters depend on the interface.
- JSON: implement `ComponentExporter`, shape output with Jackson (`@JsonProperty`, `@JsonIgnore`, `@JsonInclude(NON_EMPTY)`); never serialise `Resource`, `Page` or `ValueMap` objects.

## 4. Extending a Core Component model (delegation)

```java
@Model(adaptables = SlingHttpServletRequest.class, adapters = Teaser.class,
       resourceType = "mysite/components/teaser", defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class MyTeaser implements Teaser {
    @Self @Via(type = ResourceSuperType.class) @Delegate(excludes = Excluded.class)
    private Teaser delegate;                       // Lombok @Delegate, or hand-write pass-throughs
    @ValueMapValue private String badge;

    public String getBadge() { return badge; }
    @Override public String getTitle() { return StringUtils.upperCase(delegate.getTitle()); }
    private interface Excluded { String getTitle(); }
}
```

Never copy or subclass `*Impl` classes from Core Components — they are internal and change without notice. Delegate to the public interface via `ResourceSuperType`, and add new getters on your own interface.

## 5. HTL and XSS

- HTL escapes by context automatically in text and attribute positions. Inside `<script>`, `<style>`, event handlers and `style` attributes you **must** set `@ context` (`scriptString`, `styleString`) — or better, pass data via `data-*` attributes.
- Rich text from an RTE: `${model.text @ context='html'}` (filtered through the XSS/AntiSamy policy). `context='unsafe'` is a review blocker.
- URLs: `uri` context is the default for `href`/`src`; resolve and map paths in the model (`.html` extension, externalizer/`resourceResolver.map`), not in HTL.
- Use `data-sly-set`, `data-sly-test.flag`, `data-sly-element` (restricted to safe element names) and `data-sly-attribute` maps instead of string-building markup.
- Include children with `data-sly-resource="${'item' @ resourceType='mysite/components/text'}"`, not `data-sly-include` of another component's script.

```html
<div data-sly-use.hero="com.mysite.core.models.Hero"
     data-sly-use.tpl="core/wcm/components/commons/v1/templates.html"
     data-sly-test.hasContent="${!hero.empty}" class="cmp-hero">
  <h2 class="cmp-hero__title">${hero.title}</h2>
  <a data-sly-repeat="${hero.links}" class="cmp-hero__link" href="${item.url}">${item.label}</a>
</div>
<sly data-sly-call="${tpl.placeholder @ isEmpty=!hasContent, classAppend='cmp-hero'}"></sly>
```

The Core placeholder template renders only in edit mode, so publish never shows empty boxes. Use `wcmmode.edit` / `wcmmode.disabled` in HTL (or `WCMMode.fromRequest` in Java) only for authoring aids, never for business logic.

## 6. Dialogs (Granite UI / Coral 3)

- One `_cq_dialog/.content.xml` per component, tabs via `granite/ui/components/coral/foundation/tabs`. When extending a Core dialog, overlay only what changes: `sling:resourceSuperType` on the component plus `sling:hideChildren`, `sling:orderBefore` and `sling:hideResource` in your dialog.
- Property names start with `./`; typed values with `@TypeHint` hidden fields or `{Boolean}` / `{Long}` in defaults; checkboxes need `uncheckedValue`.
- Multifield: `granite/ui/components/coral/foundation/form/multifield` with `composite="{Boolean}true"` stores child nodes (item0, item1) — read them with `@ChildResource`. Non-composite stores a String array.
- Validation: `required="{Boolean}true"`, `maxlength`, `validation="my.validator"` registered through the `foundation.validation.validator` registry in a clientlib in category `cq.authoring.dialog` (or a component-specific extraClientlibs category).
- Pickers: `pathfield` with `rootPath`, `fileupload`/image via the Core Image dialog, `autocomplete` for tags.
- `_cq_design_dialog` is the **policy dialog** for editable templates: template-level settings (allowed heading levels, image widths, lazy loading) go there, not in the author dialog.
- `cq:editConfig`: `cq:dropTargets` for DAM drag-and-drop, `cq:inplaceEditing` for RTE, `cq:listeners` (`afteredit="REFRESH_PAGE"` only when the component truly needs it).

## 7. Templates, policies and the Style System

- Editable templates live in `/conf/<site>/settings/wcm/templates`; template authors own structure, initial content and policies. Static templates (`/apps/.../templates`) are legacy — 6.5 only, avoid in new work.
- Policies (`/conf/<site>/settings/wcm/policies`) hold allowed components, design settings and **style groups**. Read them in models via `@ScriptVariable private Style currentStyle`.
- Style System classes go on the component wrapper; name them as BEM modifiers (`cmp-teaser--dark`) and keep the markup identical across styles. Use `cq:htmlTag` / `cq:noDecoration` deliberately — removing decoration also removes the element that carries the style classes.
- Configuration shared across a site (API keys, feature flags) goes in context-aware configuration under `/conf`, not in policies or dialogs.

## 8. i18n and accessibility

- Static strings: `${'Read more' @ i18n}` with dictionaries in `/apps/<site>/i18n`; in Java, `new I18n(request)`. Authored strings are translated by the translation workflow, not dictionaries.
- Headings: expose the level via policy/dialog (like Core Title `type`) so pages keep a valid outline.
- Images: alt from DAM with an explicit "decorative" option; links need discernible text; interactive components need keyboard support, focus management and ARIA only where native elements cannot do it.

## 9. Client libraries per component

Put component CSS/JS in a component clientlib category (`mysite.components.hero`) that the site clientlib **embeds**; do not call `clientlib.js` from each component's HTL (duplicate requests, render-blocking). Author-only JS goes in an author category loaded via page policy or `cq.authoring.*`.

## 10. AEM as a Cloud Service vs 6.5

- Cloud: `/apps` and `/libs` are immutable at runtime — everything ships through Cloud Manager; no CRXDE edits; Java 11+ (21 recommended for new builds, verify current guidance); Core Components provided by the platform.
- 6.5: overlays in CRXDE are possible but still anti-patterns; Core Components installed manually; static templates and Classic UI dialogs may exist in legacy code — migrate rather than extend.

## Deliverable format

For a new or changed component, deliver: (1) component node `.content.xml`, (2) `_cq_dialog/.content.xml` (and design dialog if policy-driven), (3) model interface + `*Impl.java` with a unit test using AEM Mocks (`AemContext`), (4) HTL file, (5) `_cq_editConfig.xml` if needed, (6) clientlib category and how it is embedded, (7) sample `.model.json` output. For reviews, list findings by severity (XSS/security, broken authoring, upgrade risk, performance, style) with file, line and fix.

## Anti-patterns to reject

- Content referencing `core/wcm/components/...` directly instead of a proxy; subclassing or copying Core `*Impl` classes.
- `@ context='unsafe'`, unescaped values inside `<script>`/`style`, or building markup/URLs as strings in HTL.
- Bare `@Inject` everywhere; required injection with no defaults, so new components render nothing.
- Business logic, JCR queries or service calls in HTL or in getters called repeatedly; queries in models on every render.
- Serialising `Resource`/`Page`/`ValueMap` via the exporter; leaking internal paths in JSON.
- Dialog dropdowns for visual variants that belong in the Style System; design settings in the author dialog.
- Hard-coded allowed components, static templates, or CRXDE edits in `/apps` on new projects.
- Empty components invisible in author (no placeholder) or placeholders leaking to publish.
- Per-component `data-sly-call` clientlib includes; author-only JS shipped to publish.
