Core path ยท 6 of 34
Master HTL, AEM's template language. Learn expressions, conditional rendering, lists, component composition, context-aware escaping, and WCM mode. Understand how HTL bridges Sling Models and HTML output. Troubleshoot common rendering issues.
In this chapter, you'll understand:
In the previous chapter, we learned how Sling Models prepare data for rendering through adaptation and dependency injection.
A Sling Model can:
But we're still missing the final step:
Key question:
Consider a Title component:
"Welcome To AEM"Something needs to take that Java object and turn it into markup the browser can paint. That's HTL โ the presentation layer of modern AEM development.
Its responsibility is simple:
Core principle:
HTL bridges the gap between Java objects and HTML markup.
Old approach using JSP:
<% String title = "hello"; %>
<h1><%= title %></h1>
Problems:
HTL approach:
<h1>${model.title}</h1>
Benefits:
HTL introduced a cleaner separation between data and presentation, making components more maintainable and secure.
The most fundamental HTL feature is expression syntax:
${variable}
${object.property}
${list[0]}
${object.method()}
HTL provides several built-in objects automatically:
| Object | Purpose | Example |
|---|---|---|
| properties | Component resource properties | $ |
| currentPage | Current page being rendered | $ |
| request | HTTP request object | $ |
| resource | Current resource | $ |
| model | Sling Model (when imported) | $ |
<!-- Access component properties -->
<h1>${properties.title}</h1>
<p>${properties.description}</p>
<!-- Access page properties -->
<h2>${currentPage.title}</h2>
<!-- Access resource path -->
<span>${resource.path}</span>
The most important pattern in modern AEM development:
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<h1>${model.title}</h1>
Breaking it down:
<sly> โ HTL element (renders nothing)
data-sly-use โ Import a Sling Model
.model โ Variable name (arbitrary)
="package.path" โ Full Java class path
HTL Import
โ
Sling Model Class Name
โ
Sling Framework
โ
Adapts Current Resource to Model
โ
@ValueMapValue fields injected
โ
@PostConstruct executed
โ
Model instance ready
โ
HTL can access public getters
HTL does not fetch data directly.
HTL only consumes data exposed by Sling Models.
<!-- WRONG - Direct repository access -->
<h1>${resource.valueMap.title}</h1>
<!-- CORRECT - Through Sling Model -->
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<h1>${model.title}</h1>
Why? Because:
<sly> ElementHTL provides a special element called <sly> that allows developers to execute HTL statements without generating additional HTML output.
<sly>Unlike a normal div, span, or section element, the <sly> tag disappears completely after rendering.
Example:
<!-- Before rendering -->
<sly data-sly-test="${model.title}">
<h1>${model.title}</h1>
</sly>
<!-- After rendering (sly tag is gone!) -->
<h1>Welcome To AEM</h1>
<sly> Is Useful<sly> PatternsModel import:
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<!-- Rest of template -->
Variable assignment:
<sly data-sly-set.pageTitle="${currentPage.title}"/>
<h1>${pageTitle}</h1>
Conditional:
<sly data-sly-test="${model.title}">
<h1>${model.title}</h1>
</sly>
data-sly-test renders an element only if a condition is true.
<div data-sly-test="${model.title}">
${model.title}
</div>
This renders the <div> only if model.title is not null/empty.
If the condition is false, the entire element and its children are removed from output.
Show element if title exists:
<h1 data-sly-test="${model.title}">
${model.title}
</h1>
<!-- If title is empty, entire h1 is removed -->
Show placeholder if missing:
<div data-sly-test="${model.description}">
${model.description}
</div>
<div data-sly-test="${!model.description}">
<!-- No description available -->
</div>
Author mode specific:
<div data-sly-test="${wcmmode.edit}">
[Configure Component]
</div>
Only shows in author edit mode.
| Condition | Result |
|---|---|
${variable} | True if not null/empty |
${!variable} | True if null/empty (NOT operator) |
${variable1 && variable2} | True if both are true |
${variable1 || variable2} | True if either is true |
One of the most common HTL tasks is iterating over collections.
HTL provides two approaches with important differences.
data-sly-list creates a loop variable and provides helpful metadata.
Syntax:
<ul>
<li data-sly-list="${model.items}">${item}</li>
</ul>
What happens:
model.items = ["Apple", "Banana", "Orange"]
โ
For each item:
item = current item
itemList = metadata (index, first, last, etc.)
Available metadata:
<li data-sly-list="${model.items}">
${item} <!-- Current item -->
Index: ${itemList.index} <!-- 0, 1, 2... -->
${itemList.first} <!-- true/false -->
${itemList.last} <!-- true/false -->
</li>
data-sly-repeat repeats the existing element for each item but does not provide metadata.
<ul>
<li data-sly-repeat="${model.items}">${item}</li>
</ul>
Same output as data-sly-list, but no metadata available.
| Use | When |
|---|---|
| data-sly-list | You need index, first, last, or other metadata |
| data-sly-repeat | You just need to repeat an element |
Rendering product list with alternating styles:
<ul>
<li data-sly-list="${model.products}"
class="${itemList.index % 2 == 0 ? 'even' : 'odd'}">
${item.name}
</li>
</ul>
Uses metadata (itemList.index) to alternate CSS classes.
Modern AEM components are often built from smaller components.
HTL provides two ways to include components.
Includes another AEM component and triggers the full Sling lifecycle.
<sly data-sly-resource="${'./image' @
resourceType='company/components/image'}"/>
What happens:
Use when:
Includes another HTL script without full Sling processing.
<sly data-sly-include="image.html"/>
What happens:
Use when:
| Feature | data-sly-resource | data-sly-include |
|---|---|---|
| Sling Lifecycle | Full | None |
| Model Adaptation | Yes | No |
| Script Resolution | Yes | No |
| Component Reuse | Full component | Template snippet |
| Performance | Heavier | Lighter |
One of the most important security features of HTL.
HTL automatically escapes output based on where it appears in the HTML document.
${userInput}
HTL detects the context and applies appropriate escaping:
Consider a malicious input:
<script>alert('XSS')</script>
Without escaping (JSP):
<h1><%= userInput %></h1>
<!-- RENDERS: <h1><script>alert('XSS')</script></h1> -->
<!-- DANGEROUS! -->
With HTL (automatic escaping):
<h1>${userInput}</h1>
<!-- RENDERS: <h1><script>alert('XSS')</script></h1> -->
<!-- SAFE! Script cannot execute -->
HTL's automatic escaping prevents:
Developers don't need to remember to escape manually.
HTL does it automatically based on context.
HTL templates often behave differently in Author vs Publish environments.
HTL provides wcmmode object to detect the current environment:
<!-- Author Edit Mode -->
<div data-sly-test="${wcmmode.edit}">
[Component Configuration Required]
</div>
<!-- Preview Mode -->
<div data-sly-test="${wcmmode.preview}">
Preview content shown
</div>
<!-- Disabled Mode (Publish) -->
<div data-sly-test="${wcmmode.disabled}">
This appears on Publish
</div>
Show edit markers in author:
<div data-sly-test="${wcmmode.edit}" class="edit-marker">
[Edit this component]
</div>
Show placeholder text in author:
<div data-sly-test="${!model.title && wcmmode.edit}">
[No title configured - Add one in the dialog]
</div>
Different rendering based on environment:
<!-- Author: Show configuration help -->
<div data-sly-test="${wcmmode.edit && !model.configured}">
Configure this component
</div>
<!-- Publish: Show actual content -->
<div data-sly-test="${wcmmode.disabled || model.configured}">
${model.title}
</div>
Let's trace a Title component from author to browser display.
Content author enters a title in the component dialog:
Title: "Welcome to AEM"
This value is stored in the repository:
/content/site/en/home/jcr:content/root/title
jcr:title = "Welcome to AEM"
sling:resourceType = "company/components/title"
The Sling Model processes this data:
@Model(adaptables = Resource.class)
public class TitleModel {
@ValueMapValue
private String title;
@PostConstruct
private void init() {
if (title != null) {
title = title.trim();
}
}
public String getTitle() {
return title != null ? title : "";
}
}
HTL consumes the Sling Model:
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<h1 data-sly-test="${model.title}">
${model.title}
</h1>
<div data-sly-test="${!model.title && wcmmode.edit}">
[No title configured]
</div>
The final HTML output:
<h1>Welcome to AEM</h1>
On publish (wcmmode is disabled, title exists).
End user sees:
Welcome to AEM
(rendered as h1 heading)
Author enters data
โ
Stored in JCR
โ
Resource retrieved by ResourceResolver
โ
Sling Model adapted from Resource
โ
@ValueMapValue injects title field
โ
@PostConstruct trims whitespace
โ
HTL imports model
โ
HTL accesses ${model.title}
โ
Context-aware escaping applied
โ
HTML generated: <h1>Welcome to AEM</h1>
โ
Browser displays heading
Wrong:
<h1>${properties.title.toUpperCase()}</h1>
Problems:
Correct:
// In Sling Model
public String getDisplayTitle() {
return title != null ? title.toUpperCase() : "";
}
<!-- In HTL -->
<h1>${model.displayTitle}</h1>
Wrong:
<h1>${model.title}</h1>
<!-- model is undefined! -->
Correct:
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<h1>${model.title}</h1>
Wrong:
<!-- Trying to output raw HTML -->
<div>${model.htmlContent}</div>
<!-- This gets escaped! -->
Correct (if HTML is safe):
<div data-sly-use.html="${model.getRichTextContent @ unsafe=true}">
${html}
</div>
Wrong:
<div data-sly-test="${model.a && model.b &&
(model.c || model.d) && !model.e}">
Too complex in HTL!
</div>
Correct:
// In Sling Model
public boolean shouldDisplay() {
return model.a && model.b && (model.c || model.d) && !model.e;
}
<!-- In HTL -->
<div data-sly-test="${model.shouldDisplay}">
Clear and readable
</div>
When HTL doesn't display data, follow this checklist:
| Step | Check | Common Issue |
|---|---|---|
| 1 | Model adapting? | Missing @Model annotation |
| 2 | Getter exists? | Method not public |
| 3 | HTL imports model? | Missing data-sly-use |
| 4 | Expression correct? | Typo in property name |
| 5 | Data in JCR? | Property not saved |
| 6 | Check logs? | Errors during adaptation |
HTL is often viewed as just a templating language, but it plays a critical role in the AEM architecture.
Resources โ Content
Sling Models โ Business Logic
HTL โ Presentation โ HERE
Clean separation of concerns enables:
When reviewing component implementations, architects look for:
<sly> and data attributes${}) access variables and objectsdata-sly-use<sly> element executes HTL without generating HTMLIn this chapter, we learned how HTL transforms Sling Model data into HTML output.
We've now covered the complete Sling rendering pipeline:
You now understand the end-to-end flow from browser request to rendered HTML.
From here, you're ready to explore:
The foundation is solid. The advanced topics will build naturally on top of it.
Get an email when I publish the next chapter. No spam โ just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.