N
Naveenr.dev
Chapter 33
13 min read•2026-10-04

AEM Frontend Build Pipeline — From ui.frontend Source to Browser

A developer and architect guide to the AEM frontend build pipeline, covering ui.frontend, npm, webpack, generated Clientlibs, Maven integration, local development, Cloud Manager deployment, and production troubleshooting.

AEM Frontend Build Pipeline — From ui.frontend Source to Browser

Chapter 32 covered Clientlibs from the AEM runtime side: categories, /etc.clientlibs, proxying, loading, caching, and ownership.

There is still an important question left.

Where do the CSS and JavaScript inside those Clientlibs actually come from?

In a modern AEM project, frontend developers usually do not write production bundles directly inside ui.apps. They work with JavaScript or TypeScript, SCSS, npm packages, linting, source maps, and a bundler such as webpack.

That source has to become something AEM can package and deliver.

This is where ui.frontend fits.

The important thing to understand is not every webpack option. It is the boundary between the frontend build and AEM:

ui.frontend owns frontend source and build-time processing.

ui.apps owns the AEM repository representation of the generated application artifacts.

Once that boundary is clear, the build pipeline becomes much easier to debug.

Why AEM Separates Frontend Source from ui.apps

AEM developers traditionally work with repository structures such as:

text
/apps/myproject/components
/apps/myproject/clientlibs
/conf/myproject

Frontend development has a different toolchain.

A frontend developer may need:

text
TypeScript
JavaScript modules
SCSS
npm dependencies
linting
source maps
bundling
minification
local development server

Trying to make ui.apps act as both an AEM repository package and a modern frontend workspace mixes two different responsibilities.

ui.frontend gives the frontend code its own build environment.

A typical project therefore contains modules such as:

text
myproject/
    core/
    ui.apps/
    ui.content/
    ui.config/
    ui.frontend/
    all/
    pom.xml

Not every AEM project contains exactly this structure. The AEM Project Archetype can generate a dedicated frontend module for full-stack projects, and its exact files and module layout change across archetype versions. Treat the structure below as a responsibility map, not as a version-independent contract.

The Build Flow We Need to Understand

For a full-stack project using generated Clientlibs, think about the build in stages rather than as one Maven command.

  1. Developers maintain frontend source in ui.frontend.
  2. npm installs the frontend dependencies (npm ci is commonly used by the Maven build for reproducibility).
  3. webpack processes the source.
  4. webpack writes build output to dist.
  5. Clientlib generation converts that output into AEM Clientlib structure.
  6. Generated Clientlibs are written into ui.apps.
  7. Maven packages the AEM modules.
  8. The application package is deployed.
  9. AEM exposes the resulting Clientlibs through /etc.clientlibs.
  10. Dispatcher/CDN and the browser deliver/cache those resources.

This is one of the most useful troubleshooting models for the frontend side of AEM.

If the browser has the wrong CSS, the failure could have happened at several different stages.

Do not immediately blame Clientlibs.

The wrong source may have been compiled. The webpack build may have failed. The expected file may never have reached dist. Clientlib generation may have copied the wrong output. Maven may have packaged stale artifacts. The deployment may not contain the new package. Or the correct file may be deployed while an older cached response is still being served.

The pipeline tells us where to look.

What ui.frontend Owns

The exact structure varies by archetype version and project customization, but a webpack-based module commonly contains files such as:

text
ui.frontend/
    src/
    package.json
    package-lock.json
    tsconfig.json
    webpack.common.js
    webpack.dev.js
    webpack.prod.js
    clientlib.config.js
    pom.xml
    dist/

Each one belongs to a different part of the build.

src

This is the source area.

A project may organize it around site and component code:

text
src/main/webpack/
    site/
        main.ts
        main.scss
    components/
        header/
        teaser/
        product-card/
    resources/

The important rule is ownership: developers edit source here.

Generated output should not become the primary development source.

package.json

package.json defines the Node-based build contract.

It normally contains:

  • frontend dependencies,
  • development dependencies,
  • npm scripts,
  • webpack tooling,
  • TypeScript/Sass tooling where used,
  • Clientlib generation tooling where that model is used.

