AEM Client Libraries (Clientlibs) Deep Dive
A developer and architect guide to AEM Client Libraries covering categories, proxy delivery, dependencies and embeds, HTL inclusion, authoring clientlibs, loading strategy, caching, production debugging, and modern frontend builds.
AEM Client Libraries (Clientlibs) Deep Dive
AEM components eventually produce HTML, but most production components also depend on CSS, JavaScript, fonts, icons, or other browser-side resources.
The difficult part is not adding a <script> or <link> tag. The
difficult part is deciding who owns those frontend resources, how they
are grouped, how one library depends on another, how resources stored
under /apps reach the browser, and how the same setup behaves through
Publish, Dispatcher, CDN, and browser caching.
That is the role of AEM Client Libraries, usually called Clientlibs.
A Clientlib is AEM's repository-level mechanism for organizing and delivering client-side resources. In a traditional or full-stack AEM Sites project, it becomes the delivery boundary between frontend artifacts stored with the application and the browser.
This chapter focuses on that runtime boundary. Chapter 33 will cover the build pipeline that may generate those artifacts before they become Clientlibs.
The Problem Clientlibs Solve
A simple site can directly include several frontend files:
<link rel="stylesheet" href="/css/reset.css">
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/product.css">
<script src="/js/vendor.js"></script>
<script src="/js/site.js"></script>
<script src="/js/product.js"></script>
In a large AEM application, that quickly creates harder questions.
Which component owns product.js? Should vendor.js load on every
template? What happens when several features require the same library?
How do resources stored under /apps reach anonymous Publish traffic
without opening /apps directly? How do we keep authoring-only
JavaScript out of the public site bundle?
Clientlibs give AEM a standard contract for these concerns.
A Clientlib can aggregate CSS and JavaScript, identify the library
through categories, expose application resources through
/etc.clientlibs, and participate in dependency or embedding
relationships.
Clientlibs do not solve every frontend problem automatically. Frontend compilation, module bundling, browser caching, Dispatcher/CDN caching, code splitting, and deployment are related concerns but different responsibilities.
Where Clientlibs Sit in AEM Delivery
In a typical full-stack AEM project:
- Frontend source is developed in the project.
- A frontend build may compile and bundle that source.
- The resulting CSS and JavaScript are placed into Clientlib folders
under
/apps. - AEM resolves the requested Clientlib categories.
- Public resources are delivered through
/etc.clientlibs. - Dispatcher/CDN and the browser can cache those resources.
The frontend build answers:
How do TypeScript, Sass, npm dependencies, bundling, and optimization produce deployable artifacts?
Clientlibs answer:
Once those artifacts are part of AEM, how are they organized and delivered?
That boundary becomes important when we move into the frontend build pipeline in Chapter 33.
Clientlib Folder Structure
A Clientlib is represented by a repository node of type:
cq:ClientLibraryFolder
A source-controlled project might contain:
/apps/myproject/clientlibs/clientlib-site
with a structure such as:
clientlib-site/
.content.xml
css.txt
js.txt
css/
site.css
js/
site.js
resources/
icons/
fonts/
The repository definition can look like:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[myproject.site]"
allowProxy="{Boolean}true"/>
CRX/DE is useful for inspecting the resulting repository structure or experimenting locally. Production Clientlibs should normally be maintained in source control and deployed with the application rather than created manually in CRX/DE.
css.txt and js.txt
AEM needs to know which files belong to the generated CSS and JavaScript libraries and in which order they should be processed.
That is the role of css.txt and js.txt.
#base=css
reset.css
layout.css
site.css
#base=js
vendor.js
site.js
#base defines the root used when resolving the listed files relative
to the Clientlib.
File order still matters where JavaScript has ordering assumptions or CSS relies on the cascade. A Clientlib should not replace proper frontend module design, but its generated file order must still be deterministic.
Categories Are the Clientlib Contract
The most important Clientlib property is usually categories.
categories = [myproject.site]
The category is the logical identifier used when a page or component asks AEM to include a library.
The repository path and category are different things:
Repository location:
/apps/myproject/clientlibs/clientlib-site
Category:
myproject.site
Consumers should depend on the category rather than hardcoding the repository path.
The physical Clientlib location can change while the category remains the stable inclusion contract.
Category Naming
A simple project might use:
myproject.site
myproject.dependencies
myproject.authoring
A larger application may have meaningful feature boundaries:
myproject.site
myproject.checkout
myproject.maps
myproject.authoring.product
The goal is not to create as many categories as possible.
If every small component creates a category, the page accumulates too many delivery boundaries and inclusion decisions. If the entire site is forced into one giant category, every page may pay for code it never uses.
Categories should represent useful ownership and delivery boundaries.
allowProxy and /etc.clientlibs
Application Clientlibs are stored under /apps.
Public visitors should not require direct access to /apps, and
Dispatcher configurations commonly restrict that repository path.
AEM therefore provides the Client Library Proxy Servlet.
With:
allowProxy = true
a Clientlib under /apps can be delivered through a public path
beginning with:
/etc.clientlibs/
For example, a Clientlib stored at:
/apps/myproject/clientlibs/clientlib-site
can be exposed through a URL shaped like:
/etc.clientlibs/myproject/clientlibs/clientlib-site.css
or the corresponding JavaScript URL.
The exact production URL may contain generated cache-key or minification segments depending on the AEM version and delivery setup.
The useful boundary is simple:
Application code remains under /apps; browser delivery happens
through /etc.clientlibs.
Static Resources
CSS can reference fonts, icons, and images. For proxied Clientlibs,
static resources that need public delivery should be placed under the
Clientlib's resources folder.
That gives AEM a supported proxy/rewrite boundary rather than requiring public access to arbitrary repository paths.
What allowProxy Does Not Do
allowProxy=true does not mean the library is automatically loaded on
every page. It does not infer JavaScript dependencies, provide lazy
loading, or make all caching concerns disappear.
It enables proxy access to the Clientlib. Loading and caching remain separate design decisions.
Loading Clientlibs Through HTL
AEM provides an HTL helper template for including Clientlibs.
<sly
data-sly-use.clientlib=
"/libs/granite/sightly/templates/clientlib.html">
<sly
data-sly-call="${clientlib.css @
categories='myproject.site'}"/>
</sly>
JavaScript can be included separately:
<sly
data-sly-use.clientlib=
"/libs/granite/sightly/templates/clientlib.html">
<sly
data-sly-call="${clientlib.js @
categories='myproject.site'}"/>
</sly>
Or both can be requested:
<sly
data-sly-use.clientlib=
"/libs/granite/sightly/templates/clientlib.html">
<sly
data-sly-call="${clientlib.all @
categories='myproject.site'}"/>
</sly>
This is HTL template usage, not JavaScript Use-API.
The category remains the contract between the rendering layer and the Clientlib.
dependencies and embed Are Different
These properties are often treated as if they mean the same thing.
They do not.
dependencies
Suppose:
myproject.product
requires:
myproject.base
The product Clientlib can declare:
dependencies = [myproject.base]
The dependency remains a separate Clientlib. Loading the product library causes the required category to participate in the output as a dependency.
Use this relationship when the other library remains an independently meaningful delivery unit.
embed
A consuming Clientlib can instead declare:
embed = [myproject.base]
The embedded code becomes part of the generated consuming library.
That changes the browser-delivery shape.
The design question is:
Should the other library remain an independent delivery unit, or should its code become part of this Clientlib?
For new AEM as a Cloud Service implementations, avoid creating deep Clientlib dependency/embed graphs without a clear reason. Modern frontend builds often provide a cleaner place to express JavaScript module dependencies and bundle boundaries.
Existing AEM 6.5 applications may use these properties extensively, so developers still need to understand them.
Clientlib Processors
AEM supports processing configuration through properties such as:
jsProcessor
cssProcessor
Older projects may contain configurations involving YUI or Google Closure Compiler.
Do not copy those configurations blindly into a new project.
The AEM version and build model matter. A modern frontend build may already own transpilation, minification, and other optimization before the generated artifacts reach the Clientlib.
The practical rule is:
Know which layer owns optimization.
Avoid processing the same responsibility in several layers simply because Clientlibs support processors.
Properties We Should Not Teach as Core Clientlib Design
The older notes contained properties such as serializationType and
presented them as standard Clientlib configuration.
That should not be carried into this chapter without a verified platform-specific contract.
The core design here stays with documented Clientlib behavior such as:
categories
allowProxy
dependencies
embed
cssProcessor
jsProcessor
Likewise, overriding Adobe libraries should not be presented as a generic Clientlib technique. Authoring UI customization should use the supported extension mechanisms for the AEM version being targeted.
Site Clientlibs vs Feature Clientlibs
A project eventually has to decide how frontend code is divided.
There are two common extremes.
One Giant Site Bundle
Everything goes into:
myproject.site
This is operationally simple, but every page can end up downloading code for features it never uses.
For a small site, that trade-off may be acceptable.
For a large application containing maps, checkout, calculators, video players, and other expensive features, it may not be.
A Clientlib for Every Component
The opposite design creates:
myproject.title
myproject.button
myproject.image
myproject.card
myproject.teaser
myproject.navigation
Now the page has many delivery boundaries and inclusion decisions.
That can become harder to maintain than the bytes it saves.
A Practical Boundary
Group frontend code by meaningful runtime behavior.
For example:
myproject.site
myproject.dependencies
myproject.maps
myproject.checkout
The global site category owns the baseline experience. Heavy or uncommon features can have separate boundaries where the performance benefit justifies them.
There is no universal correct number of Clientlibs.
Component-Specific Loading
A component can conditionally include a category:
<sly
data-sly-use.clientlib=
"/libs/granite/sightly/templates/clientlib.html">
<sly
data-sly-test="${model.enabled}"
data-sly-call="${clientlib.all @
categories='myproject.maps'}"/>
</sly>
This can be useful for an expensive feature that appears on only a small number of pages.
But component-level inclusion is not automatically a performance win.
Consider how frequently the component appears, whether several instances can render on one page, whether the library is already globally loaded, browser caching, dependency relationships, and whether JavaScript expects a particular DOM or loading order.
A loading strategy should be deliberate rather than scattering Clientlib calls through every component.
Page-Level Loading
Global site CSS and JavaScript usually belong to a page-level delivery decision.
The exact integration depends on the page component and project architecture.
The important ownership rule is that global frontend resources should be owned at page level rather than repeatedly included by individual content components.
A useful split is:
Page-level:
myproject.site
Feature-level:
myproject.maps
myproject.checkout
Authoring-only:
myproject.authoring.*
Authoring and Dialog Clientlibs
Authoring JavaScript is a different concern from public-site JavaScript.
A component dialog may need custom field validation, conditional visibility, initialization, or authoring events.
A dialog can reference additional categories through extraClientlibs.
For example:
myproject.authoring.product
with JavaScript such as:
(function ($, document) {
"use strict";
$(document).on(
"dialog-ready",
function () {
// initialize dialog behavior
}
);
})(Granite.$, document);
The important design rule is isolation.
Do not put dialog-only code into:
myproject.site
That code is needed on Author, not by anonymous visitors. Site JavaScript should likewise not assume Granite authoring APIs exist on Publish.
Authoring Mode Is Not an Environment
HTL can inspect WCM mode:
<sly data-sly-test="${wcmmode.edit}">
...
</sly>
That can be useful when rendering behavior genuinely differs in authoring mode.
But wcmmode.edit should not be described as an environment-specific
loading mechanism.
It describes a rendering mode.
Environment concerns are things such as local, development, stage, and
production. Those belong to deployment/configuration architecture rather
than wcmmode.
Caching: Separate the Layers
Clientlib caching is often explained too broadly.
There are several layers.
AEM Clientlib Resource
AEM generates or delivers the CSS/JavaScript representation for the requested Clientlib.
Dispatcher and CDN
In a full-stack AEM as a Cloud Service delivery model, /etc.clientlibs
resources can be cached through Dispatcher and CDN.
Browser Cache
The browser caches the resulting resource according to the delivered URL and cache headers.
Deployment and Cache Keys
AEM as a Cloud Service can produce Clientlib URLs containing long cache-key segments, for example:
clientlib-site.lc-<hash>-lc.min.css
That is more precise than saying every Clientlib is simply "fingerprinted based on content."
When frontend code appears stale, identify which layer is serving the stale representation before invalidating caches blindly.
Production Debugging --- Works on Author, Missing on Publish
A common failure is:
The page looks correct on Author, but CSS or JavaScript is missing on Publish.
Start with the browser.
Is the expected /etc.clientlibs/... request present?
If it is missing entirely, inspect category inclusion.
If the request returns 403 or 404, inspect the Clientlib path,
allowProxy, deployment contents, Dispatcher rules, and public resource
placement.
If the file loads but expected code is absent, inspect:
css.txt
js.txt
category resolution
frontend build output
generated Clientlib contents
Do not begin by clearing every cache.
First determine whether the failure is inclusion, resolution, delivery, content, or caching.
Production Debugging --- Category Exists but Nothing Loads
A Clientlib existing under /apps does not mean a page automatically
loads it.
Check the definition:
categories = [myproject.site]
and the consumer:
categories='myproject.site'
Category typos are easy to miss because the repository node can still look correct.
Also verify that the deployed package contains the expected Clientlib and, for generated Clientlibs, that the frontend build actually copied its artifacts into the package.
Production Debugging --- Hidden Dependencies
JavaScript can appear correct locally because another category happens to be present on the page.
Then a template or loading strategy changes and the hidden dependency becomes visible.
Before fixing that by adding another Clientlib dependencies entry,
decide which layer should own the relationship:
- frontend module/import graph,
- generated bundle,
- Clientlib dependency,
- or page-level loading contract.
Modern frontend builds usually provide a better place for JavaScript module dependencies than an increasingly complex Clientlib dependency graph.
A Repeatable Clientlib Troubleshooting Sequence
When a Clientlib problem reaches production:
- Check page source for the expected CSS/JS URL.
- Check the browser network response:
200,304,403, or404. - Confirm public application assets are requested through
/etc.clientlibs, not direct/appsURLs. - Verify the requested category exactly matches the Clientlib definition.
- Verify
allowProxywhere public proxy delivery is required. - Check that
css.txtorjs.txtlists the expected files. - Verify those files actually exist in the deployed Clientlib.
- If
ui.frontendgenerates the Clientlib, verify its build and copy/generation output. - Check Dispatcher/CDN behavior.
- Check the browser cache using the exact URL that was requested.
On local development systems, Clientlib debugging tools and
debugClientLibs=true can help inspect resolution and embedded content.
Those are debugging aids, not production architecture.
A Production Organization Example
A project might have:
/apps/myproject/clientlibs/
clientlib-site
clientlib-dependencies
clientlib-maps
clientlib-authoring
The names are not important by themselves.
The responsibilities are.
clientlib-site owns baseline site CSS and JavaScript.
clientlib-dependencies exists only if the build/delivery model
benefits from a separately delivered dependency bundle.
clientlib-maps owns a heavy feature that is not required on most
pages.
clientlib-authoring owns Author-only extensions.
This is an example, not a required AEM folder structure.
Clientlibs in Modern AEM Projects
Clientlibs do not mean frontend developers must hand-maintain production
CSS and JavaScript directly inside ui.apps.
A full-stack AEM project can use:
ui.frontend
as a separate frontend build module.
Developers work there with normal frontend tooling such as npm, webpack, Sass, TypeScript, JavaScript modules, and frontend dependencies.
The build produces optimized artifacts, and those artifacts can then be
copied or generated into Clientlibs under ui.apps.
That gives us two clean responsibilities.
Frontend Build Responsibility
Owns source compilation, module bundling, transpilation, frontend dependency resolution, and optimization.
Clientlib Responsibility
Owns the AEM repository representation, category contract, proxy delivery, page/component inclusion, and AEM-side delivery integration.
If webpack already owns the JavaScript dependency graph, reproducing that same graph through many Clientlib dependency entries usually makes the system harder to understand.
AEM as a Cloud Service Has More Than One Frontend Delivery Model
There is one more architecture point to keep separate from classic Clientlib delivery.
A full-stack AEM as a Cloud Service pipeline can build frontend
artifacts, generate Clientlibs under ui.apps, deploy them with the
application, and deliver them through /etc.clientlibs.
AEMaaCS also supports a dedicated front-end pipeline model where
frontend artifacts are deployed to the built-in CDN rather than
delivered through /etc.clientlibs.
So Clientlibs remain essential for existing AEM applications and full-stack delivery, but an architect should not assume that every new Cloud Service frontend uses the same delivery path.
The project's deployment model decides that boundary.
Chapter 33 will cover this build and deployment decision in detail.
Architect Review Checklist
Before approving a Clientlib design, I check:
- Are categories named by responsibility rather than implementation accident?
- Are public application Clientlibs stored under
/appsand exposed through the supported proxy mechanism? - Is
allowProxyconfigured where public delivery requires it? - Are public static resources kept under the Clientlib
resourcesboundary? - Is global site code owned at page level?
- Are expensive feature bundles separated only where there is a real benefit?
- Is authoring/dialog JavaScript isolated from public-site JavaScript?
- Are
dependenciesandembedused intentionally? - Does the frontend build own JavaScript module dependency resolution where appropriate?
- Are Clientlib processors compatible with the actual AEM version and build pipeline?
- Can the team trace a production Clientlib from page source back to category, repository definition, and frontend build output?
- Is caching understood across browser, Dispatcher, and CDN?
- Does the chosen AEMaaCS frontend deployment model actually use Clientlibs or the dedicated frontend pipeline?
Summary
Clientlibs are not just folders containing CSS and JavaScript.
They are an AEM delivery contract.
Categories identify frontend delivery responsibilities.
/apps owns application code while /etc.clientlibs provides the
public proxy path for Clientlib delivery.
dependencies and embed represent different relationships and
should not be used interchangeably.
Global, feature-specific, and authoring frontend code need deliberate ownership boundaries.
Caching must be understood across AEM, Dispatcher/CDN, and the browser rather than treated as one automatic Clientlib feature.
Modern frontend tooling can generate the artifacts while Clientlibs handle AEM integration and delivery.
That last boundary leads directly into the next chapter.
What's Next
Chapter 33 --- AEM Frontend Build Pipeline
The next chapter follows frontend code before it becomes an AEM runtime resource:
frontend source
npm / webpack
build output
Clientlib generation
Maven packaging
CI/CD
AEM deployment
That is where ui.frontend, build tooling, generated Clientlibs,
pipeline ownership, and deployment strategy fit.
Enjoyed this chapter?
Get an email when I publish the next chapter. No spam — just new technical deep-dives.
Comments
Share feedback or questions about this blog post.
No comments yet. Be the first to share your thoughts.