For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/foundation/your-first-component.md.

Components are the primary building blocks of AEM pages. A button, a text block, a carousel, or a hero banner — each of these elements is a

Your First Component

Component Structure and Building Your First Component in AEM

Components are the primary building blocks of AEM pages. A button, a text block, a carousel, or a hero banner — each of these elements is a component. In this article, we will build a component from scratch and understand every file involved.


1. The Component Triad

Every complete AEM Component is typically made up of three main parts (the triad). Not every component requires all three (an extremely simple component might only need HTL), but most real-world components use this architecture:

ComponentFile TypePurpose
DialogXML (.content.xml)Defines the Authoring UI — input form fields for content authors.
HTL Template.htmlResponsible for rendering HTML markup based on data provided by the Model.
Sling Model.javaReads content data from JCR, processes business logic, and safely supplies data to the Template.

2. Project File Structure

A Component's source code is split across two separate locations (modules): UI (Frontend/Config) and Logic (Backend).

UI Module (Inside ui.apps):

ui.apps/src/main/content/jcr_root/apps/mysite/components/
└── hello/
    ├── .content.xml        # Component definition (cq:Component definition)
    ├── hello.html          # HTL template (Display markup)
    └── _cq_dialog/
        └── .content.xml    # Author dialog (Authoring UI)

Backend Module (Inside core):

core/src/main/java/com/mysite/core/models/
└── HelloModel.java         # Sling Model

3. Steps to Build a Component

Step 1: Component Definition

The .content.xml file in the component root folder defines the cq:Component node:

<?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="Hello"
    jcr:description="A simple greeting component"
    componentGroup="My Site - Content"/>
PropertyFunction
jcr:primaryTypeMust be cq:Component
jcr:titleDisplay name in the Component Browser list
jcr:descriptionTooltip description on mouse hover
componentGroupComponent grouping. Use a common group (e.g. "My Site - Content") for easier search.

Step 2: Write HTL Template

HTL (HTML Template Language) handles rendering HTML. Create hello.html:

<div class="cmp-hello" data-sly-use.model="com.mysite.core.models.HelloModel">
    <h2 class="cmp-hello__title">${model.greeting}</h2>
    <p class="cmp-hello__message">${model.message}</p>
</div>

This template performs 3 actions:

  1. Uses data-sly-use to instantiate the Sling Model (Java class).
  2. Retrieves data from the Model using $\{model.property\} syntax.
  3. Outputs HTML adhering to BEM naming conventions (cmp- prefix) — standard for AEM Core Components.

Step 3: Create Sling Model

The Sling Model reads values from the JCR node and prepares data for HTL:

package com.mysite.core.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Default;
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 HelloModel {

    @ValueMapValue
    @Default(values = "Hello, World!")
    private String greeting;

    @ValueMapValue
    @Default(values = "This is my first AEM component.")
    private String message;

    public String getGreeting() { return greeting; }
    public String getMessage() { return message; }
}

Annotation Explanations:

AnnotationMeaning
@Model(adaptables = Resource.class)Declares this class as a Sling Model adapted from a Resource (JCR Node).
@ValueMapValueInjects values from the component's JCR properties into this variable.
@DefaultProvides a default value if the Author hasn't entered anything yet.

Important: If you omit defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL, Sling Model's default strategy is REQUIRED. If an author drags the component onto a page without entering form data (no JCR data yet), the model fails and throws an error instead of displaying defaults. Always use OPTIONAL.

Step 4: Create Author Dialog

The Dialog defines which input fields an author sees. Create _cq_dialog/.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" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Hello"
    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 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">
                                            <!-- Greeting Input Field -->
                                            <greeting jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                                fieldLabel="Greeting"
                                                name="./greeting"
                                                required="{Boolean}true"/>
                                            <!-- Message Input Field -->
                                            <message jcr:primaryType="nt:unstructured"
                                                sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
                                                fieldLabel="Message"
                                                name="./message"/>
                                        </items>
                                    </column>
                                </items>
                            </columns>
                        </items>
                    </properties>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>

Note: The name attribute (such as name="./greeting") is critical. It defines the property name saved in JCR and must match the variable injected in your Sling Model (private String greeting;).


4. Component Rendering Flow

What actually happens when a user loads a page containing your component?


5. Deployment and Testing

  1. Build source code: In project root (mysite), run:

    mvn clean install -PautoInstallSinglePackage
  2. Drag & drop component onto a page:

    • Open AEM: http://localhost:4502/sites.html
    • Edit a page (Edit mode).
    • Open Side Panel, search for Hello and drag it onto the page.
    • Click component (wrench icon) to open Dialog, enter content and save.
  3. Verify in CRXDE Lite: Browse to the page path, navigate to the component node you dropped, and verify stored data:

    hello
    ├── jcr:primaryType = "nt:unstructured"
    ├── sling:resourceType = "mysite/components/hello"
    ├── greeting = "Welcome to My Site"        (<- From Dialog)
    └── message = "We build amazing things."   (<- From Dialog)

6. Debug Playbook: From Dialog to HTML (4 Checkpoints)

If your component does not render content as expected, use this 4-step check to isolate the issue:


7. Proxy Components (AEM Core Components)

In real project scenarios, instead of building basic components like "Text" or "Image" from scratch, we use Core Components (provided out-of-the-box by Adobe) via the Proxy Component design pattern.

Create a lightweight component pointing to a Core Component via sling:resourceSuperType:

<!-- apps/mysite/components/text/.content.xml -->
<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="Text"
    componentGroup="My Site - Content"
    sling:resourceSuperType="core/wcm/components/text/v2/text"/>

Thanks to sling:resourceSuperType, your component inherits all features, dialogs, and Sling Models from core/wcm/components/text/v2/text. You only override specific files (such as HTL) if you want custom markup. The Maven archetype generates these proxies automatically when initializing a project.