A simplified, illustrative example might look like this:

json
{
  "scripts": {
    "start": "webpack serve --config webpack.dev.js",
    "dev": "webpack --config webpack.dev.js && clientlib",
    "prod": "webpack --config webpack.prod.js && clientlib"
  }
}

Do not copy these script names blindly. They vary across archetype versions and projects.

Read the project's package.json first.

That file tells you what npm run dev, npm run prod, or another project-specific command actually executes.

What Webpack Is Doing Here

Webpack is not an AEM feature.

It is the frontend build system sitting before AEM packaging.

Its job can include:

  • resolving JavaScript/TypeScript modules,
  • compiling TypeScript,
  • processing SCSS,
  • bundling dependencies,
  • extracting CSS,
  • copying static resources,
  • generating source maps,
  • applying development or production optimizations.

AEM does not need to understand the original TypeScript or SCSS source.

It needs the resulting browser-ready artifacts.

That is why the output directory matters.

webpack.common.js

Projects commonly keep shared build behavior in webpack.common.js.

A simplified configuration might define an entry point:

javascript
const path = require("path");

const SOURCE_ROOT = path.resolve(
    __dirname,
    "src/main/webpack"
);

module.exports = {
    entry: {
        site: path.join(
            SOURCE_ROOT,
            "site/main.ts"
        )
    },

    output: {
        path: path.resolve(__dirname, "dist")
    }
};

The entry point answers:

Where does webpack begin constructing the frontend dependency graph?

The output path answers:

Where should the generated artifacts go?

Real configurations contain more loaders and plugins, but these two concepts are enough to understand the AEM integration.

Development and Production Builds Are Different

A project commonly separates development and production webpack configuration.

Development

Development configuration may prioritize:

text
source maps
faster compilation
local development server
live reload
easier debugging

Production

Production configuration normally prioritizes:

text
optimized output
minification
smaller bundles
production-safe source maps or no source maps
deterministic build artifacts

This is why we should not describe npm run prod as simply another way to copy source files.

Its purpose is normally to produce production-oriented frontend output.

The exact optimization behavior still comes from the project's webpack configuration.

Adobe's current WKND full-stack example uses webpack.common, webpack.dev.js, and webpack.prod.js; the production configuration enables production optimization/minification. Those filenames are common in archetype-based projects, but the responsibility matters more than memorizing the filenames.

The dist Directory Is a Build Boundary

After webpack runs, the generated artifacts commonly appear under:

text
ui.frontend/dist

For example:

text
dist/
    clientlib-site/
        site.css
        site.js
        resources/

dist is not the final AEM repository location.

It is the output of the frontend build.

That distinction is useful when debugging.

If site.css is already wrong in dist, the problem is on the frontend-build side.

If dist/site.css is correct but the generated Clientlib is wrong, investigate Clientlib generation.

If the generated Clientlib is correct but Publish serves an older file, move further down the pipeline.

This is much faster than treating the entire build as one black box.

From dist to AEM Clientlibs

In the standard full-stack Clientlib model documented by Adobe, generated webpack output is transformed into AEM Clientlibs before ui.apps is packaged.

AEM Project Archetype-based implementations use aem-clientlib-generator for this integration.

The generator takes build output and creates the Clientlib representation expected by AEM.

That can include:

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

The generated Clientlib is placed under the ui.apps repository structure.

For example:

text
ui.apps/
    src/main/content/jcr_root/
        apps/myproject/clientlibs/
            clientlib-site/

This is the handoff between the Node/frontend world and the AEM/FileVault world.

Understanding clientlib.config.js

In projects using aem-clientlib-generator, clientlib.config.js defines how frontend output becomes Clientlibs.

A simplified configuration can look like:

javascript
const path = require("path");

const BUILD_DIR = path.join(
    __dirname,
    "dist"
);

const CLIENTLIB_DIR = path.join(
    __dirname,
    "..",
    "ui.apps",
    "src",
    "main",
    "content",
    "jcr_root",
    "apps",
    "myproject",
    "clientlibs"
);

