All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
This is a companion to references/extending-core-components.md which covers the reference guide for Sling Resource Merger, Model Delegation, and ComponentExporter patterns.
<?xml version="1.0" encoding="UTF-8"?>
<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="CTA List"
jcr:description="Fixed list of CTA buttons"
componentGroup="[group]"
sling:resourceSuperType="core/wcm/components/list/v4/list"/><?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="CTA List"
sling:resourceType="cq/gui/components/authoring/dialog">
<content
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<tabs
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/tabs"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<!-- HIDE inherited tabs from core List using Sling Resource Merger -->
<listSettings
jcr:primaryType="nt:unstructured"
sling:hideResource="{Boolean}true"/>
<itemSettings
jcr:primaryType="nt:unstructured"
sling:hideResource="{Boolean}true"/>
<!-- Custom Properties tab -->
<properties
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<listTitle
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="List Title"
fieldDescription="Title displayed above the list"
name="./listTitle"/>
<!-- Hidden field to force "static" list type -->
<listFrom
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/hidden"
name="./listFrom"
value="static"/>
<!-- Custom pages multifield -->
<pages
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Pages"
fieldDescription="Add pages to display">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./pages">
<items jcr:primaryType="nt:unstructured">
<linkURL
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Page"
name="./linkURL"
rootPath="/content"/>
<linkText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Link Text"
fieldDescription="Custom text (optional)"
name="./linkText"/>
</items>
</field>
</pages>
</items>
</column>
</items>
</columns>
</items>
</properties>
</items>
</tabs>
</items>
</content>
</jcr:root>package [package].models;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.ListItem;
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = {CtaListModel.class, ComponentExporter.class},
resourceType = CtaListModel.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION
)
public class CtaListModel implements ComponentExporter {
public static final String RESOURCE_TYPE = "[project]/components/cta-list";
/**
* Delegate to core List model (use fully qualified name to avoid collision)
*/
@Self
@Via(type = ResourceSuperType.class)
private com.adobe.cq.wcm.core.components.models.List coreList;
@SlingObject
private Resource resource;
@ValueMapValue
private String listTitle;
private List<CtaListPageItem> pages;
@PostConstruct
protected void init() {
pages = new ArrayList<>();
Resource pagesResource = resource.getChild("pages");
if (pagesResource != null) {
for (Resource itemResource : pagesResource.getChildren()) {
CtaListPageItem pageItem = itemResource.adaptTo(CtaListPageItem.class);
if (pageItem != null && pageItem.hasContent()) {
pages.add(pageItem);
}
}
}
}
public String getListTitle() {
return listTitle;
}
public List<CtaListPageItem> getPages() {
return Collections.unmodifiableList(pages);
}
/**
* Delegate to core List model for future use
*/
public Collection<ListItem> getListItems() {
return coreList != null ? coreList.getListItems() : Collections.emptyList();
}
public boolean hasContent() {
return (listTitle != null && !listTitle.trim().isEmpty()) ||
(pages != null && !pages.isEmpty());
}
@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}package [package].models;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
@Model(
adaptables = Resource.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class CtaListPageItem {
@ValueMapValue
private String linkURL;
@ValueMapValue
private String linkText;
public String getLinkURL() {
return linkURL;
}
public String getLinkText() {
return linkText;
}
public boolean hasContent() {
return linkURL != null && !linkURL.trim().isEmpty();
}
}When testing models that use SlingHttpServletRequest as adaptable:
package [package].models;
import static org.junit.jupiter.api.Assertions.*;
import org.apache.sling.api.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
@ExtendWith(AemContextExtension.class)
class CtaListModelTest {
private final AemContext context = new AemContext();
@BeforeEach
void setUp() {
// Register both model classes
context.addModelsForClasses(CtaListModel.class, CtaListPageItem.class);
}
/**
* Helper to adapt via request (required for SlingHttpServletRequest adaptable)
*/
private CtaListModel getModel(Resource resource) {
context.currentResource(resource);
return context.request().adaptTo(CtaListModel.class);
}
@Test
void testWithCompleteData() {
// Create resource with sling:resourceType
Resource componentResource = context.create().resource("/content/test",
"sling:resourceType", CtaListModel.RESOURCE_TYPE,
"listTitle", "Most Visited");
// Create multifield items
context.create().resource("/content/test/pages/item0",
"linkURL", "/content/page1",
"linkText", "Page One");
context.create().resource("/content/test/pages/item1",
"linkURL", "/content/page2",
"linkText", "Page Two");
CtaListModel model = getModel(componentResource);
assertNotNull(model);
assertTrue(model.hasContent());
assertEquals("Most Visited", model.getListTitle());
assertEquals(2, model.getPages().size());
assertEquals(CtaListModel.RESOURCE_TYPE, model.getExportedType());
}
@Test
void testWithEmptyData() {
Resource componentResource = context.create().resource("/content/test",
"sling:resourceType", CtaListModel.RESOURCE_TYPE);
CtaListModel model = getModel(componentResource);
assertNotNull(model);
assertFalse(model.hasContent());
assertNull(model.getListTitle());
assertTrue(model.getPages().isEmpty());
}
}When using @Self @Via(type = ResourceSuperType.class) to delegate to a Core Component model, the delegated field (e.g., coreList) will be null in wcm.io AemContext unit tests.
Why this happens: The wcm.io mock context does not perform actual Sling Resource Merger resolution.The @Via(type = ResourceSuperType.class) annotation relies on the resource super type chain to adaptthe resource as the parent component's model, but in a mock environment this resolution never occurs.As a result, the injected Core Component model is always null.
What this means in practice:
getListItems(), getTitle()) will throw a NullPointerException or return null if the delegation field is not guardedgetListTitle(),getPages(), hasContent()), not delegated Core Component behaviorRecommendations:
null:// In your Sling Model — null-guard pattern for delegated methods
public String getTitle() {
return coreList != null ? coreList.getTitle() : null;
}
public Collection<ListItem> getListItems() {
return coreList != null ? coreList.getListItems() : Collections.emptyList();
}@Via(type = ResourceSuperType.class)resolves correctly and delegated methods return real data, use AEM integration tests running in anactual Sling container where the Resource Merger is available.sling:resourceSuperType pointing to core componentsling:hideResource="{Boolean}true" to hide unwanted tabssling:hideResource to hide unwanted fields (if needed)sling:hideProperties for field overrides (if needed)sling:orderBefore for tab positioning (if needed)SlingHttpServletRequest.classresourceType to @Model annotationadapters with model class AND ComponentExporter.class@Exporter annotation@Self @Via(type = ResourceSuperType.class)@SlingObject for Resource accessgetExportedType() methodhasContent() methodcontext.request().adaptTo()context.currentResource() before adaptingsling:resourceType to test resourcessetUp()getExportedType().tessl-plugin
skills
aem-rde
aem-workflow
code-assessment
content-distribution
create-component
assets
references
dispatcher
incident-response
references
dispatcher-foundation
performance-tuning
references
dispatcher-foundation
security-hardening
references
dispatcher-foundation
technical-advisory
references
dispatcher-foundation
workflow-orchestrator
references
dispatcher-foundation
ensure-agents-md
references
migration
references
dispatcher
legacy-ui
template-modernization
validation
scripts
dispatcher-tool
validate-migration