This project uses Edge Delivery Services in Adobe Experience Manager Sites as a Cloud Service, built on aem-boilerplate.
Follow the patterns in this codebase and instructions in this file while working in this repository.
When facing trade-offs, follow this order: Intuitive (author-friendly) > Simple (minimal) > Consistent (matches existing patterns).
- Install:
npm install(ornpm ci) - Lint:
npm run lint - Lint (fix):
npm run lint:fix - Local dev:
npx -y @adobe/aem-cli up --no-open --forward-browser-logs(oraem upafternpm install -g @adobe/aem-cli)- Server: http://localhost:3000 (auto-reload)
- Inspect HTML/DOM:
curl http://localhost:3000/{path}or.plain.htmlorconsole.login code
- Node.js 24; npm only (not pnpm/yarn)
- ESLint 8.57.1 with eslint-config-airbnb-base; Stylelint 17.13.0 with stylelint-config-standard
- AEM Edge Delivery: https://www.aem.live/
- No runtime dependencies. Zero production deps for optimal performance and automatic code-splitting via
/blocks/. - No build step. Code runs as ES modules in the browser. Do not add bundlers, transpilers, or build tools.
- Do not modify:
scripts/aem.js(core AEM library),package-lock.json(let npm manage it),node_modules/(generated),head.html(global head content). - Always use
.jsin imports. ESLint and native ES modules require it:import { foo } from './bar.js';
- Security
- Do not commit secrets (API keys, passwords).
- Use
.hlxignore(same format as.gitignore) to exclude files from being served.
- Accessibility
- Valid heading hierarchy.
alton all images (empty for decorative, descriptive for content).- Keyboard support where there is interaction.
:focus-visibleon focusable controls.- ARIA only when a native element cannot express the behavior.
- Meet WCAG 2.1 AA.
- Performance
- Optimize developer-committed images in git (author uploads auto-optimized).
- Use
lazy-styles.cssanddelayed.jsfor non-critical resources. - PageSpeed must score 100 (https://www.aem.live/developer/keeping-it-100).
- Responsiveness
- Default styles target mobile (no
max-widthqueries). - Breakpoints at 600 / 900 / 1200px.
- Default styles target mobile (no
- Localization
- No hard-coded user-facing text.
- All strings configurable or data-driven.
- Lint: Airbnb (ESLint), Stylelint standard.
- HTML: Semantic elements. Native interactive elements (buttons, links, summary) over ARIA. DOM order matches visual order. No wrappers for layout only. Prefer native state:
<details open>,[aria-expanded],[aria-pressed]. - CSS: Native only. Class names flat, short, context-scoped. No utility classes. Style by attributes (
[open],[aria-expanded="true"]) over state classes. Prefer em/ch for typography. Prefer shorthand unless overriding one side. Logical properties for reading direction; physical for viewport. Avoid one-off custom properties. - JS: ES6+ native modules, vanilla only. Early returns, named variables, explicit conditionals. Use existing utils in
scripts/aem.jsandscripts/scripts.js; add new only when no fit. No unused code. - Comments: Only when intent or constraints are non-obvious or to record a decision. No comments that only restate the code.
├── blocks/{blockname}/
│ ├── {blockname}.js # Block decoration
│ └── {blockname}.css # Block styles
├── styles/
│ ├── styles.css # LCP-critical global styles
│ ├── lazy-styles.css # Below-fold styles
│ └── fonts.css # Font declarations
├── scripts/
│ ├── aem.js # Core AEM library for page decoration logic
│ ├── scripts.js # Page decoration entry point and global utilities
│ └── delayed.js # Delayed functionality (e.g. martech/analytics)
├── icons/ # SVG files; reference in code with <span class="icon icon-{name}"></span>
├── fonts/ # Web fonts
├── head.html # Global <head> content
└── 404.html # Custom error page
Organization:
- Global reusable →
scripts/scripts.js,styles/styles.css; block-specific → block folders - New utilities go in
scripts/scripts.js(notaem.js) - Reuse inherited styles from
styles/styles.cssbefore adding block CSS (cascade)
- Content structure: Pages are composed of sections → sections contain default content (text, headings, links) and blocks
- See content structure and markup reference
- Test content: For local development without authored content:
- Create static HTML files in
drafts/folder - Pass
--html-folder draftswhen starting dev server - Use
.htmlor.plain.htmlextensions
- Create static HTML files in
- Three-phase loading: Pages load in phases for performance (eager → LCP, lazy → rest, delayed → martech); see
loadPage()inscripts.js
File structure: Every block lives in blocks/{blockname}/ with two files: {blockname}.css and {blockname}.js (must export default decorate(block)).
// blocks/example/example.js
/** @param {Element} block */
export default async function decorate(block) {
// 1. Load dependencies
// 2. Extract configuration
// 3. Transform DOM
// 4. Add event listeners
}Block content:
- Expected HTML = contract between author and developer; decide structure before coding
- Keep structure simple for authors working in documents; handle missing/extra fields without breaking
- If structure requires hidden conventions or non-obvious formatting in authoring, redesign—authors work in documents, not code
Scoping: Blocks are self-contained.
- JS: Work only within the
blockelement passed todecorate()—don't touch elements outside the block - CSS: Scope all selectors to the block. Bad:
.item-list. Good:.{blockname} .item-list. - Avoid
.{blockname}-containerand.{blockname}-wrapper(reserved for sections)
Auto-blocking: Blocks can also be created programmatically from content patterns; see buildAutoBlocks() in scripts.js.
- Local (uncommitted code + previewed content): http://localhost:3000/{path}
- Preview:
https://{branch}--progressrail--aemsites.aem.page/{path} - Live:
https://main--progressrail--aemsites.aem.live/{path}
npm run lintpasses (CI).- Test locally at http://localhost:3000/{path}.
- Push branch →
https://{branch}--progressrail--aemsites.aem.page/{path}. - Run PageSpeed Insights on preview; meet Performance requirement.
- Open PR with
.github/pull_request_template.md: issue ref (Fix #<id>), test URLs (before/after)—required. gh pr checkspass before review.
- AGENTS.override.md — team or temporary overrides.
- AGENTS.local.md — personal preferences; add to
.gitignoreso it is not committed.
- Search with
site:www.aem.live - Developer Tutorial
- The Anatomy of a Project
- Best Practices
- Working with AI Agents
- AEM Documentation
- Doc search:
curl -s https://www.aem.live/docpages-index.json | jq -r '.data[] | select(.content | test("KEYWORD"; "i")) | "\(.path): \(.title)"'