htl-scripting

Write, edit, and review AEM HTL (HTML Template Language) scripts. HTL is the server-side template system for AEM components — HTML files with ${expression} syntax and data-sly-* block attributes. Use when creating or modifying .html component scripts, fixing XSS context errors, building iteration/conditional markup, wiring Sling Models via data-sly-use, or constructing reusable HTL templates. Also activate when the user mentions HTL, Sightly, data-sly-*, or AEM component markup.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "htl-scripting" with this command: npx skills add headwirecom/aem-agent-skills/headwirecom-aem-agent-skills-htl-scripting

HTL Scripting

HTL (HTML Template Language) is AEM's server-side template language. It replaces JSP for AEM components. HTL files are valid HTML — all logic lives in ${expressions} and data-sly-* attributes which are stripped from output.

Key mental model: HTL = HTML + expressions (${...}) + block attributes (data-sly-*). There is no imperative code in HTL. All business logic belongs in Java (Sling Models) or JavaScript Use-API objects, referenced via data-sly-use.

Expressions

Syntax: ${ expression @ option1, option2=value }

${myVar}                          <!--/* simple identifier */-->
${myObject.key}                   <!--/* dot access */-->
${myObject['key']}                <!--/* bracket access */-->
${myArray[0]}                     <!--/* array index */-->
${true}  ${42}  ${'literal'}      <!--/* literals */-->
${[1, 2, 3]}                      <!--/* array literal */-->

Operators

${!myVar}                              <!--/* NOT */-->
${a && b}                              <!--/* AND (returns first falsy or last value) */-->
${a || b}                              <!--/* OR  (returns first truthy or last value) */-->
${cond ? valA : valB}                  <!--/* ternary — ':' MUST have surrounding spaces */-->
${a == b}  ${a != b}                   <!--/* equality (strict, no type coercion) */-->
${a < b}  ${a <= b}  ${a > b}  ${a >= b}  <!--/* comparison (same-type only) */-->
${'bc' in 'abcd'}                      <!--/* string contains */-->
${item in myArray}                     <!--/* array/list membership */-->
${'key' in myMap}                      <!--/* object/map key check */-->

Default value pattern (|| returns first truthy operand):

${properties.pageTitle || properties.jcr:title || resource.name}

Boolean casting

Falsy: false, 0, '', "", [] (empty iterable), null Truthy: everything else (including "false" string, [0])

Expression options (@ ...)

Options modify expressions. Syntax: ${expr @ opt1, opt2=val}.

Display context — override automatic XSS escaping:

${value @ context='html'}        <!--/* allow safe HTML tags */-->
${value @ context='uri'}         <!--/* validate as URI */-->
${value @ context='text'}        <!--/* default for text nodes: encode HTML entities */-->
${value @ context='unsafe'}      <!--/* DISABLES all XSS protection — avoid */-->

See references/xss-contexts.md for the full context table.

Format — string interpolation, dates, numbers:

${'Asset {0} of {1}' @ format=[current, total]}
${'yyyy-MM-dd' @ format=myDate}
${'#,###.00' @ format=1000}

i18n — internationalization:

${'Assets' @ i18n}
${'Assets' @ i18n, locale='de', hint='menu label'}

Array join:

${['a','b','c'] @ join=', '}     <!--/* outputs: a, b, c */-->

URI manipulation — modify URL parts without string concatenation:

${'page.html' @ selectors='print', extension='pdf'}
${'path/page.html' @ prependPath='/content/site', appendPath='jcr:content'}
${request.requestURL @ addQuery={'lang': 'en'}, fragment='section1'}
${url @ scheme='https', domain='example.com', removeSelectors}

URI options: scheme, domain, path, prependPath, appendPath, selectors, addSelectors, removeSelectors, extension, suffix, prependSuffix, appendSuffix, query, addQuery, removeQuery, fragment.

Block Statements (data-sly-*)

All data-sly-* attributes are removed from rendered output. Multiple blocks can coexist on one element — they execute in priority order (see end of section).

data-sly-use — load logic

