htl-scripting

📁 headwirecom/aem-agent-skills 📅 2 days ago
1
总安装量
1
周安装量
#45080
全站排名
安装命令
npx skills add https://github.com/headwirecom/aem-agent-skills --skill htl-scripting

Agent 安装分布

amp 1
opencode 1
kimi-cli 1
codex 1
github-copilot 1
gemini-cli 1

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 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