---
name: aem-frontend-development
description: Build and review Adobe Experience Manager Sites front-end code (AEM as a Cloud Service first, 6.5 noted) — the archetype ui.frontend webpack build, client libraries, Style System CSS, responsive grid, author-safe JavaScript, the Cloud Manager front-end pipeline, Site Themes and performance. Use this whenever the user edits ui.frontend, webpack config or clientlib.config.js (aem-clientlib-generator), a clientlib .content.xml with categories, embed, dependencies or allowProxy, js.txt/css.txt, /etc.clientlibs URLs, async/defer loading, BEM styles for the Style System, aem-GridColumn or layout container breakpoints, JS that breaks the page editor or needs wcmmode checks, npm run watch, aem-site-theme-builder, the front-end pipeline, SPA Editor or Universal Editor questions, or Core Image renditions and Core Web Vitals on an AEM site. Also apply it when reviewing AEM front-end pull requests, clientlib structure or page load performance.
metadata:
  technology: AEM
  type: development
---

# AEM Front-end Development

AEM front-end code runs in two very different places: a cached, CDN-fronted publish page and an author page editor that re-renders components inside an iframe. Build one bundle per site, load it predictably, and write JS that survives both environments without knowing which one it is in.

## 1. Decide first

| Need | Use | Notes |
|---|---|---|
| Sites built on the Maven archetype (HTL components) | **ui.frontend** webpack build → aem-clientlib-generator → ui.apps | Default for developer-owned projects |
| Theme-only changes, fast deploys, designer-owned CSS/JS | **Front-end pipeline** in Cloud Manager | Cloud only; deploys a `dist` build independently of the full-stack pipeline |
| Low-code sites from Site Templates | **Site Theme** + aem-site-theme-builder + front-end pipeline | Theme repo is plain npm; no Java |
| App-like experiences, content from AEM | **Headless** (GraphQL / Content Fragments) + Universal Editor | Preferred over SPA Editor for new work |
| Existing React/Angular SPA Editor project | Maintain; plan migration | Adobe has deprecated SPA Editor for new projects — verify current guidance before investing |

## 2. ui.frontend and clientlib generation

- `npm run dev` / `npm run prod` build via webpack into `dist`; `clientlib.config.js` (aem-clientlib-generator) copies the output into `ui.apps/.../clientlibs/clientlib-site` and `clientlib-dependencies`, writing `.content.xml`, `js.txt` and `css.txt`. Those generated folders are build output: never hand-edit them and keep them out of review noise.
- Maven runs npm via `frontend-maven-plugin`; pin the Node/npm version there and commit the lockfile so local, CI and Cloud Manager builds match.
- Webpack already minifies, so set processors to avoid double processing:

```js
// clientlib.config.js (excerpt)
libs: [{
  name: 'clientlib-site', allowProxy: true, serializationFormat: 'xml',
  categories: ['mysite.site'], dependencies: ['mysite.dependencies'],
  cssProcessor: ['default:none', 'min:none'], jsProcessor: ['default:none', 'min:none'],
  assets: { js: ['dist/clientlib-site/*.js'], css: ['dist/clientlib-site/*.css'],
            resources: { cwd: 'dist/clientlib-site', files: ['**/*.*'], flatten: false,
                         ignore: ['**/*.js', '**/*.css'] } }
}]
```

- One entry per site (plus an optional small `dependencies` lib). Split further only with evidence (e.g. a heavy component loaded on few pages).

## 3. Client libraries

```xml
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:ClientLibraryFolder" allowProxy="{Boolean}true"
    categories="[mysite.base]" embed="[mysite.components.hero,mysite.components.teaser]"/>
```