<!--/* Sling Model (preferred for AEM as a Cloud Service) */-->
<sly data-sly-use.model="com.example.core.models.MyModel"/>
${model.title}

<!--/* Relative HTL template file */-->
<sly data-sly-use.tmpl="partials/card.html"/>

<!--/* With parameters */-->
<sly data-sly-use.nav="${'com.example.Nav' @ depth=2}"/>
  • Identifier (.model) is global — usable anywhere after declaration.
  • Without identifier, the object is available as useBean.
  • Prefer Sling Models (Java Use-API) for AEM as a Cloud Service. JavaScript Use-API is deprecated for AEMaaCS.

data-sly-text — set element text

<p data-sly-text="${properties.jcr:title}">placeholder</p>
<!--/* outputs: <p>Actual Title</p> — placeholder replaced */-->

Applies text context (HTML-encodes) by default.

data-sly-attribute — set attributes

<!--/* Single attribute */-->
<div data-sly-attribute.class="${cssClass}"></div>

<!--/* Multiple from map */-->
<input data-sly-attribute="${{'id': 'foo', 'class': 'bar'}}" type="text"/>

<!--/* Boolean attribute */-->
<input data-sly-attribute.checked="${isChecked}"/>
<!--/* true → <input checked/>, false → <input/> */-->
  • Empty string ${''} removes the attribute.
  • on* event handlers and style cannot be set (XSS risk).
  • Processed left-to-right when multiple attribute blocks exist.

data-sly-element — change tag name

<div data-sly-element="${headingLevel}">Title</div>
<!--/* headingLevel='h2' → <h2>Title</h2> */-->

Restricted to a safe allowlist of element names (no script, style, form, input).

data-sly-test — conditional rendering

<p data-sly-test="${wcmmode.edit}">Edit mode only</p>

<!--/* Capture result in identifier (global scope, keeps original type) */-->
<sly data-sly-test.hasTitle="${properties.jcr:title}"/>
<h1 data-sly-test="${hasTitle}">${hasTitle}</h1>
  • Omitted value = false (element never renders).
  • Identifier stores the original value, not a boolean cast.

data-sly-list — iterate (repeats content only)

<ul data-sly-list="${currentPage.listChildren}">
    <li>${item.title} (${itemList.count})</li>
</ul>

<!--/* Custom identifier */-->
<ul data-sly-list.child="${pages}">
    <li class="${childList.first ? 'first' : ''}">${child.title}</li>
</ul>

<!--/* Iteration control */-->
<ul data-sly-list="${items @ begin=0, end=9, step=2}">
    <li>${item.name}</li>
</ul>
  • Default item variable: item. Metadata: itemList.
  • Custom data-sly-list.foo → item is foo, metadata is fooList.
  • Metadata properties: index (0-based), count (1-based), first, middle, last, odd, even.
  • For Maps: item = key, access value with ${myMap[item]}.
  • Element is hidden if collection is empty.

data-sly-repeat — iterate (repeats entire element)

<div data-sly-repeat.article="${articles}" id="${article.id}">
    ${article.excerpt}
</div>
<!--/* Outputs N <div> elements, one per article */-->

Same options/metadata as data-sly-list. Difference: list repeats inner content, repeat repeats the host element.

data-sly-include — include another script

<sly data-sly-include="header.html"/>
<sly data-sly-include="${'template.html' @ prependPath='partials'}"/>
  • Replaces element content. Host element is not rendered.
  • Variables from current scope are not passed to included script.
  • AEM extension: wcmmode option controls WCM mode for included script.

data-sly-resource — include a resource (sub-request)

<div data-sly-resource="./header"></div>
<div data-sly-resource="${'./content' @ resourceType='myapp/components/text'}"></div>
<div data-sly-resource="${'./list' @ selectors='summary', removeSelectors}"></div>
  • Creates a new rendering context (separate request).
  • Options: resourceType, selectors, addSelectors, removeSelectors, appendPath, prependPath, requestAttributes.
  • AEM extension: accepts Map or Record objects with a resourceName property to create synthetic resources. If sling:resourceType is missing, falls back to resourceType option or current resource type.

