Org Skills

aem-component-development

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.

Download .zip Raw Source
When agents use itUse 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.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill aem-component-development -a github-copilot
# or with the org installer (adds .github/skills/aem-component-development):
npx -y github:AGCO-Global/org-skills add skill aem-component-development

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill aem-component-development
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill aem-component-development -g

Installs the aem-development-skills plugin, which bundles all AEM / Development skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-development-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-component-development -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill aem-component-development -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-component-development -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-component-development -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

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

<!-- 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

@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)

@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.
<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.