module.exports = {
    context: BUILD_DIR,
    clientLibRoot: CLIENTLIB_DIR,

    libs: [
        {
            name: "clientlib-site",
            allowProxy: true,
            categories: ["myproject.site"],

            assets: {
                js: {
                    cwd: "clientlib-site",
                    files: ["**/*.js"],
                    flatten: false
                },

                css: {
                    cwd: "clientlib-site",
                    files: ["**/*.css"],
                    flatten: false
                },

                resources: {
                    cwd: "clientlib-site",
                    files: ["**/*.*"],
                    flatten: false,
                    ignore: [
                        "**/*.js",
                        "**/*.css"
                    ]
                }
            }
        }
    ]
};

Do not focus on memorizing the JavaScript object.

Look at the responsibilities.

context says where generated frontend output comes from.

clientLibRoot says where generated AEM Clientlibs should be written.

name defines the generated Clientlib folder.

categories defines the AEM category contract.

allowProxy controls proxy delivery.

assets maps generated JavaScript, CSS, and resources into the Clientlib.

This file is effectively the bridge between Chapter 33 and Chapter 32.

Do Not Manually Edit Generated Clientlibs

This is an easy mistake.

A developer finds:

text
ui.apps/.../clientlibs/clientlib-site/site.css

changes the generated CSS directly, installs ui.apps, and sees the fix.

Then someone runs the frontend build.

The manual change disappears.

That is expected.

If ui.frontend owns generation, the source of truth is upstream.

The fix belongs in the SCSS, TypeScript, JavaScript, webpack configuration, or generation configuration that produced the Clientlib.

Generated output should be treated as build output unless the project deliberately uses a different ownership model.

Component Frontend Code

A frontend module does not mean all code has to live in one enormous site.js.

A project can organize source by component:

text
components/
    navigation/
        navigation.ts
        navigation.scss

    teaser/
        teaser.ts
        teaser.scss

    product-card/
        product-card.ts
        product-card.scss

A site entry point can import those modules:

typescript
import "../components/navigation/navigation";
import "../components/teaser/teaser";
import "../components/product-card/product-card";

import "./main.scss";

SCSS can follow a similar composition strategy.

This gives developers component ownership in source while webpack still decides how the production bundles are emitted.

That distinction matters.

Source organization and browser bundle organization do not have to be identical.

Twenty component source folders do not require twenty Clientlibs.

Webpack can compose many source modules into a smaller number of runtime bundles.

npm Dependencies vs Clientlib Dependencies

This boundary causes confusion in older AEM projects.

Suppose frontend code imports a package:

typescript
import Swiper from "swiper";

That is a frontend module dependency.

npm resolves the package. Webpack decides how it participates in the generated bundle.

That is different from:

text
dependencies = [myproject.dependencies]

on a Clientlib.

The first relationship exists during frontend build time.

The second exists at the AEM Clientlib delivery layer.

If webpack already owns the JavaScript module graph, do not automatically reproduce every npm dependency as an AEM Clientlib dependency.

Choose one layer deliberately.

Maven Is the Orchestrator

A developer can work directly inside ui.frontend using npm.

But the complete AEM project is normally built with Maven.

In the Adobe-documented full-stack archetype flow, frontend-maven-plugin in ui.frontend/pom.xml orchestrates the frontend build, including webpack bundling and Clientlib generation, as part of the Maven build.

Conceptually, Maven triggers the Node/npm work, the frontend build produces output, Clientlib generation writes the AEM Clientlibs into ui.apps, and the AEM packages are then assembled.

From the project root, a local build may be:

bash
mvn clean install

A project may also provide profiles for installing the resulting package to a local AEM instance.

The exact command should come from that project's README and POM configuration rather than being treated as universal.

The architecture point is more important:

Maven is not compiling SCSS itself.

It is orchestrating a build in which the frontend module performs frontend work before AEM packages are finalized.

Why Build Order Matters

Imagine Maven packages ui.apps before the frontend module has generated the latest Clientlibs.

The package can contain stale frontend artifacts.

That is why the module relationship and Maven lifecycle matter.

