Build your first AEM component — no Java needed
CRXDE, a dialog, HTL — and the one deployment warning nobody tells beginners.
📺 Prefer to watch? This is the written version of Episode 4 of the AEM Complete Tutorial Series:
Most AEM component tutorials start with Java. Sling Models, annotations, Maven builds — before you’ve rendered a single line of HTML.
We’re doing it differently. Today you build a working, authorable, styled component with nothing but AEM and a browser. No Java. No build for most of it. And by the end, you’ll know exactly what AEM does behind the scenes when an author hits save — which is the part most tutorials skip.
You need AEM running locally (Episode 2) and the aeminsider-site project deployed (Episode 3).
CRXDE Lite — the repository, raw
Open localhost:4502/crx/de.
This is the developer’s window into the repository. Everything in AEM lives in the JCR — the Java Content Repository. Pages, components, configurations, images. All of it stored the same way: as nodes and properties.
A node is like a folder that can also hold data. A property is a name and a value on that node. That’s the entire data model. Once this clicks, AEM stops being magic.
Three top-level folders matter:
/apps — your code. Components, client libraries. Deployed from the ui.apps module. /content — the actual website. Pages, assets, authored data. From ui.content. /conf — configuration. Most importantly, editable templates. Also from ui.content.
And one you’ll see but never touch: /libs. Adobe’s own code. Rule number one of AEM: never modify /libs. Look, learn, don’t touch.
Notice something? The repository and the codebase are mirrors. Code is just content that gets deployed. Keep that in your head — it explains half of AEM.
Two ways to build a component
You can create a component in your codebase (needs a deployment before it shows up), or directly in CRXDE (live instantly — the easy way, and today’s way).
One warning before we start. If you build a component in CRXDE and then run a deployment, your work gets overwritten by the code. Gone. Export it to your codebase before any deployment — I show you exactly how at the end of this post.
Create the component
In CRXDE, right-click /apps/aeminsider/components → Create → Create Node:
Name:
cardType:
cq:Component
Add two properties: jcr:title = Card (the name authors see) and componentGroup = AEM Insider Site - Content (remember this one — it comes back later). Then create a file inside: card.html.
And hit Save All. Every change in CRXDE needs Save All. Nothing exists until you click it. Burn that into your muscle memory.
The dialog — the author’s form
Authors don’t use CRXDE. Authors need a form. In AEM, that form is a node called cq:dialog inside your component.
Real practitioner move: nobody writes dialogs from scratch. We copy a working one and adapt it. The archetype gave us a helloworld component with a one-field dialog. Perfect donor.
Copy cq:dialog from helloworld, paste it into card, then drill down to the text field and adapt it. The key property on every field is name. ./title means: save the value as a property called title, directly on the component’s node.
Make two fields: a textfield saving ./title and a textarea saving ./description. (Your copied helloworld dialog may nest slightly differently — the structure above is the shape that matters: container → tabs → tab → fields.)
HTL — the dialog writes, HTL reads
Now make card.html use those values:
<div class="cmp-card">
<h2>${properties.title}</h2>
<p>${properties.description}</p>
</div>See the contract? The dialog saves ./title. HTL reads properties.title. Same name on both sides. That’s the whole connection — no code in between.
And one big thing: HTL escapes output automatically. XSS protection is on by default. In the old JSP days you had to remember to escape everything yourself. In HTL, safe is the default.
No Java yet. No Sling Model. Just properties in, HTML out. There is a Java layer for real logic — Sling Models — and we’ll get there later in the series. For a component like this, you don’t need it.
Why it’s not on the page yet
Here’s the number one question every new AEM developer asks: “I built my component. Why can’t I see it in the editor?”
A component needs three things to be usable on a page: the component itself, the dialog, and — the one everybody misses — the page’s template has to allow it.
Modern AEM pages are built on editable templates (they live in /conf). Every template has policies, and the policy on the layout container says exactly which component groups are allowed inside.
Think of the template as a bouncer. Your component is real, it exists, it’s standing outside. The policy is the guest list. Our group — AEM Insider Site - Content — is the entry ticket, and the archetype’s default template already has it on the list.
Author it
Sites console → your site → Create Page → open it in the editor. Filter the component browser for “Card”, drag it into the container, open the dialog, fill in the title and description, save.
It renders. Your HTL, your dialog, your properties — working together.
Behind the scenes — the round trip
This is the part most tutorials skip. Go back to CRXDE and find your test page under /content. Every page has a jcr:content node; inside it, root, then container — and there’s a card node. AEM created it when you dragged the component onto the page.
Click it. There are your properties: title, description — written by the dialog. And one more: sling:resourceType, pointing at aeminsider/components/card. That’s the link. It tells AEM: to render this node, use that component.
So the full round trip: the author saves the dialog → values become properties on a node under the page → on render, Sling follows the resourceType to your component → card.html runs → properties.title reads that exact node → HTML goes out.
Content lives under /content. The component lives under /apps. One is data, the other is the renderer. That separation is the heart of AEM.
Style it (and a confession)
It works — but it looks like 1995. For today, add a style block right inside card.html, scoped to the component class:
<style>
.cmp-card {
padding: 24px;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,.08);
}
.cmp-card h2 { color: #D32F2F; }
</style>
<div class="cmp-card">
<h2>${properties.title}</h2>
<p>${properties.description}</p>
</div>Now that’s a card. But I need to be straight with you: a style tag inside the component is fine for learning, not how real projects ship CSS. Ten cards on a page means ten copies of the same CSS, no bundling, no minification, no proper caching. AEM’s answer is client libraries — registered CSS/JS that AEM bundles, minifies, versions, and loads once per page. That’s a full video of its own, later in the series.
Export it to code — before a deployment eats it
Remember the warning. This component lives only on the instance. The next mvn clean install -PautoInstallPackage would wipe it out. Here’s the rescue:
Open Package Manager:
localhost:4502/crx/packmgrCreate Package. Name:
card-component, group:my_packagesEdit → Filters → add filter with root path
/apps/aeminsider/components/card. The filter is the shopping list — just our card, nothing else.Build, then Download.
Unzip it. The structure is the repository path as folders:
jcr_root/apps/aeminsider/components/card. And look closer —card.htmlis a plain file, the component node became.content.xml, and the dialog became a folder called_cq_dialog.cq:in the repository turns into_cq_on disk. That’s how AEM serializes nodes into files.Copy the
cardfolder into your project:ui.apps/src/main/content/jcr_root/apps/aeminsider/components/. Commit to Git.
Proof it worked: run the deployment from Episode 3 and refresh your page. The card is still there. Deployment-proof. From CRXDE experiment to version-controlled code — that’s the full professional loop.
Recap
CRXDE Lite is your window into the repository. Everything is nodes and properties.
/apps is code, /content is data, /conf is config — mirrors of the codebase.
A component = a
cq:Componentnode + title + group + an HTL file.name="./title"writes.${properties.title}reads. Same name, both sides.The template’s policy is the guest list. Your component group gets you in.
Dialog save → node properties →
sling:resourceType→ HTL → HTML.
What’s next
Next episode: AEM Dialogs — Deep Dive. Today we copied a dialog and changed two fields. Next time we master them — dropdowns, checkboxes, path pickers, rich text, tabs, and validation so authors can’t save broken content.
📺 Full series playlist: youtube.com/@aeminsider 🌐 Website: aeminsider.com
Stuck somewhere? Component not showing up? Reply to this email or drop a comment on the video — I read all of them.
This is AEM Insider. Straight talk on AEM.
