Understanding HTL (Sightly) in AEM
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.
Content Objective
- Understand what HTL is and why it replaced JSP
- Master HTL expressions and common syntax patterns
- Learn how to use Sling Models in HTL templates
- Discover conditional rendering with data-sly-test
- Master list rendering with data-sly-list vs data-sly-repeat
- Learn component composition with data-sly-resource and data-sly-include
- Understand context-aware escaping and security
- Troubleshoot common HTL rendering issues
The Journey So Far
In the previous chapter, we learned how Sling Models prepare data for rendering through adaptation and dependency injection.
A Sling Model can:
- Retrieve content from the repository
- Perform validation and business logic
- Execute complex operations
- Expose clean data to the view layer
However, one important question still remains:
,[object Object],
A Simple Example
Consider a Title component:
- The Sling Model successfully retrieves the title value:
"Welcome To AEM" - The value is available inside the model object
- But the browser cannot render Java objects
- The browser only understands HTML
This is where HTL enters into the picture.
HTL is the presentation layer of modern AEM development.
Its responsibility is simple:
,[object Object],
Where HTL Fits in the Rendering Pipeline

HTL bridges the gap between Java objects and HTML markup.
Why HTL Was Created
Before HTL: JSP
Old approach using JSP:
<% String title = "hello"; %>
<h1><%= title %></h1>
Problems:
- Hard to maintain — Business logic mixed with presentation
- Hard to secure — Easy to introduce vulnerabilities
- Hard to read — Java code scattered throughout templates
- Hard to test — Logic embedded in templates
After HTL: Clean Separation
HTL approach:
<h1>${model.title}</h1>
Benefits:
- Clean — Simple, readable syntax
- Secure — Built-in escaping prevents XSS
- Maintainable — Logic stays in models
- Testable — Models tested independently
HTL introduced a cleaner separation between data and presentation, making components more maintainable and secure.
Understanding HTL Expressions
Basic Expression Syntax
The most fundamental HTL feature is expression syntax:
${variable}
${object.property}
${list[0]}
${object.method()}
Common Objects Available in HTL
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) | $ |
Example: Accessing Properties
<!-- 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>
Using Sling Models in HTL
The most important pattern in modern AEM development:
Importing a Sling Model
<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
How HTL Uses Models
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
Key Principle
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:
- Sling Model contains validation logic
- Sling Model contains business rules
- Sling Model is testable
- Sling Model is reusable
- HTL remains focused on presentation
The <sly> Element
HTL provides a special element called <sly> that allows developers to execute HTL statements without generating additional HTML output.
Understanding <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>
Why <sly> Is Useful
- Model initialization — Load data without generating HTML
- Conditional logic — Render content based on conditions
- Iteration — Loop through items without extra markup
- Variable assignment — Create temporary variables
Common <sly> Patterns
Model 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>
Conditional Rendering with data-sly-test
data-sly-test renders an element only if a condition is true.
Basic Pattern
<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.
Real-World Examples
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.
Common Conditions
| 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 |
Rendering Lists: data-sly-list vs data-sly-repeat
One of the most common HTL tasks is iterating over collections.
HTL provides two approaches with important differences.
data-sly-list: Creates Loop Variables
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 Element
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.
When to Use Each
| Use | When |
|---|---|
| data-sly-list | You need index, first, last, or other metadata |
| data-sly-repeat | You just need to repeat an element |
Real-World Example
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.
Component Composition
Modern AEM components are often built from smaller components.
HTL provides two ways to include components.
data-sly-resource: Full Component Rendering
Includes another AEM component and triggers the full Sling lifecycle.
<sly data-sly-resource="${'./image' @
resourceType='company/components/image'}"/>
What happens:
-
- ResourceResolver finds child resource
-
- Component Resolution identifies component
-
- Sling Model adapts resource
-
- HTL template renders
-
- Full lifecycle executed
Use when:
- Including complex components with models
- Components need full Sling processing
- Child resource needs adaptation and initialization
data-sly-include: HTL-Only Rendering
Includes another HTL script without full Sling processing.
<sly data-sly-include="image.html"/>
What happens:
-
- Find script file
-
- Include HTL content
-
- Minimal processing
Use when:
- Simple template inclusion
- No Sling Model needed
- Performance-sensitive code
Comparison
| 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 |
HTL Context-Aware Escaping
One of the most important security features of HTL.
What Is Context-Aware Escaping?
HTL automatically escapes output based on where it appears in the HTML document.
${userInput}
HTL detects the context and applies appropriate escaping:
- HTML context → HTML escaping
- JavaScript context → JavaScript escaping
- URL context → URL escaping
- CSS context → CSS escaping
Why This Matters
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 -->
This Is Why HTL Replaced JSP
HTL's automatic escaping prevents:
- Cross-Site Scripting (XSS) attacks
- HTML injection
- JavaScript injection
- URL injection
Developers don't need to remember to escape manually.
HTL does it automatically based on context.
WCM Mode and Authoring
HTL templates often behave differently in Author vs Publish environments.
HTL provides wcmmode object to detect the current environment:
WCM Mode Values
<!-- 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>
Real-World Use Cases
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>
Real Component Example: Complete End-to-End Flow
Let's trace a Title component from author to browser display.
Step 1: Author Input
Content author enters a title in the component dialog:
Title: "Welcome to AEM"
Step 2: Repository (JCR Storage)
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"
Step 3: Sling Model (Business Logic)
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 : "";
}
}
Step 4: HTL Template (Presentation)
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>
Step 5: HTML Generated
The final HTML output:
<h1>Welcome to AEM</h1>
On publish (wcmmode is disabled, title exists).
Step 6: Browser Output
End user sees:
Welcome to AEM
(rendered as h1 heading)
Complete Data Flow
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
Common HTL Mistakes
Mistake #1: Putting Business Logic in HTL
Wrong:
<h1>${properties.title.toUpperCase()}</h1>
Problems:
- Business logic in presentation
- Hard to test
- Hard to maintain
Correct:
// In Sling Model
public String getDisplayTitle() {
return title != null ? title.toUpperCase() : "";
}
<!-- In HTL -->
<h1>${model.displayTitle}</h1>
Mistake #2: Forgetting to Import Models
Wrong:
<h1>${model.title}</h1>
<!-- model is undefined! -->
Correct:
<sly data-sly-use.model="com.company.core.models.TitleModel"/>
<h1>${model.title}</h1>
Mistake #3: Not Using Context-Aware Escaping
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>
Mistake #4: Complex Conditions in HTL
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>
Troubleshooting: HTL Not Rendering Data
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 |
Debug Process
-
Empty output?
-
Check browser console (no error = HTL compiled ok)
-
Check error.log for adaptation errors
-
Verify property exists in CRX/DE
-
Test model directly (bypass HTL)
-
Add debug output to Sling Model
Why Architects Care About HTL
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:
- Maintainability — Changes to logic don't require template updates
- Testability — Models tested independently of templates
- Security — Built-in escaping prevents vulnerabilities
- Performance — Sling Models can be optimized separately
- Reusability — Models used by multiple templates or APIs
When reviewing component implementations, architects look for:
- Business logic in Sling Models (not HTL)
- Presentation logic in HTL (not models)
- Proper use of
<sly>and data attributes - Context-aware escaping working correctly
- No direct JCR access from HTL
Key Takeaways
- HTL is the presentation layer that consumes Sling Model data
- HTL expressions (
${}) access variables and objects - Sling Models are imported using
data-sly-use <sly>element executes HTL without generating HTML- data-sly-test conditionally renders elements
- data-sly-list provides loop metadata; data-sly-repeat does not
- data-sly-resource includes full components; data-sly-include includes HTL only
- Context-aware escaping prevents XSS automatically
- WCM mode enables author-specific rendering
- Business logic belongs in models, not templates
What Happens Next?
In this chapter, we learned how HTL transforms Sling Model data into HTML output.
We've now covered the complete Sling rendering pipeline:
- Chapter 1: Request Flow & Dispatcher
- Chapter 2: Resources & ResourceResolver
- Chapter 3: Component & Script Resolution
- Chapter 4: URL Decomposition
- Chapter 5: Sling Models (Adaptation & Injection)
- Chapter 6: HTL (Presentation Layer)
You now understand the end-to-end flow from browser request to rendered HTML.
From here, you're ready to explore:
- Advanced HTL — Global objects, formatting, localization
- Custom Servlets — Building REST APIs with @SlingServlet
- Query API — Searching content with QueryBuilder
- Workflows — Content automation and publishing
- Performance Optimization — Caching, dispatch cache, CDN strategies
The foundation is solid. The advanced topics will build naturally on top of it.