When debugging CI failures, check the order:

  1. Was Node installed?
  2. Were npm dependencies installed?
  3. Did the webpack build run?
  4. Did Clientlib generation run?
  5. Were generated files present before ui.apps packaging?
  6. Did the final AEM package include them?

A green Maven build is valuable, but knowing these stages tells us what the build actually proved.

Local Frontend Development

One advantage of ui.frontend is that frontend developers do not need a complete AEM package deployment for every CSS adjustment.

A development webpack configuration can provide a local development server, source maps, and live reload.

Some archetype-based development setups can also proxy AEM paths such as:

text
/content
/etc.clientlibs

to a local AEM instance.

This creates a useful feedback loop for frontend work.

But the local webpack server is not the production runtime.

Before considering a feature complete, test the generated artifacts through the actual AEM path as well:

text
ui.frontend
→ generated Clientlib
→ ui.apps
→ AEM
→ /etc.clientlibs

That catches integration problems that a standalone webpack page cannot.

Two Useful Local Development Loops

Teams often need both.

Fast Frontend Loop

Use the frontend module directly.

Useful for:

  • SCSS work,
  • TypeScript/JavaScript development,
  • linting,
  • source maps,
  • frontend unit tests,
  • rapid visual iteration.

AEM Integration Loop

Run the frontend build, generate Clientlibs, package/install the application, and test through AEM.

Useful for:

  • HTL integration,
  • Clientlib category loading,
  • proxy delivery,
  • component markup integration,
  • authoring behavior,
  • Dispatcher-like path assumptions.

The fast loop improves developer speed.

The integration loop proves the real application boundary.

Full-Stack Pipeline Delivery

For a traditional full-stack AEM as a Cloud Service project, the frontend build participates in the application build.

The generated Clientlibs are packaged under ui.apps, deployed with the application, and delivered from AEM through URLs under:

text
/etc.clientlibs

This model couples frontend deployment to the full-stack application pipeline.

That can be completely appropriate when frontend and backend changes need to move together.

But it has an operational consequence.

A CSS-only change may still require the full application pipeline.

That is one reason AEM as a Cloud Service also supports a dedicated frontend pipeline model.

Dedicated Front-End Pipeline in AEM as a Cloud Service

AEM as a Cloud Service also supports a front-end pipeline in Cloud Manager.

This is not simply the same full-stack build with a faster deployment command.

The delivery contract changes.

In Adobe's front-end pipeline model, Cloud Manager builds only the frontend code from the configured ui.frontend code location and deploys the resulting frontend resources directly to the built-in CDN. The browser no longer receives those frontend resources through the traditional /etc.clientlibs path.

A full-stack Clientlib delivery model is roughly:

text
ui.frontend source
webpack build
Clientlib generation
ui.apps
full-stack Cloud Manager pipeline
AEM
/etc.clientlibs
browser

With the dedicated front-end pipeline, the frontend delivery side becomes:

text
ui.frontend source
Cloud Manager front-end pipeline
built-in CDN
browser

The second model requires the AEM project to be prepared for the front-end pipeline contract. Adobe's current WKND guidance updates the full-stack project so AEM knows which CDN-hosted CSS and JavaScript resources belong in the rendered page. Configuration such as SiteConfig and HtmlPageItemsConfig participates in that integration.

After deployment, the generated frontend resources are served from an Adobe-managed static-...adobeaemcloud.com hostname rather than /etc.clientlibs.

That distinction matters operationally.

In the full-stack model, frontend artifacts become AEM Clientlibs and are deployed with the application.

In the dedicated front-end pipeline model, frontend artifacts have an independent CDN delivery lifecycle, while the AEM application contains the configuration needed to reference those resources.

Do not remove existing Clientlibs and switch the pipeline in isolation. Moving an existing project from full-stack Clientlib delivery to the dedicated front-end pipeline is a migration of the frontend delivery contract and should be planned and tested as such.

When a Dedicated Frontend Pipeline Helps

The dedicated frontend pipeline becomes interesting when the frontend has a deployment lifecycle that should not always wait for backend/AEM application deployment.

