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(ornpm install -g @adobe/aem-clithenaem up)- Server at http://localhost:3000 with auto-reload
- View: playwright, puppeteer, or browser; if unavailable, ask human for feedback
- Inspect delivered HTML/DOM:
curl http://localhost:3000/{path}(or.plain.html) orconsole.login code - The
curlcommands above return content before the decoration pipeline runs. To see the fully decorated HTML after the pipeline runs (sections, blocks, buttons, etc.), use thedecorateCLI tool (npm install -g aem-decorate):decorate /path/to/page— decorated HTML to stdoutdecorate /path/to/page --format md— decorated output as markdowndecorate /path/to/page --selector "main .section"— specific elements onlydecorate /path/to/page --no-header --no-footer— main content only
- Cloudflare Workers (contact form):
npm run dev:contact-us,npm run deploy:contact-us
- Node.js; npm only (not pnpm/yarn)
- ESLint with eslint-config-airbnb-base; Stylelint with stylelint-config-standard
- AEM Edge Delivery: https://www.aem.live/
- Cloudflare Workers (Wrangler) for serverless endpoints
- 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:
- Client-side code is public; do not commit secrets (API keys, passwords)
- Use
.hlxignore(same format as.gitignore) to exclude files from being served
- Accessibility:
- Valid heading hierarchy;
altrequired on all images—empty (alt="") for decorative, descriptive for content - Meet WCAG 2.1 AA
- Valid heading hierarchy;
- Performance:
- Optimize developer-committed images/assets in git (author-uploaded images are auto-optimized)
- Use
lazy-styles.cssanddelayed.jsfor non-critical resources - PageSpeed must score 100 (see https://www.aem.live/developer/keeping-it-100)
- Responsiveness:
- Default styles target mobile (no
max-widthqueries) - Define breakpoints at 600/900/1200px
- Default styles target mobile (no
- Localization:
- No hard-coded user-facing text (e.g. labels, error messages)
- Make all strings configurable or data-driven
- Airbnb (ESLint), Stylelint standard
- JavaScript: ES6+ native modules; no transpiling or build
- CSS: Native CSS (features with equal or better browser support than ES6 modules); no preprocessors or frameworks
- HTML: Semantic HTML5 elements with ARIA attributes
├── 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
│ ├── shared.js # Shared utilities (createTag, query-index helpers, Chart.js loader)
│ └── delayed.js # Delayed functionality (social share injection, ShareThis)
├── templates/
│ └── article/ # Article page template
├── tools/
│ ├── quick-edit/ # AEM Sidekick quick-edit plugin
│ ├── search/ # Search tooling
│ └── plugins/ # Additional Sidekick plugins
├── workers/
│ └── contact_us/ # Cloudflare Worker for contact form (Wrangler)
├── icons/ # SVG files; reference in code with <span class="icon icon-{name}"></span>
├── fonts/ # Web fonts (Montserrat, EB Garamond, Roboto, JetBrains Mono)
├── head.html # Global <head> content
└── 404.html # Custom error page
Organization:
- Global reusable code →
scripts/scripts.js,scripts/shared.js,styles/styles.css; block-specific code → block folders - Check existing utilities in
scripts/aem.js,scripts/scripts.js, andscripts/shared.jsbefore writing new ones- New utilities →
scripts/shared.js(notaem.jsorscripts.jsunless page-level)
- New utilities →
- Check inherited styles from
styles/styles.cssbefore adding block CSS (use 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 - Dark/light theme: Persisted in
localStorage(diyfire-theme), applied viadata-themeattribute andlight-scheme/dark-schemebody classes - Auto-blocking: Fragment links (
/fragments/*) and YouTube URLs are automatically wrapped in blocks; seebuildAutoBlocks()inscripts.js. Use#_dnbhash to opt out of fragment auto-blocking. - Dynamic blocks: Tabs and modal are conditionally loaded after sections via
blocks/dynamic/index.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)
URL construction uses {repo} and {owner} from gh repo view --json nameWithOwner; use git branch for {branch}.
- Local (uncommitted code + previewed content): http://localhost:3000/{path}
- Preview:
https://{branch}--{repo}--{owner}.aem.page/{path} - Live:
https://main--{repo}--{owner}.aem.live/{path}
- Lint passes:
npm run lintmust pass (CI enforces this) - Test locally: Verify at http://localhost:3000
- Push to branch:
https://{branch}--{repo}--{owner}.aem.page/{path} - Performance: Run PageSpeed Insights on preview URL; fix until meeting Performance requirement
- Open PR: Include test URLs (before/after)—PR will be rejected without this. If no existing page demonstrates the change, create test content as static HTML and ask for help copying it to a CMS page.
- Checks pass: Run
gh pr checksbefore requesting review
- 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)"'