htl-scripting
npx skills add https://github.com/headwirecom/aem-agent-skills --skill htl-scripting
Agent 安装分布
Skill 文档
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 andstylecannot 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 isfoo, metadata isfooList. - 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:
wcmmodeoption 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
MaporRecordobjects with aresourceNameproperty to create synthetic resources. Ifsling:resourceTypeis missing, falls back toresourceTypeoption 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:
templateset,test,usecalltextelement,include,resourceunwraplist,repeatattribute
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
- Never use
context='unsafe'unless explicitly required and security-reviewed. - Script/style contexts require explicit context â expressions in
<script>or<style>produce no output without@ context='scriptToken'etc. on*andstyleattributes cannot be set viadata-sly-attribute.- Ternary
:requires spaces â${a ? b : c}not${a ? b:c}. ==is strict â no type coercion. Compare same types.- Prefer Sling Models over JS Use-API for AEM as a Cloud Service.
data-sly-listhides the host element when collection is empty. Usedata-sly-testfirst if you need fallback markup.- Template scope is isolated â pass all needed data as parameters to
data-sly-call.
Reference Files
- references/xss-contexts.md â Full XSS display context table with allowed values
- references/global-objects.md â AEM/Sling global objects, Use-API patterns, Sling Model integration