ISML Development (SFRA + Bootstrap 4)
This skill is SFRA-only. It intentionally avoids SiteGenesis patterns.
SFRA storefront markup is built around Bootstrap 4 (grid, forms, alerts, utilities). Keep your ISML consistent with existing SFRA markup and CSS conventions.
When to Use
- •You’re editing
templates/default/**in an SFRA cartridge. - •You need to apply/extend SFRA layout (
common/layout/page/common/layout/checkout). - •You’re building reusable components via
<isinclude>and Bootstrap 4 markup. - •You’re debugging output encoding, caching, or remote include behaviour.
When NOT to Use
- •For business logic, data fetching, persistence, or complex transformations (belongs in controllers/models/scripts).
- •For redesigning UI with a different framework (SFRA baseline expects Bootstrap 4).
Quick Checklist
[ ] No business logic in ISML (no data fetches, no persistence) [ ] <isscript> used only for SFRA asset registration (CSS/JS) [ ] Output encoding is NOT disabled unless justified and safe [ ] <iscontent>/<isredirect>/<iscache> placement constraints respected [ ] Remote includes used sparingly with security middleware [ ] Template receives all data via pdict (controller owns data) [ ] Markup uses Bootstrap 4 classes (grid, forms, alerts)
Where Templates Live (SFRA)
/my-cartridge/cartridge/templates/
/default # Default templates (SFRA)
/product/detail.isml
/util/modules.isml # Optional: <ismodule> tag definitions
/fr_FR # Locale overrides (use sparingly)
SFRA Golden Rule: Templates Are Presentation-Only
NEVER use <isscript> for business logic. The only exception is asset management:
<!-- ✅ Only acceptable use of isscript -->
<isscript>
var assets = require('*/cartridge/scripts/assets.js');
assets.addCss('/css/product.css');
assets.addJs('/js/product.js');
</isscript>
All data comes from controllers via pdict:
<!-- ✅ Correct: Data from controller -->
<div class="price h5 mb-0">${pdict.product.price.sales.formatted}</div>
Bootstrap 4 Conventions (SFRA UI Baseline)
Use these classes/patterns by default:
| Goal | Bootstrap 4 pattern |
|---|---|
| Layout | container → row → col-* |
| Spacing | mt-*, mb-*, py-*, px-* |
| Buttons | btn btn-primary, btn btn-outline-primary |
| Alerts | alert alert-danger, alert alert-success |
| Forms | form-group, form-control, is-invalid, invalid-feedback |
| Responsive show/hide | d-none d-md-block, d-md-none |
Avoid mixing Bootstrap 5-only classes (e.g. g-*, row-cols-*, btn-close, badge bg-*) into SFRA baseline templates.
Essential Tags
Conditional Logic
<isif condition="${pdict.product.available}">
<span class="badge badge-success">${Resource.msg('label.instock','product',null)}</span>
<iselseif condition="${pdict.product.preorderable}">
<span class="badge badge-info">${Resource.msg('label.preorder','product',null)}</span>
<iselse>
<span class="badge badge-secondary">${Resource.msg('label.outofstock','product',null)}</span>
</isif>
Loops
<isloop items="${products}" var="product" status="loopstate">
<div class="col-6 col-sm-4 col-lg-3 mb-3">
<div class="card h-100">
<div class="card-body">
<div class="small text-muted">#${loopstate.count}</div>
<div class="font-weight-bold">${product.name}</div>
</div>
<isif condition="${loopstate.first}">
<div class="card-footer bg-transparent">
<span class="badge badge-warning">${Resource.msg('label.featured','common',null)}</span>
</div>
</isif>
</div>
</div>
</isloop>
Loop status: count (1-based), index (0-based), first, last, odd, even
Variables
<isset name="productName" value="${product.name}" scope="page"/>
<span>${productName}</span>
<isremove name="productName" scope="page"/>
Scopes (required): page, request, session, pdict
Output
<!-- HTML encoded (default) -->
<isprint value="${product.name}"/>
<!-- Unencoded (use carefully) -->
<isprint value="${htmlContent}" encoding="off"/>
<!-- Formatted -->
<isprint value="${price}" style="CURRENCY"/>
<isprint value="${order.creationDate}" style="DATE_SHORT"/>
Includes
<!-- Local include (shared pdict) -->
<isinclude template="product/components/price"/>
<!-- Remote include (isolated, own cache) -->
<isinclude url="${URLUtils.url('Product-GetPrice', 'pid', product.ID)}"/>
Prefer local includes for components. Only use remote includes when you need an independent cache policy or request isolation; see the remote include reference.
Decorator Pattern
Decorator template (common/layout/page.isml):
<!DOCTYPE html>
<html>
<head><title>${pdict.pageTitle}</title></head>
<body>
<isinclude template="components/header"/>
<main>
<isreplace/> <!-- Content inserted here -->
</main>
<isinclude template="components/footer"/>
</body>
</html>
Using decorator:
<isdecorate template="common/layout/page">
<div class="content">
<h1>${pdict.welcomeMessage}</h1>
</div>
</isdecorate>
SFRA provides two primary decorators:
- •
common/layout/page- Standard storefront pages - •
common/layout/checkout- Checkout process pages
Use <isdecorate> on almost all storefront templates; keep full <html>/<head>/<body> only in layout templates.
Tag Location Constraints
| Tag | Constraint |
|---|---|
<iscontent> | Must be before DOCTYPE |
<isredirect> | Must be before DOCTYPE |
<iscache> | Place at beginning |
<isreplace/> | Only inside decorator templates |
<isbreak>/<iscontinue>/<isnext> | Only inside <isloop> |
Caching
<!-- Cache for 24 hours --> <iscache type="relative" hour="24"/> <!-- Daily cache (expires at midnight) --> <iscache type="daily" hour="0" minute="0"/> <!-- Vary by price/promotion --> <iscache type="relative" hour="1" varyby="price_promotion"/>
Place <iscache> at the beginning of the template.
Prefer controller caching middleware (SFRA */cartridge/scripts/middleware/cache) for page-level caching decisions; use <iscache> selectively for fragments.
Content Type
<!-- Must be first in template --> <iscontent type="text/html" charset="UTF-8"/> <!-- For JSON --> <iscontent type="application/json" charset="UTF-8"/>
Custom Modules
Define in util/modules.isml:
<ismodule template="components/productcard"
name="productcard"
attribute="product"
attribute="showPrice"/>
Use anywhere:
<isinclude template="util/modules"/>
<isproductcard product="${product}" showPrice="${true}"/>
In SFRA, most reuse is done via <isinclude template="...">. Use <ismodule> custom tags only when it measurably improves readability/consistency.
Security: XSS Prevention
Always rely on default encoding. <isprint> automatically HTML-encodes:
<!-- ✅ Secure (default) -->
<isprint value="${pdict.searchPhrase}" />
<!-- ❌ Vulnerable -->
<isprint value="${pdict.searchPhrase}" encoding="off" />
For JavaScript context, use SecureEncoder:
<script>
var term = "${require('dw/util/SecureEncoder').forJavaScript(pdict.searchPhrase)}";
</script>
For HTML attributes, prefer SecureEncoder.forHTMLAttribute(...). Avoid encoding="off" unless the value is fully controlled and intentionally contains HTML.
Built-in Utilities
Pre-imported, use directly:
<!-- URLUtils -->
<a href="${URLUtils.url('Product-Show', 'pid', product.ID)}">View</a>
<img src="${URLUtils.staticURL('/images/logo.png')}" alt="Logo"/>
<!-- Resource (localization) -->
${Resource.msg('button.addtocart', 'product', null)}
${Resource.msgf('cart.items', 'cart', null, cartCount)}
<!-- StringUtils -->
${StringUtils.truncate(description, 100, '...')}
Expressions
<!-- Property access -->
${pdict.product.name}
${pdict.product.price.sales.value}
<!-- Method calls -->
${product.getAvailabilityModel().isInStock()}
<!-- Operators -->
${price > 100 ? 'expensive' : 'affordable'}
${firstName + ' ' + lastName}
Comments
<!-- HTML comment (visible in source) -->
<iscomment>
ISML comment - stripped from output.
Use for sensitive documentation.
</iscomment>
Best Practices
- •Use Bootstrap 4 components/utilities for UI consistency
- •Use
<isdecorate>with SFRA layouts (common/layout/page,common/layout/checkout) - •Keep templates “dumb”: controllers/models prepare view models; templates render
- •Use
<isinclude template>for component composition; keep components small - •Default encoding prevents XSS; use
SecureEncoderfor JS/attribute contexts - •Minimize remote includes; when used, require
server.middleware.include+ explicit security + cache policy - •Prefer controller caching middleware; use
<iscache>thoughtfully
Common SFRA Form Pattern (Bootstrap 4)
<form action="${URLUtils.url('Account-Login')}" method="post" class="login">
<input type="hidden" name="${pdict.csrf.tokenName}" value="${pdict.csrf.token}"/>
<div class="form-group">
<label class="form-control-label" for="login-email">
${Resource.msg('label.login.email','login',null)}
</label>
<input
id="login-email"
type="email"
class="form-control <isif condition="${pdict.loginForm.username.invalid}">is-invalid</isif>"
name="${pdict.loginForm.username.htmlName}"
value="${pdict.loginForm.username.value}"
autocomplete="email"
required>
<isif condition="${pdict.loginForm.username.invalid}">
<div class="invalid-feedback">
<isprint value="${pdict.loginForm.username.error}"/>
</div>
</isif>
</div>
<button type="submit" class="btn btn-primary btn-block">
${Resource.msg('button.login','login',null)}
</button>
</form>
MCP ISML Tools
search_isml_elements("loop") // Find by intent
get_isml_element("isprint") // Full documentation
list_isml_elements() // All elements by category
Related Skills
- •sfcc-localization - Essential companion skill for localizing template text. Covers resource bundles,
Resource.msg(), parameterized messages, locale fallback, and internationalization best practices. Always use resource bundles instead of hardcoded text in templates.
Detailed References
- •Remote Includes - Local vs remote, caching, security
- •Utilities & Expressions - Built-in objects, expression syntax
- •SFRA Base Templates (Index) - Jumping-off point
- •SFRA Layouts - Decorators, hooks,
<isreplace/> - •SFRA Structure & Components - Where templates live, composition patterns
- •SFRA Pages: Catalog/Search - Homepage/PDP/search/category patterns
- •SFRA Pages: Cart/Account/Auth - Cart/account/login patterns
External Reference
- •Legacy long-form doc (for deeper examples): https://raw.githubusercontent.com/taurgis/sfcc-dev-mcp/refs/heads/main/docs/best-practices/isml_templates.md