For example, a team may want:

  • faster frontend-only deployments,
  • independent frontend release cadence,
  • CDN-oriented frontend delivery,
  • less coupling between CSS/JS changes and the full-stack pipeline.

That does not mean every project should migrate immediately. Existing Clientlib-based implementations may require project changes before they satisfy the front-end pipeline contract.

Architecture should consider:

  • current Clientlib assumptions,
  • page integration,
  • deployment governance,
  • team ownership,
  • rollback expectations,
  • cache behavior,
  • compatibility with the existing project,
  • whether frontend and backend releases genuinely need independent cadence.

The pipeline choice is an operational architecture decision, not just a frontend preference.

Production Failure — Local Build Works, Maven Fails

This is common enough to deserve its own troubleshooting path.

A developer runs:

bash
npm run prod

successfully.

Then:

bash
mvn clean install

fails in ui.frontend.

Check:

  • Node/npm versions used by Maven versus the developer shell,
  • lock-file consistency,
  • frontend-maven-plugin configuration,
  • dependency installation,
  • environment-specific scripts,
  • whether CI starts from a clean workspace,
  • whether generated files were accidentally relied on locally.

A local node_modules directory can hide dependency problems that appear immediately in CI.

A clean build is the better test of reproducibility.

Production Failure — Build Passes but New CSS Is Missing

Follow the artifact.

First inspect the source.

Then inspect:

text
ui.frontend/dist

If the CSS is missing there, investigate webpack entry points, imports, loaders, or build configuration.

If dist is correct, inspect the generated Clientlib under ui.apps.

If that is correct, inspect the built AEM package.

If the package is correct, inspect the deployed /etc.clientlibs response.

If the response is correct but the browser still renders old CSS, inspect CDN and browser caching.

This is why understanding the build stages is more useful than repeatedly running Maven.

Production Failure — Generated Clientlib Is Empty

If webpack succeeds but the generated Clientlib has no expected assets, inspect the boundary between dist and clientlib.config.js.

Typical causes include:

  • wrong context,
  • wrong cwd,
  • file glob does not match generated output,
  • webpack output folder changed,
  • Clientlib generator configuration was not updated,
  • entry point stopped producing the expected asset.

The frontend build and Clientlib generation can both be individually valid while their contract is broken.

Production Failure — Works Through Webpack Dev Server but Not AEM

A standalone development page can hide AEM integration problems.

Check:

  • Clientlib category inclusion,
  • /etc.clientlibs delivery,
  • allowProxy,
  • paths to fonts/images,
  • assumptions about DOM generated by HTL,
  • author vs publish behavior,
  • context paths and absolute URLs,
  • Clientlib resources placement.

A webpack dev server proves frontend code can run.

It does not prove AEM is delivering it correctly.

Production Failure — Works on Author but Not Publish

At this point, separate build problems from runtime-delivery problems.

If the generated package contains the correct Clientlib on both tiers, the frontend build has probably done its job.

Move the investigation to:

  • page inclusion,
  • /etc.clientlibs request,
  • Dispatcher filters,
  • CDN/cache behavior,
  • Publish deployment state,
  • public resource paths.

This is where Chapter 32 and Chapter 33 meet.

Chapter 33 helps us prove the artifact was built and packaged correctly.

Chapter 32 helps us prove AEM delivered it correctly.

CI/CD Should Reproduce the Frontend Build

A healthy pipeline should not depend on a developer committing manually generated files after every frontend change unless that is an explicit project convention.

The build should be reproducible from source and locked dependencies.

That means paying attention to:

text
package-lock.json
Node version
npm version
frontend-maven-plugin
webpack configuration
Clientlib generator version
Maven module ordering

Version drift is not just a frontend inconvenience.

It can change production artifacts.

For architecture reviews, the question is:

Can another developer or CI agent clone this repository and produce the same deployable frontend without relying on somebody's workstation state?

If not, the build contract is incomplete.

What Belongs in Source Control

The exact answer depends on project convention, but responsibilities should be explicit.

Always treat source configuration as source:

