Build a Custom Component in AEM as a Cloud Service: HTL, Sling Model and Touch UI Dialog
Building a custom component was one of the first things I wrote about on this blog. In AEM: Create Custom Component in CQ5 with Custom Dialog the component was a JSP with a Classic UI dialog, and later posts like Develop a Component in Touch UI and Create a Basic Sightly Component in AEM moved to Touch UI and HTL. Those posts still get readers, but on AEM as a Cloud Service a few things have changed.
The Classic UI is removed, so cq:Dialog ExtJS dialogs are gone. The HTL JavaScript Use-API was officially deprecated in release 2024.10.0. Code reaches /apps only through Cloud Manager (or a Rapid Development Environment), and Cloud Manager checks your unit test coverage. The modern recipe is a component node, a Granite UI (Coral 3) dialog, a Sling Model and an HTL script, with a unit test next to it.
In this tutorial I will build an Event Card component from scratch: a title, a location, a level, a link and an agenda that authors fill with a composite multifield. Let’s see it step by step.

Table of contents
- Why this matters
- Who should read this
- Prerequisites
- How it worked in CQ5 and AEM 6.x vs AEMaaCS
- Anatomy of a component on AEMaaCS
- Hands-on: build the Event Card component step by step
- Optional: JSON output with the Sling Model Exporter
- Extend a Core Component or build your own?
- Common mistakes and troubleshooting
- Hands-on challenge
- Key takeaways
- Knowledge check
- Frequently asked questions
- Continue your learning
- References
Why this matters
Core Components cover most of what a site needs, but every project ends up with a few components that are specific to the business: an event card, a product spec table, a store locator teaser. If you build those the old way, with JSP logic and scriptlets, they are hard to test, and untested code is exactly what the Cloud Manager code quality step flags. Adobe’s code quality rules mark coverage below 50% as an “Important” issue, which pauses the pipeline until someone reviews it.
A component built from HTL, a Sling Model and a Granite UI dialog keeps markup, logic and authoring separate. The model is plain Java that you can test without an AEM instance, the HTL stays readable for front-end developers, and the dialog is declarative XML.
Who should read this
This post is for developers who know basic AEM concepts and want the current way of building components on AEMaaCS, and for anyone maintaining JSP or Classic UI components in a project that is moving to the cloud.
Prerequisites
- A project generated from the AEM Project Archetype with
aemVersion=cloud. The examples use the app IDmysiteand the packagecom.mysite.core. - A local AEM SDK author on
http://localhost:4502, Java 21 and Maven. See Set Up AEMaaCS Local Development. - Basic Sling Model and HTL knowledge. My older post Use Sling Model with Sightly in AEM covers the basics.
- Optional: access to a Rapid Development Environment (RDE) and the
aioCLI with the RDE plugin.
How it worked in CQ5 and AEM 6.x vs AEMaaCS
| Part | CQ5 / early AEM 6.x | AEM as a Cloud Service |
|---|---|---|
| Rendering script | JSP with scriptlets | HTL |
| Logic | JSP, WCMUsePojo or the JavaScript Use-API | Sling Models (the JavaScript Use-API is deprecated since 2024.10.0) |
| Dialog | Classic UI cq:Dialog (ExtJS), later Coral 2 | Granite UI Coral 3 cq:dialog (granite/ui/components/coral/foundation/*) |
| Design settings | Design mode and design dialogs | Content policies on editable templates |
| Deployment | Package Manager, CRXDE or VLT straight into /apps | Cloud Manager pipeline; RDE for fast iterations; /apps is immutable at runtime |
| Build JDK | Java 8 or 11 | JDK 21 preferred (17 and 11 also listed for builds); Java 21 runtime |
Anatomy of a component on AEMaaCS
A custom component is spread over two Maven modules. This is the complete list of files we will create:
ui.apps/src/main/content/jcr_root/apps/mysite/components/eventcard/
├── .content.xml (component definition)
├── eventcard.html (HTL script)
└── _cq_dialog/
└── .content.xml (Touch UI edit dialog)
core/src/main/java/com/mysite/core/models/
├── EventCard.java (model interface)
├── AgendaItem.java (model for one multifield entry)
└── impl/
└── EventCardImpl.java (Sling Model implementation)
core/src/test/java/com/mysite/core/models/impl/EventCardImplTest.java
core/src/test/resources/com/mysite/core/models/impl/EventCardImplTest.json
The component definition tells AEM the component exists. The dialog defines what authors can enter. The Sling Model reads and prepares the values. HTL renders them. That separation is the whole idea.
Hands-on: build the Event Card component step by step
Step 1 : Create the component definition
Create the folder ui.apps/src/main/content/jcr_root/apps/mysite/components/eventcard with this .content.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root 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="Event Card"
jcr:description="Shows an event with its location, level, agenda and a link."
componentGroup="MySite - Content"/>
componentGroup decides where the component appears in the side panel and in the Allowed Components list of a template policy. A group that starts with a dot is hidden from the UI, which is what you want for helper components but not here. If you were extending a Core Component, you would also add sling:resourceSuperType; Adobe’s WKND Byline component, for example, extends core/wcm/components/image/v2/image.
Step 2 : Build the Touch UI dialog
Create _cq_dialog/.content.xml. The root uses cq/gui/components/authoring/dialog, and every field uses a Granite UI Coral 3 resource type. The agenda is a composite multifield, so each entry is saved as its own child node with a time and a topic property.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/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="Event Card"
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">
<properties
jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<title
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"
required="{Boolean}true"/>
<location
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Location"
name="./location"/>
<level
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Level"
name="./level">
<items jcr:primaryType="nt:unstructured">
<beginner jcr:primaryType="nt:unstructured" text="Beginner" value="beginner"/>
<intermediate jcr:primaryType="nt:unstructured" text="Intermediate" value="intermediate"/>
<advanced jcr:primaryType="nt:unstructured" text="Advanced" value="advanced"/>
</items>
</level>
<link
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Event page"
name="./link"
rootPath="/content/mysite"/>
<openInNewTab
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
text="Open the link in a new tab"
name="./openInNewTab"
value="true"
uncheckedValue="false"/>
</items>
</properties>
<agenda
jcr:primaryType="nt:unstructured"
jcr:title="Agenda"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<agenda
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Agenda items">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./agenda">
<items jcr:primaryType="nt:unstructured">
<time
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Time"
name="./time"/>
<topic
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Topic"
name="./topic"/>
</items>
</field>
</agenda>
</items>
</agenda>
</items>
</tabs>
</items>
</content>
</jcr:root>
After an author saves the dialog, the content looks like this in CRXDE:
eventcard (sling:resourceType = mysite/components/eventcard)
title = AEM Meetup Bengaluru
location = Bengaluru
level = beginner
link = /content/mysite/us/en/events
openInNewTab = true
agenda
item0 (time = 10:00, topic = Welcome and introductions)
item1 (time = 10:30, topic = Editable templates in AEMaaCS)

Note:- Every
namemust start with./, otherwise the value is not stored on the component node. This is still the most common reason for “my dialog does not save” questions I get.
Step 3 : Write the Sling Model interface
I always start with an interface. HTL and tests depend only on it, and the implementation can change freely.
package com.mysite.core.models;
import java.util.List;
import com.adobe.cq.export.json.ComponentExporter;
public interface EventCard extends ComponentExporter {
String getTitle();
String getLocation();
String getLevel();
String getLink();
boolean isOpenInNewTab();
List<AgendaItem> getAgenda();
boolean isEmpty();
}
Extending ComponentExporter is optional. I add it because it costs nothing and gives you JSON output later (see the exporter section). Each agenda entry gets a small model of its own:
package com.mysite.core.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 AgendaItem {
@ValueMapValue
private String time;
@ValueMapValue
private String topic;
public String getTime() {
return time;
}
public String getTopic() {
return topic;
}
}
Step 4 : Implement the Sling Model
The implementation follows the same pattern as Adobe’s WKND Byline model: adaptable from the request, registered for the component’s resource type, optional injection by default, and a @PostConstruct method for anything that needs code.
package com.mysite.core.models.impl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.StringUtils;
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.ExporterConstants;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import com.adobe.cq.export.json.ComponentExporter;
import com.mysite.core.models.AgendaItem;
import com.mysite.core.models.EventCard;
@Model(
adaptables = SlingHttpServletRequest.class,
adapters = {EventCard.class, ComponentExporter.class},
resourceType = EventCardImpl.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class EventCardImpl implements EventCard {
static final String RESOURCE_TYPE = "mysite/components/eventcard";
@SlingObject
private Resource resource;
@ValueMapValue
private String title;
@ValueMapValue
private String location;
@ValueMapValue
private String level;
@ValueMapValue
private String link;
@ValueMapValue
private boolean openInNewTab;
private final List<AgendaItem> agenda = new ArrayList<>();
@PostConstruct
protected void init() {
Resource agendaRoot = resource.getChild("agenda");
if (agendaRoot != null) {
for (Resource entry : agendaRoot.getChildren()) {
AgendaItem item = entry.adaptTo(AgendaItem.class);
if (item != null && StringUtils.isNotBlank(item.getTopic())) {
agenda.add(item);
}
}
}
}
@Override
public String getTitle() {
return title;
}
@Override
public String getLocation() {
return location;
}
@Override
public String getLevel() {
return level;
}
@Override
public String getLink() {
// internal page paths need the .html extension, external URLs stay as they are
if (StringUtils.startsWith(link, "/content/")) {
return link + ".html";
}
return link;
}
@Override
public boolean isOpenInNewTab() {
return openInNewTab;
}
@Override
public List<AgendaItem> getAgenda() {
return Collections.unmodifiableList(agenda);
}
@Override
public boolean isEmpty() {
return StringUtils.isBlank(title);
}
@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}
A few choices worth explaining:
- Request as adaptable. Adobe’s examples adapt from
SlingHttpServletRequest, which gives you access to request-scoped objects later (for example when you delegate to a Core Component model withModelFactory, as the WKND Byline does). - Injector-specific annotations.
@ValueMapValueand@SlingObjectsay exactly where a value comes from, which is easier to read than the generic@Inject. - Agenda in
@PostConstruct. I read the child nodes explicitly and skip empty entries, so the HTL never has to deal with half-filled rows. - An
isEmpty()method. The HTL uses it to decide between real output and the author placeholder.
Note:- The Apache Sling Models documentation now also lists the Jakarta Servlet request (
SlingJakartaHttpServletRequest) as an adaptable. Adobe’s AEMaaCS documentation and the WKND tutorial still useSlingHttpServletRequest, so that is what I use here. Keep an eye on the AEMaaCS release notes before you switch.
Step 5 : Write the HTL script
Create eventcard.html. It uses the Core Components placeholder template, which only renders in edit and preview mode when the component is empty, so authors see a box they can click instead of nothing.
<sly data-sly-use.card="com.mysite.core.models.EventCard"
data-sly-use.placeholderTemplate="core/wcm/components/commons/v1/templates.html"></sly>
<div class="cmp-eventcard" data-sly-test="${!card.empty}">
<h3 class="cmp-eventcard__title">${card.title}</h3>
<p class="cmp-eventcard__meta" data-sly-test="${card.location || card.level}">
<span class="cmp-eventcard__location" data-sly-test="${card.location}">${card.location}</span>
<span class="cmp-eventcard__level cmp-eventcard__level--${card.level}" data-sly-test="${card.level}">${card.level}</span>
</p>
<ul class="cmp-eventcard__agenda" data-sly-test="${card.agenda}" data-sly-list.item="${card.agenda}">
<li class="cmp-eventcard__agenda-item"><strong>${item.time}</strong> ${item.topic}</li>
</ul>
<a class="cmp-eventcard__link"
data-sly-test="${card.link}"
href="${card.link}"
data-sly-attribute.target="${card.openInNewTab ? '_blank' : ''}"
data-sly-attribute.rel="${card.openInNewTab ? 'noopener noreferrer' : ''}">View event</a>
</div>
<sly data-sly-call="${placeholderTemplate.placeholder @ isEmpty=card.empty, classAppend='cmp-eventcard'}"></sly>
HTL escapes every expression for its context automatically: the title as HTML text, the link as a URI, the level inside the class attribute. That is one of the main reasons Adobe prefers HTL over JSP. Notice there is no Java logic in the script; if you find yourself writing a long expression, move it into the model.
For the styling, add the CSS to the ui.frontend module next to the other component styles. The archetype compiles it into the clientlib-site client library (category mysite.site), which is served from /etc.clientlibs because the library sets allowProxy.
.cmp-eventcard {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 1.25rem;
}
.cmp-eventcard__level--beginner { color: #047857; }
.cmp-eventcard__level--advanced { color: #b91c1c; }
.cmp-eventcard__agenda { list-style: none; padding: 0; }
Step 6 : Add a unit test
AEM Mocks (from wcm.io) is already part of the archetype’s core module. Put a small content tree in a JSON file:
{
"eventcard": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "mysite/components/eventcard",
"title": "AEM Meetup Bengaluru",
"location": "Bengaluru",
"level": "beginner",
"link": "/content/mysite/us/en/events",
"openInNewTab": true,
"agenda": {
"jcr:primaryType": "nt:unstructured",
"item0": { "jcr:primaryType": "nt:unstructured", "time": "10:00", "topic": "Welcome and introductions" },
"item1": { "jcr:primaryType": "nt:unstructured", "time": "10:30", "topic": "Editable templates in AEMaaCS" },
"item2": { "jcr:primaryType": "nt:unstructured", "time": "11:00" }
}
},
"empty": {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType": "mysite/components/eventcard"
}
}
Then test the model through the same adaptation path that HTL uses:
package com.mysite.core.models.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import com.mysite.core.models.AgendaItem;
import com.mysite.core.models.EventCard;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
@ExtendWith(AemContextExtension.class)
class EventCardImplTest {
private final AemContext ctx = new AemContext();
@BeforeEach
void setUp() {
ctx.addModelsForClasses(EventCardImpl.class, AgendaItem.class);
ctx.load().json("/com/mysite/core/models/impl/EventCardImplTest.json", "/content/test");
}
@Test
void readsDialogValues() {
ctx.currentResource("/content/test/eventcard");
EventCard card = ctx.request().adaptTo(EventCard.class);
assertEquals("AEM Meetup Bengaluru", card.getTitle());
assertEquals("/content/mysite/us/en/events.html", card.getLink());
assertTrue(card.isOpenInNewTab());
assertEquals(2, card.getAgenda().size()); // the entry without a topic is skipped
assertFalse(card.isEmpty());
}
@Test
void isEmptyWithoutTitle() {
ctx.currentResource("/content/test/empty");
EventCard card = ctx.request().adaptTo(EventCard.class);
assertTrue(card.isEmpty());
assertTrue(card.getAgenda().isEmpty());
}
}
Run mvn clean install in core. Two small tests like these cover nearly every line of the model, which keeps you well above the 50% coverage threshold that Cloud Manager checks.
Step 7 : Deploy locally or to an RDE
Build and deploy the whole project to the local SDK:
mvn clean install -PautoInstallSinglePackage
autoInstallSinglePackage deploys the all package; the autoInstallPackage profile deploys one module only. To try the component on a Rapid Development Environment, install the same package with the RDE plugin of the aio CLI:
aio plugins:install @adobe/aio-cli-plugin-aem-rde
aio aem:rde:install all/target/mysite.all-1.0.0-SNAPSHOT.zip
For Dev, Stage and Production the only way in is the Cloud Manager pipeline. Cloud Manager builds with the JDK named in .cloudmanager/java-version at the root of your repository; use 21 for new projects.
Step 8 : Allow the component in a template and test it
A new component does not appear on pages until a template allows it. Open Tools > General > Templates > MySite > Content Page, select the main Layout Container in Structure mode, open its policy and tick Event Card under MySite – Content on the Allowed Components tab. The companion post Editable Templates, Policies and the Style System in AEM as a Cloud Service explains policies in detail.
Now open a page, drag the Event Card in, check that the placeholder shows, fill the dialog and add two agenda rows. View the page with ?wcmmode=disabled to see the published markup.

Optional: JSON output with the Sling Model Exporter
Because the model has an @Exporter annotation and implements ComponentExporter, Sling can serialize it to JSON with the model selector. That is handy for debugging, and for any front end that wants the component data without the markup:
curl -u admin:admin "http://localhost:4502/content/mysite/us/en/jcr:content/root/container/container/eventcard.model.json"
The path depends on where the component sits on your page; copy it from CRXDE. The JSON contains one entry per getter, plus :type with the resource type from getExportedType(). Add @JsonIgnore to getters you do not want to expose.
Extend a Core Component or build your own?
| Situation | Better choice | Why |
|---|---|---|
| You need a Title, Teaser or Image with a small tweak | Proxy component, optionally with the Style System | You inherit features, accessibility fixes and upgrades from Core Components |
| You need extra fields on an existing component | Extend it with sling:resourceSuperType and delegate in your model | Adobe’s WKND Byline extends the Core Image this way |
| The data and markup are specific to your business | Custom component like Event Card | Nothing to inherit; keep it small and tested |
If you have not created proxy components yet, start with Create Proxy Component in AEM. Most projects need far fewer custom components than they think.
Common mistakes and troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The component is not in the side panel | Not allowed in the Layout Container policy, or componentGroup starts with a dot | Allow it in the template policy; use a visible group |
| Dialog values are not saved | Field name without the ./ prefix | Use name="./title" |
| Agenda is always empty | Multifield without composite="{Boolean}true", so values are stored as one string array | Set composite and put name="./agenda" on the inner container |
| HTL renders nothing and the log shows the model can’t be adapted | The core bundle is not active, or the model package is not registered | Check the bundle on http://localhost:4502/system/console/bundles (local SDK only); rebuild core |
| CSS works on author but returns 404 on publish | Client library without allowProxy | Set allowProxy="{Boolean}true" and reference it through /etc.clientlibs |
| Pipeline pauses at code quality | Coverage below 50% or other Important issues | Add AEM Mocks tests for the new models |
Hands-on challenge
Add a Speakers tab to the dialog with a second composite multifield (name and role), expose it as List<Speaker> in the model, render it in HTL and extend the unit test. Then give the component a design dialog with a Styles tab and a “Compact” style. Success means the test passes, the JSON from .model.json shows the speakers, and the style changes the card without touching the HTL.
Key takeaways
- A component on AEMaaCS is a component node, a Granite UI Coral 3 dialog, a Sling Model and an HTL script.
- Classic UI dialogs are removed and the JavaScript Use-API is deprecated; use Sling Models with HTL.
- Use a composite multifield for repeating groups of fields and read the child nodes in the model.
- Keep logic in Java and test it with AEM Mocks; Cloud Manager flags coverage below 50%.
- Deploy with
autoInstallSinglePackagelocally,aio aem:rde:installon an RDE, and Cloud Manager for every other environment. - A component is only available to authors after a template policy allows it.
Knowledge check
1. Which resource type does the root node of a Touch UI dialog use?
cq/gui/components/authoring/dialog.
2. Why does the agenda multifield need composite="{Boolean}true"?
Without it a multifield stores one property with an array of values. Composite mode stores each entry as a child node, so one entry can hold several fields.
3. What does DefaultInjectionStrategy.OPTIONAL change?
Missing properties no longer make the adaptation fail; the fields simply stay null or their default value.
4. When does the Core Components placeholder template render?
Only in edit or preview mode, and only when isEmpty is true.
5. How can you deploy the component to Production?
Only through a Cloud Manager pipeline. /apps is immutable at runtime, and Package Manager cannot install code there.
Frequently asked questions
Do I need a cq:editConfig for my component?
Not for a component with a dialog; Adobe’s reference guide says it appears in the components browser even without one. Add it when you need drop targets or listeners such as afteredit="REFRESH_PAGE".
Can I still use WCMUsePojo or the JavaScript Use-API?
The JavaScript Use-API was officially deprecated in release 2024.10.0, with the Java Use-API as the replacement, so move those scripts to Java. WCMUsePojo is a Java Use-API class and still works, but for new code use Sling Models: they are easier to test and are what Adobe’s examples use.
Should my model adapt from Resource or from the request?
Adobe’s component examples adapt from SlingHttpServletRequest. Use Resource for small helper models like AgendaItem, which only read properties.
How do I test the dialog itself?
Unit tests cover the model. For the dialog, open it on the local SDK or an RDE, and consider UI tests in the archetype’s ui.tests module for critical components.
Can I build the same component for Edge Delivery Services?
Not with this code. Edge Delivery Services uses blocks written in plain JavaScript and CSS. HTL components like this one are for AEM Sites pages rendered by AEM.
Continue your learning
- Templates and styles: Editable Templates, Policies and the Style System in AEM as a Cloud Service.
- Sling Models basics: Use Sling Model with Sightly in AEM.
- Multifields: Create TouchUI Multifield Component using HTL and Create Touch UI Nested MultiField Dialog in AEM.
- Proxy components: Create Proxy Component in AEM.
- Unit testing with JUnit 5 and AEM Mocks is coming soon in the learning path.
- Tool: AEM Sling Model Generator and Analyzer.
- Practice quiz: AEM Development practice test 1.
- Roadmap: AEM as a Cloud Service learning path.
References
- WKND tutorial: Custom Component
- Components Reference Guide (AEMaaCS)
- Apache Sling Models
- Core Components placeholder template
- HTL specification
- AEMaaCS release 2024.10.0 (JavaScript Use-API deprecation)
- WKND tutorial: Client-Side Libraries
- Rapid Development Environments
- Cloud Manager build environment
- Cloud Manager code quality testing
- AEM Project Archetype on GitHub
Leave a Reply