N
Naveenr.dev
Chapter 32
11 min read•2026-10-04

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:

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

  1. Frontend source is developed in the project.
  2. A frontend build may compile and bundle that source.
  3. The resulting CSS and JavaScript are placed into Clientlib folders under /apps.
  4. AEM resolves the requested Clientlib categories.
  5. Public resources are delivered through /etc.clientlibs.
  6. 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:

text
cq:ClientLibraryFolder

A source-controlled project might contain:

text
/apps/myproject/clientlibs/clientlib-site

with a structure such as:

text
clientlib-site/
    .content.xml
    css.txt
    js.txt
    css/
        site.css
    js/
        site.js
    resources/
        icons/
        fonts/

The repository definition can look like:

xml
<?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.

text
#base=css

reset.css
layout.css
site.css
text
#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.

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

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

text
myproject.site
myproject.dependencies
myproject.authoring

A larger application may have meaningful feature boundaries:

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

text
allowProxy = true

a Clientlib under /apps can be delivered through a public path beginning with:

text
/etc.clientlibs/

For example, a Clientlib stored at:

text
/apps/myproject/clientlibs/clientlib-site

can be exposed through a URL shaped like:

text
/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.

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

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

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

text
myproject.product

requires:

text
myproject.base

The product Clientlib can declare:

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

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

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

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

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

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

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

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

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

text
myproject.authoring.product

with JavaScript such as:

javascript
(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:

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

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

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

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

text
categories = [myproject.site]

and the consumer:

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

  1. Check page source for the expected CSS/JS URL.
  2. Check the browser network response: 200, 304, 403, or 404.
  3. Confirm public application assets are requested through /etc.clientlibs, not direct /apps URLs.
  4. Verify the requested category exactly matches the Clientlib definition.
  5. Verify allowProxy where public proxy delivery is required.
  6. Check that css.txt or js.txt lists the expected files.
  7. Verify those files actually exist in the deployed Clientlib.
  8. If ui.frontend generates the Clientlib, verify its build and copy/generation output.
  9. Check Dispatcher/CDN behavior.
  10. 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:

text
/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:

text
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 /apps and exposed through the supported proxy mechanism?
  • Is allowProxy configured where public delivery requires it?
  • Are public static resources kept under the Clientlib resources boundary?
  • 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 dependencies and embed used 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:

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