- **`embed`** concatenates other categories into this one — one request. **`dependencies`** emits a separate `<script>`/`<link>` before it — use sparingly, each is a request.
- `allowProxy="{Boolean}true"` on every lib under `/apps`: publish denies reads on `/apps`, so assets must be served as `/etc.clientlibs/<project>/clientlibs/...`. Reference fonts and images relative to the clientlib `resources` folder, never `/apps/...` paths.
- Component clientlibs are **embedded** into the site lib. Do not include them per component in HTL; duplicated includes and render-blocking scripts follow.
- Include once, from the page component (or the Core Page policy's clientlib settings): CSS in `<head>`, JS at end of body or deferred.

```html
<sly data-sly-use.clientlib="core/wcm/components/commons/v1/templates/clientlib.html">
  <sly data-sly-call="${clientlib.css @ categories='mysite.site'}"/>
  <sly data-sly-call="${clientlib.js @ categories='mysite.site', defer=true}"/>
</sly>
```

The Core Components clientlib template supports `async`/`defer` and related attributes; the older `/libs/granite/sightly/templates/clientlib.html` does not. `async` breaks ordering between libs — prefer `defer`.
- Cache busting: AEM as a Cloud Service serves long-cache, content-hashed clientlib URLs out of the box; on 6.5 use a versioned-clientlib solution (e.g. ACS AEM Commons) plus Dispatcher cache headers.
- Author-only code (dialog validators, editor extensions) goes in categories such as `cq.authoring.dialog` or a component's `extraClientlibs`, never in the site bundle.

## 4. CSS: BEM, Style System, responsive grid

- Core Components emit BEM (`.cmp-teaser`, `.cmp-teaser__title`). Follow the same scheme; one SCSS partial per component under `ui.frontend/src/main/webpack/components/`.
- Style System classes are added to the **component wrapper**, not the inner `cmp-` element. Name them as modifiers and scope descendants:

```scss
.cmp-teaser--dark .cmp-teaser__title { color: var(--color-inverse); }
.cmp-teaser--hero-layout .cmp-teaser { display: grid; grid-template-columns: 1fr 1fr; }
```

- Styles must not depend on the markup order of another component, on author-only wrappers, or on `!important` to beat Core defaults — raise specificity with the block class instead.
- Design tokens as CSS custom properties; themes and brands swap token values, not selectors.
- Layout Container uses `aem-Grid` / `aem-GridColumn--<breakpoint>--<n>` classes. Breakpoint names and widths in the template's `cq:responsive` config **must match** the generated grid SCSS; a mismatch makes layout mode lie to authors.
- Never style `.aem-GridColumn` directly for component looks; use the component's own classes.

## 5. JavaScript that respects the editor

- Initialise from markup: select `[data-cmp-is="hero"]`, read config from `data-*` attributes, and re-run init for nodes added later. The page editor replaces component DOM after every dialog save, so bind with a **MutationObserver** (as Core Components do) or event delegation, not a single `DOMContentLoaded` pass.

```js
const SEL = '[data-cmp-is="hero"]';
const init = el => { if (el.dataset.cmpInit) return; el.dataset.cmpInit = 'true'; /* ... */ };
const scan = node => {
  if (node.nodeType !== 1) return;
  if (node.matches(SEL)) init(node);
  node.querySelectorAll(SEL).forEach(init);
};
document.addEventListener('DOMContentLoaded', () => {
  scan(document.body);
  new MutationObserver(ms => ms.forEach(m => m.addedNodes.forEach(scan)))
    .observe(document.body, { childList: true, subtree: true });
});
```

- In edit mode, disable behaviour that fights authors: autoplaying carousels, scroll hijacking, modals on load, `preventDefault` on clicks. Emit a flag from HTL (e.g. `data-wcmmode="${wcmmode.edit ? 'edit' : ''}"` on `<body>`) rather than sniffing URLs or `window.Granite`.
- Preview with `?wcmmode=disabled` on author and always test on publish; author injects extra wrappers, overlays and CSS that hide bugs.
- No global namespace pollution, no jQuery in new code unless the project already depends on it; ES modules bundled by webpack.

## 6. Front-end pipeline and Site Themes (Cloud)

- The front-end pipeline runs `npm run build` on a front-end module and expects a `dist` folder; it deploys theme artifacts independently, so CSS/JS changes ship without a full-stack deployment. The site must be configured to load the pipeline-deployed theme — check current Adobe docs for archetype versions and settings that enable this.
- Site Templates themes: local development with aem-site-theme-builder (`npm run live` proxies a real AEM site and injects your local theme), then deploy through the front-end pipeline.
- Keep one source of truth: do not mix a pipeline-deployed theme with a clientlib-shipped copy of the same CSS.

## 7. Performance

- Budget per page: one site CSS, one deferred site JS, optional small vendor lib. Measure with Lighthouse/CrUX on **publish** behind the CDN, not on author.
- Critical CSS: inline a small above-the-fold subset only if measured LCP gains justify the maintenance; otherwise keep one cacheable stylesheet.
- Images: Core Image component with widths configured in the policy (`srcset`), lazy loading, and **web-optimized image delivery** on Cloud (or Dynamic Media / Smart Imaging where licensed). Never serve original DAM binaries or hand-built rendition paths. Give the LCP image eager loading and explicit dimensions.
- Fonts: self-host in clientlib resources, `font-display: swap`, preload the one or two critical faces.
- Third-party tags via a tag manager with async loading; audit them — they usually dominate TBT.

## 8. Accessibility

Visible focus styles, colour contrast from tokens (checked for each Style System variant), `prefers-reduced-motion` for animations, keyboard support for custom widgets, and no CSS that hides content screen readers need (`display:none` on labels). Run axe in CI against publish-mode renders of component library pages.

## 9. Local development

`npm run watch` (archetype) rebuilds and syncs clientlibs to a local SDK/Quickstart; the webpack dev server (`npm start`) serves static markup for isolated styling. Community tools such as aemfed add sync + live reload. Validate against the local AEM SDK Dispatcher before pushing — clientlib proxy, cache headers and filters differ from author.

## Deliverable format

For new or changed front-end work, deliver: the SCSS/JS source under `ui.frontend` (or theme repo), `clientlib.config.js` or clientlib `.content.xml` changes with categories and how they are included, the HTL include (if changed), any Style System policy entries (style group, label, class), and a short verification note (author edit mode, publish, breakpoints, Lighthouse/axe). For reviews, list findings by severity (editor breakage, security/proxy, performance, accessibility, maintainability) with file and fix.

## Anti-patterns to reject

- Hand-editing generated clientlib folders; committing `dist` output alongside source without a reason.
- Missing `allowProxy`, or URLs pointing to `/apps/...` from CSS/JS.
- Per-component clientlib includes in HTL; long `dependencies` chains instead of `embed`.
- Render-blocking JS in `<head>`; `async` on libs that depend on each other.
- JS that initialises once on page load and breaks after an author edits a component; autoplay or modals active in edit mode.
- Style System variants implemented as new components or as dialog fields; `!important` wars with Core CSS.
- Grid breakpoints in CSS that differ from the template's responsive config.
- Serving original DAM images; no `srcset`; lazy-loading the LCP image.
- Starting a new project on SPA Editor without checking current Adobe guidance on headless + Universal Editor.
- Author-only scripts in the publish bundle; testing performance only on author.
