HTL (Slightly)
08 – HTL (Sightly)
Target: 3–4 years experienced AEM Developer Syllabus points covered (17, 18): Point 17 — "How will a model be called in HTL?" Point 18 — "Sightly tags and their uses." Project domain: a global energy technology company's marketing site.
Before we start — a note on the name
Your syllabus says "sightly tags." That is worth a moment, because it tells you something useful.
Sightly was the original name. Adobe introduced it in AEM 6.0 as the replacement for JSP. In AEM 6.3 it was renamed to HTL — HTML Template Language — when Adobe donated it to Apache Sling as an open specification.
They are the same thing. But a lot of people in the industry still say Sightly, and interviewers who learned AEM before 6.3 often do.
How to handle it if an interviewer says "Sightly": just answer normally and use "HTL" naturally in your response. Something like "Sightly — or HTL as it's called since 6.3 — is..." That signals you know both names and roughly when the change happened, without correcting them. Correcting an interviewer on terminology is never worth the point.
The one idea that makes this whole topic click
Most people describe HTL as a limited templating language, as though the limits are an unfortunate compromise.
That is backwards. The restrictions are the entire design.
HTL deliberately cannot express arbitrary logic. You cannot write a loop with a counter, call a static method, or perform a calculation. And that is not because Adobe ran out of time — it is the point.
Here is what JSP allowed:
Business logic in the view. Untestable, unreviewable, and impossible for a front-end developer to touch safely. And note there is no escaping on that output — a classic XSS hole.
HTL makes that impossible. There is no syntax for it. So the logic has to move into a Sling Model, where it can be unit tested — which is exactly what file 05 was about.
Say this in an interview and it lands:
"The thing I'd emphasise about HTL is that its restrictions are the feature, not a limitation. It deliberately can't express arbitrary logic, which forces business logic out of the template and into a Sling Model where it's testable and reviewable. JSP allowed scriptlets, and that's how you end up with business rules hidden in a view nobody can test. HTL also escapes output automatically based on context, so XSS becomes something you have to actively opt out of rather than something you have to remember to prevent."
That answer covers security, testability and maintainability in three sentences, and it frames you as someone who understands why rather than what.
1. Introduction
1.1 What is HTL?
HTL is AEM's server-side templating language. An HTL file is a valid HTML file with special
data-sly-*attributes that AEM processes when rendering.
Three properties that follow from that:
It is valid HTML. You can open an HTL file in a browser and it renders — not with real data, but the structure is visible. data-sly-* are just data attributes, which browsers ignore. That means a front-end developer can work with the file directly.
It is compiled. AEM compiles HTL into a Java servlet at runtime, then caches it. So HTL is not interpreted per request — the performance is closer to Java than to a scripting language.
It is server-side only. Everything happens on AEM before the HTML reaches the browser.
1.2 Why HTL replaced JSP
Four reasons, and giving several is what makes a good answer:
Security. HTL escapes output automatically, and — crucially — it picks the escaping based on where the value appears. A value in element content is HTML-escaped; the same value in an href is URI-escaped. In JSP you had to call the XSS API manually every single time, and one forgotten call was a vulnerability.
Separation of concerns. HTL cannot express business logic, so logic has to live in a Sling Model.
Front-end friendliness. A valid HTML file that opens in a browser. A front-end developer can work with it without understanding Java.
Fewer ways to be wrong. Less expressive means fewer mistakes possible.
1.3 A real project example to adapt
"All our components use HTL — we have no JSP left. The pattern is consistent:
data-sly-usebinds a Sling Model, the HTL guards its outer element withdata-sly-testso an unconfigured component renders nothing rather than an empty div, and all the logic lives in the model. We use<sly>wherever we need an HTL construct without an element in the output, so the markup stays clean for the front-end team. The only place we usecontext='unsafe'is emitting JSON-LD structured data, and that's justified because the JSON is produced by a serialiser rather than concatenated by hand."
That covers the pattern, the empty-state discipline, markup cleanliness, and the one security exception with its justification.
2. Core Concepts
2.1 How you call a Sling Model from HTL (syllabus point 17)
Your syllabus asks this directly. The answer is data-sly-use.
Breaking it down piece by piece:
data-sly-use is the block element that instantiates something.
.cta is the variable name you choose. It could be .model, .banner, anything. This trips people up — the part after the dot is not fixed, it is your identifier for the rest of the template.
The value is the fully qualified class name.
${cta.linkText} then calls the getter. And this follows the JavaBeans convention, which is the detail worth stating:
That is why file 05 insisted on getters. A public field will not work — HTL looks for a getter.
Now, what is the model actually adapted from?
data-sly-use adapts the current resource — or the current request, if the model declares SlingHttpServletRequest.class in its adaptables. HTL works this out from the model's declaration; you do not tell it.
That connects back to file 05's adaptables question. If your model is Resource -adaptable, HTL adapts the resource the component is rendering. If it is request-adaptable, HTL adapts the request. Either way the same data-sly-use syntax works — which is why you can change a model's adaptables without touching the HTL.
Passing parameters into a model:
Those arrive as injectable values in the model — a @ValueMapValue or @RequestAttribute named rootPath can pick it up. Useful when the same model needs different inputs in different places.
Two other things data-sly-use can load, worth knowing because interviewers ask what else it does:
The JS Use-API exists but is rare in modern projects — server-side JavaScript in AEM is generally discouraged in favour of Sling Models, and saying so shows current awareness.
The interview answer for point 17:
"
data-sly-use, with the fully qualified class name. Sodata-sly-use.cta="com.energy.core.models.CtaModel"— wherectais a variable name I choose, not a keyword.Then I call getters through the expression syntax:
${cta.linkText}callsgetLinkText(), and${cta.ready}callsisReady(). It follows the JavaBeans convention, which is why a Sling Model needs getters rather than public fields.HTL adapts either the current resource or the current request depending on what the model declares in its
adaptables— I don't specify that in the HTL, which is why I can change a model from resource-adaptable to request-adaptable without touching the template.I can also pass parameters with the
@syntax if the same model needs different inputs in different places. Anddata-sly-usecan load two other things: another HTL file, usually to reuse itsdata-sly-templateblocks, or a JavaScript Use-object — though the JS Use-API is rare now, since Sling Models are the standard."
2.2 The <sly> element — the one you'll use constantly
Before the full list of block elements, this one deserves its own section because it appears everywhere.
The problem it solves. Every data-sly-* attribute has to sit on an element. But often you want the HTL behaviour without an element in the output.
That div exists purely to hold the attribute, and it will interfere with your CSS.
<sly> is a synthetic element that never appears in the output.
Same behaviour, no wrapper.
<sly> is the modern approach. The older way was data-sly-unwrap, which you will still see:
Both work; <sly> is cleaner and current.
One caution: a <sly> with data-sly-use still scopes its variable to the rest of the template, so a self-closing <sly/> followed by usage works fine — which is the pattern above.
2.3 Every block element and what it's for (syllabus point 18)
Your syllabus asks for "sightly tags and their uses." Here they are, grouped by purpose.
Group 1 — Getting data
Group 2 — Controlling output
Group 3 — Including other things
Group 4 — Modifying the element
That is eleven, plus <sly>. Now each one properly.
2.4 data-sly-test — conditional rendering
If the expression is falsy, the element and everything inside it is removed. Not hidden — not rendered at all.
What counts as falsy in HTL:
Storing the result for reuse — this is the feature people forget:
data-sly-test.hasImage both tests and stores the result in a variable, which you can then negate. That is HTL's version of if/else, and it is worth knowing because HTL has no else.
Why you should always guard the outer element — the discipline from file 02:
And notice ${cta.ready} is a single flag from the model rather than five separate checks in the template. That is the right split: the model decides whether it has enough to render; the template just asks.
2.5 data-sly-list and data-sly-repeat — iteration
The difference is what gets repeated.
data-sly-list repeats the element's children:
Output: one <ul> containing several <li>.
data-sly-repeat repeats the element itself:
Output: several <li> and no wrapper.
The rule: use data-sly-list when you want the container rendered once; use data-sly-repeat when the repeated thing is the element.
The loop variables — these come up in interviews:
For a loop variable named item, HTL automatically provides itemList with:
The naming rule: the list object is your variable name plus List. Name it .card and you get cardList. If you omit the name entirely, the defaults are item and itemList.
A performance point that connects to file 05: everything inside a loop runs once per iteration. A getter called inside data-sly-list over fifty items is called fifty times. That is exactly why expensive work belongs in @PostConstruct.
2.6 data-sly-resource versus data-sly-include
These look similar and are genuinely different. This is a common interview question.
data-sly-resource includes a resource — it changes what content is being rendered, and Sling resolves the script for that resource type.
data-sly-include includes a script — the resource stays the same, only the script changes.
The distinction:
data-sly-resourcechanges the resource.data-sly-includechanges the script.
When to use each: data-sly-resource for rendering a child component or another piece of content — that is what you use most of the time. data-sly-include for splitting one component's own markup across several files, where the resource context should not change.
data-sly-resource options, several of which you have already met:
decoration and decorationTagName are the extra-divs answer from file 02, and being able to place them here shows the files connect.
2.7 data-sly-template and data-sly-call — reusable markup
This is HTL's version of a function, and it is how the clientlib helper from file 04 works.
Define a template:
Call it:
Templates from another file — the important pattern:
And now file 04 makes more sense. This is exactly the clientlib mechanism:
clientlib.html is a file full of data-sly-template blocks named css, js and all. You load the file with data-sly-use and call a template from it. Being able to explain that connection is a good moment in an interview.
Why templates matter practically: they are how you avoid duplicating markup between a server-rendered first batch and an AJAX-loaded second batch — the exact problem in file 02's Load More story.
2.8 data-sly-element, data-sly-attribute, data-sly-text
data-sly-element sets the tag name dynamically:
If headingLevel is "h2", that renders <h2>...</h2>.
Why this matters: heading levels should fit the page's document outline for accessibility, so authors need to choose. Without data-sly-element you would need a chain of tests for h1 through h6.
A security note: HTL restricts this to a whitelist of safe elements. You cannot inject <script> through it, which is deliberate.
data-sly-attribute sets attributes dynamically:
And here is the behaviour that connects to file 05: if the value is null or empty, the attribute is removed entirely rather than rendered empty.
That is why file 05's CtaModel returned null from getTarget():
You get <a href="..."> rather than <a href="..." target="">. Cleaner and valid.
You can also set a whole map of attributes:
data-sly-text replaces the element's content:
Why it exists: the placeholder stays visible when a front-end developer opens the file directly in a browser, but is replaced when AEM renders. That is the front-end-friendliness point in practice.
2.9 data-sly-set and data-sly-unwrap
data-sly-set defines a variable:
Useful for a value used several times, so an expensive getter is called once.
data-sly-unwrap removes the element but keeps the content:
Largely superseded by <sly>, but common in older code.
2.10 The expression language
Basic access:
Property names containing a colon need bracket notation — this is a genuinely common trip-up:
Operators:
What is deliberately absent: no arithmetic, no method calls with arguments, no string manipulation, no variable assignment beyond data-sly-set. If you need any of those, they belong in the model. That absence is the design.
String formatting — the correct way to build a string:
Joining an array:
Internationalisation:
The hint gives translators context, which matters on a multi-country site where the same English word translates differently by context.
Date and number formatting:
2.11 Display contexts — HTL's security model
This is the most important security concept in HTL, and it comes up in every security-conscious interview.
HTL escapes every expression automatically. But escaping is not one thing — the correct escaping depends on where the value lands.
HTL infers the context from position and applies the right one. That is the fundamental improvement over JSP, where you had to call the XSS API with the correct method yourself, every time.
You can override the context explicitly:
The contexts worth knowing:
The two you will actually use are html and uri.
context='html' for rich-text fields, because the author's markup is meant to render:
Without it, the author sees literal <p> tags on the page — which is scenario 17 in file 02.
context='uri' for URLs, to block javascript: URLs:
And context='unsafe' — the one to be careful about.
It disables escaping completely. The only place we use it is the JSON-LD structured data from file 02:
And the justification matters more than the usage:
"JSON-LD must not be HTML-escaped or search engines can't parse it, so
unsafeis necessary there. It's safe because the JSON was produced by Jackson, which escapes its own values — not because the content is trusted. If I'd built that string by concatenating author input,unsafewould be a genuine XSS hole. That distinction is the whole argument:unsafeis acceptable when a serialiser produced the string, and never when I assembled it myself."
Being able to state that reasoning is what separates knowing the option from understanding it.
2.12 The global objects
HTL gives you a set of objects without any data-sly-use:
wcmmode is the one worth calling out, because it drives the authoring placeholder pattern used throughout this repository:
An unconfigured component renders nothing on publish, but shows a clickable placeholder to authors. Small touch, and authors genuinely notice.
A caution on properties and currentPage: they are convenient, and using them for anything beyond trivial display drags logic back into the template. If you find yourself writing conditionals over properties, that decision belongs in the model.
3. Internal Working
3.1 How HTL actually runs
graph TD A["Request for a page"] --> B["Sling resolves the resource type<br/>and finds the HTL script"] B --> C{"Is the compiled<br/>version cached?"} C -->|Yes| E["Execute the compiled servlet"] C -->|No| D["HTL compiler:<br/>parse → generate Java → compile"] D --> E E --> F["data-sly-use → adapt to the Sling Model"] F --> G["Evaluate expressions,<br/>apply contextual escaping"] G --> H["HTML output"]
Two things worth taking from this:
HTL is compiled, not interpreted. It becomes a Java servlet, which is then cached. So the performance is Java performance, not scripting-language performance.
Which means HTL has compilation errors, and they appear in error.log at first render — not at deploy time. A malformed expression will deploy successfully and fail the first time someone loads the page. That is worth knowing, because it explains why a broken template gets through a build.
3.2 How data-sly-use resolves a model
sequenceDiagram participant H as HTL participant S as Sling Models participant R as Resource / Request H->>S: data-sly-use.cta = "com.energy...CtaModel" S->>S: read the model's @Model adaptables alt adaptables = Resource.class S->>R: adapt the CURRENT RESOURCE else adaptables = SlingHttpServletRequest.class S->>R: adapt the CURRENT REQUEST end R-->>S: model instance (or NULL) S-->>H: bind to the variable "cta" H->>H: ${cta.linkText} → getLinkText()
The critical branch is in the middle. HTL reads the model's adaptables and adapts accordingly. You never specify it in the template — which is why changing a model's adaptables requires no HTL change.
And note the "or NULL". If adaptation fails, data-sly-use binds nothing, and every ${cta.something} renders empty. No error, no exception, just a blank component. That is why "my component renders nothing" is usually a Sling Model problem rather than an HTL problem, and why file 05's five-cause checklist is the thing to reach for.
A quick diagnostic: output the object itself.
An object reference means the model resolved and the problem is in a getter. Nothing at all means adaptation returned null.
3.3 How contextual escaping is chosen
graph TD A["An expression ${value}"] --> B{"Where does it sit?"} B -->|"Element content"| C["HTML escaping"] B -->|"An attribute"| D["Attribute escaping"] B -->|"href / src"| E["URI escaping"] B -->|"Inside script"| F["JS string escaping"] B -->|"Inside style"| G["CSS escaping"] C --> H{"Did you specify<br/>@ context?"} D --> H E --> H F --> H G --> H H -->|No| I["Use the inferred context"] H -->|Yes| J["Use what you asked for"]
The key insight: the default is inferred from position and is almost always correct. You only specify a context when you deliberately want something other than the safe default — rich text that should render as markup, or serialised JSON that must not be escaped.
Which reframes the risk: in JSP, forgetting to escape was a vulnerability. In HTL, you have to actively override the safe default to create one. That inversion is the security story.
8. Coding Examples
8.1 The simple CTA — every element explained
Note the comment syntax: <!--/* ... */--> is an HTL comment and is stripped from the output. A plain <!-- ... --> HTML comment is sent to the browser. Using the wrong one leaks your internal notes to every visitor — a small thing that occasionally shows up in a security review.
8.2 The FAQ accordion — lists, loop variables, contexts and ARIA
8.3 Templates — one markup definition, two render paths
The template file — /apps/energy/components/templates/card.html:
Used by the component for the first batch:
And the same template for the AJAX batches — cardsfragment.html, which the servlet from file 07 includes:
This is the payoff of the servlet decision in file 07 — returning rendered HTML rather than JSON is what lets both paths share one template.
8.4 The page component — global objects, clientlibs, wcmmode
8.5 Common patterns worth memorising
9. Common Mistakes
10. Best Practices
On the split. HTL is presentation only. If you are writing a condition over more than one field, that decision belongs in the model as a single flag.
On guarding. Always guard the outer element with data-sly-test, and add an edit-mode placeholder so authors get something clickable while visitors get nothing.
On markup. <sly> for any construct that should not produce an element. decorationTagName rather than decoration=false where the element matters. Semantic elements throughout.
On escaping. Trust the default. Only add a context when you deliberately need something other than safe — html for genuine rich text, uri for URLs. Treat unsafe as requiring a written justification in a comment.
On performance. Assume everything in a loop runs per iteration. Store a repeated value with data-sly-set. Keep the real work in the model's @PostConstruct.
On reuse. data-sly-template for any markup rendered from more than one place — especially anything rendered both server-side and via AJAX.
On accessibility. Authorable heading levels via data-sly-element. aria-* through data-sly-attribute so empty values omit the attribute. Real buttons and links. An aria-live region for anything that updates.
On i18n. Every user-visible literal gets @ i18n, with a hint where the word is ambiguous.
11. Debugging Tips
Split the problem first, the same move as every other file: is the data missing, or is the rendering wrong?
An object reference means the model adapted and the problem is in a getter or the markup. Nothing at all means adaptation returned null — which is a Sling Model problem, and file 05's five-cause checklist applies.
HTL compilation errors appear at first render, not at deploy. HTL is compiled into a Java servlet the first time a page uses it, so a malformed expression deploys successfully and fails when someone loads the page. The error is in error.log and usually names the file and line. This surprises people who expect a build to catch it.
The Developer layer in the page editor shows which script actually rendered each component, along with its resource type. That answers "is my HTL even being used" in one look — useful when an inherited script from a sling:resourceSuperType is winning and you did not realise.
When the escaping looks wrong: view source rather than the rendered page. If you see <p> you are missing context='html'. If you see raw tags where you expected escaping, someone has used unsafe.
When a component renders nothing: check the data-sly-test guard first — it may be working exactly as designed because the model says it is not ready. Temporarily removing the guard tells you which.
12. Performance Optimization
The loop multiplication is the main one. Everything inside data-sly-list or data-sly-repeat runs once per iteration. A getter that does a repository lookup, inside a fifty-item loop, is fifty lookups. The fix is in the model — @PostConstruct — but the diagnosis is in the template, because that is where you can see how many times a getter is actually reached.
Store repeated values with data-sly-set rather than calling the same getter several times.
data-sly-resource has real cost. Each include is a resource resolution and a script execution. In a loop over many items that adds up, which is one more reason to bound list sizes in the model.
Keep the guard cheap. data-sly-test="${model.ready}" should be a boolean field set in @PostConstruct, not a getter that does work.
Clientlib splitting is the biggest front-end lever, and it lives in the page policy rather than the HTL — file 04.
HTL itself is fast. It compiles to a Java servlet and is cached, so the template language is rarely the bottleneck. When a page is slow it is almost always the model, a query, or an external call — not the HTL.
15. Comparison Tables
HTL vs JSP
data-sly-list vs data-sly-repeat
data-sly-resource vs data-sly-include
Display contexts
<sly> vs data-sly-unwrap
Where logic goes
Next topic
09-Workflows — your syllabus point 19: what a workflow is, the model/process/launcher structure, writing a custom process step, and how workflows differ from schedulers and Sling Jobs. Your syllabus note says "say you haven't done it but you know it" — so this file is built to let you answer confidently and honestly at the same time.
Topic 08 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.