data-sly-template / data-sly-call — reusable templates

<!--/* Define */-->
<template data-sly-template.card="${@ title, description, link}">
    <div class="card">
        <h3>${title}</h3>
        <p>${description}</p>
        <a href="${link}">Read more</a>
    </div>
</template>

<!--/* Call */-->
<sly data-sly-call="${card @ title='Hello', description=desc, link=url}"/>

<!--/* Load from external file */-->
<sly data-sly-use.lib="templates/cards.html"/>
<sly data-sly-call="${lib.card @ title='Hello', description=desc, link=url}"/>
  • Template element is never shown.
  • Missing parameters become empty string.
  • Scope is not inherited — pass everything via parameters.

data-sly-unwrap — remove host element

<div data-sly-unwrap>Content shows without div wrapper</div>
<div data-sly-unwrap="${wcmmode.edit}">Unwrapped in edit mode only</div>

data-sly-set — assign a variable

<sly data-sly-set.fullName="${profile.firstName} ${profile.lastName}"/>
<p>${fullName}</p>

Global scope after declaration.

The <sly> tag

A virtual container element — never renders in output. Use it to apply block logic without adding markup:

<sly data-sly-test="${showBanner}" data-sly-resource="./banner"/>

Block priority order

When multiple data-sly-* appear on the same element:

  1. template
  2. set, test, use
  3. call
  4. text
  5. element, include, resource
  6. unwrap
  7. list, repeat
  8. attribute

Same-priority → evaluated left-to-right.

Comments

<!--/* HTL comment: stripped from output entirely */-->
<!-- HTML comment: kept in output, expressions inside ARE evaluated -->

Common Patterns

Conditional CSS classes

<div class="${item.active ? 'nav-item active' : 'nav-item'}">
<div data-sly-attribute.class="${['nav-item', item.active ? 'active' : ''] @ join=' '}">

Safe link rendering

<a href="${page.path @ extension='html'}" title="${page.title}">${page.navTitle || page.title}</a>

Component with edit placeholder

<sly data-sly-use.model="com.example.MyModel"/>
<div data-sly-test="${model.hasContent}" class="my-component">
    ${model.text @ context='html'}
</div>
<div data-sly-test="${!model.hasContent && wcmmode.edit}" class="cq-placeholder">
    Configure this component
</div>

Recursive template

<template data-sly-template.nav="${@ items}">
    <ul data-sly-list="${items}">
        <li>
            ${item.title}
            <sly data-sly-call="${nav @ items=item.children}"/>
        </li>
    </ul>
</template>
<sly data-sly-call="${nav @ items=rootItems}"/>

Critical Rules

  1. Never use context='unsafe' unless explicitly required and security-reviewed.
  2. Script/style contexts require explicit context — expressions in <script> or <style> produce no output without @ context='scriptToken' etc.
  3. on* and style attributes cannot be set via data-sly-attribute.
  4. Ternary : requires spaces${a ? b : c} not ${a ? b:c}.
  5. == is strict — no type coercion. Compare same types.
  6. Prefer Sling Models over JS Use-API for AEM as a Cloud Service.
  7. data-sly-list hides the host element when collection is empty. Use data-sly-test first if you need fallback markup.
  8. Template scope is isolated — pass all needed data as parameters to data-sly-call.

Reference Files

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

aem-component-development

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

Agent Guardian

Agent体验守护系统。解决AI助手常见体验问题:长时间无响应、任务卡死、中英文混用、状态不透明。包含看门狗监控、智能状态汇报、即时状态查询、语言一致性过滤、消息队列追踪。适用于所有渠道(QQ/微信/Telegram/飞书/Discord等)。当用户抱怨"等太久没回复"、"回复中英文混着"、"不知道在干什么"时使...

Registry SourceRecently Updated
Automation

Proactive Agent V2

Transform AI agents from task-followers into proactive partners that anticipate needs and continuously improve. Now with WAL Protocol, Working Buffer, Autono...

Registry SourceRecently Updated