text
package.json
package-lock.json
webpack configuration
TypeScript/JavaScript source
SCSS source
clientlib generation configuration
Maven configuration

Never commit:

text
node_modules

Whether dist or generated Clientlib output is committed depends on the project's build convention. In an archetype-style generated-Clientlib workflow, those files should still be treated as build output even if the repository happens to contain them.

The important part is avoiding ambiguous ownership where developers edit generated files while the build also overwrites them.

One layer must be the source of truth.

Performance Starts Before the Clientlib Exists

Chapter 32 discussed Clientlib loading and runtime boundaries.

But many performance decisions are already made earlier.

Webpack decides things such as:

  • which modules enter a bundle,
  • whether code is split,
  • how CSS is extracted,
  • which static assets are copied,
  • whether source maps are emitted,
  • how production optimization is configured.

By the time an oversized site.js reaches AEM, Clientlibs cannot magically fix the bundle architecture.

AEM can deliver the file efficiently.

It cannot undo poor frontend composition.

That is why frontend performance belongs to both the build architecture and the runtime delivery architecture.

Architect View — Define Ownership at Every Boundary

A maintainable AEM frontend pipeline has clear ownership.

Frontend Source

Owned by frontend development.

Contains:

text
TypeScript/JavaScript
SCSS
frontend resources
npm dependencies
tests

Webpack

Owns frontend transformation and bundling.

Clientlib Generation

Owns translation from frontend build output into AEM Clientlib structure when the full-stack Clientlib model is used.

ui.apps

Owns deployable AEM application repository content.

Maven

Orchestrates the full application build.

Cloud Manager

Owns cloud build/deployment execution according to the selected pipeline model.

AEM/CDN

Owns runtime delivery.

When one stage fails, investigate the owner of that stage first.

Architect Review Checklist

Before approving a frontend build architecture, I would check:

  • Is ui.frontend the clear source of truth for frontend source?
  • Are generated artifacts treated as generated artifacts?
  • Can the frontend build run independently for fast development?
  • Can the complete Maven build reproduce the frontend output from a clean checkout?
  • Is the Node/npm toolchain version controlled?
  • Is the lock file committed?
  • Are webpack entry points understandable?
  • Is the boundary between dist and Clientlib generation explicit?
  • Are Clientlib categories aligned with the runtime design from Chapter 32?
  • Are npm dependencies and Clientlib dependencies treated as different layers?
  • Are authoring-only assets separated from public site assets?
  • Can developers trace a browser asset back through AEM, ui.apps, Clientlib generation, dist, and source?
  • Is the project intentionally using the full-stack Clientlib model or the dedicated AEMaaCS frontend pipeline?
  • Does the CI/CD model match that architectural choice?
  • Are frontend performance decisions reviewed before the artifacts reach AEM?

If these answers are clear, the frontend pipeline is usually understandable to both the frontend team and the AEM team.

Summary

The ui.frontend module is not just another Maven folder.

It is the build-time boundary for frontend engineering in a full-stack AEM project.

Frontend developers work with normal frontend source and tooling.

Webpack turns that source into browser-ready artifacts.

In the Clientlib-based full-stack model, Clientlib generation converts those artifacts into AEM repository structures under ui.apps.

Maven orchestrates the complete build.

AEM then delivers the deployed Clientlibs through /etc.clientlibs, with Dispatcher/CDN and the browser participating in runtime delivery and caching.

AEM as a Cloud Service can also use a dedicated front-end pipeline. After the project is prepared for that delivery contract, Cloud Manager builds the frontend separately and the resulting resources are delivered from the built-in CDN rather than /etc.clientlibs.

The most useful debugging habit is to follow the artifact:

source → webpack output → generated AEM artifact → Maven package → deployment → browser response

At each step, ask whether the expected artifact is still correct.

That turns a vague "frontend is not updated" problem into a specific build or delivery problem.

What's Next

Chapter 34 — Responsive Design & Image Delivery

The next chapter moves from frontend build architecture to what the browser actually receives across devices: responsive layouts, responsive images, image delivery, performance, and the AEM-specific decisions that affect those resources.

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.