Build and review sites on AEM Edge Delivery Services (aem.live, formerly Franklin/Helix) — boilerplate project setup, content sources, blocks, loading phases, spreadsheets/JSON, preview/live and the Sidekick, with a 100 Lighthouse score as the bar.
When agents use itUse this whenever the user works in an aem-boilerplate repo, writes or debugs a block (blocks/<name>/<name>.js with export default decorate(block)), edits scripts.js, delayed.js, styles.css or fstab.yaml, configures mountpoints or the configuration service, runs aem up, uses .aem.page/.aem.live or branch preview URLs, builds query-index, redirects or headers sheets, adds third-party scripts, fights LCP or PageSpeed failures on a PR, designs content models for Google Drive, SharePoint, Document Authoring or Universal Editor authoring, or asks whether EDS fits versus traditional AEM Sites. Also apply it when someone wants to add React, a bundler or npm build to an EDS project.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill aem-edge-delivery-services -a github-copilot
# or with the org installer (adds .github/skills/aem-edge-delivery-services):
npx -y github:AGCO-Global/org-skills add skill aem-edge-delivery-services
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the aem-headless-skills plugin, which bundles all AEM / Headless skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-headless-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-edge-delivery-services -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill aem-edge-delivery-services -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-edge-delivery-services -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-edge-delivery-services -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
AEM Edge Delivery Services
Edge Delivery is a "no build, no framework" platform: content is authored as documents or in Universal Editor, turned into semantic HTML at the edge, and decorated by small vanilla JS/CSS blocks loaded in strict phases. Every decision is judged by two questions: does it keep the page at a 100 Lighthouse score, and can an author create it without a developer?
1. When EDS fits
Situation
Choose
Why
Marketing/content site, performance and time-to-launch matter most
EDS with document authoring
Fastest pages, simplest authoring, no build pipeline
Same, but authors need AEM governance, MSM/workflows, structured components
EDS + Universal Editor (AEM authoring)
AEM author UX, EDS delivery
Deep integration with AEM components, personalization on AEM publish, complex OSGi logic
Traditional AEM Sites
Server-side Java/HTL model and existing component library
App-like, heavily stateful UI
Headless/SPA, possibly embedded in EDS pages
EDS blocks are for content, not apps
2. Project setup and content sources
Start from the aem-boilerplate GitHub template (use the Universal Editor variant of the boilerplate when authoring in AEM); install the AEM Code Sync GitHub app on the repo — it syncs code to the edge on every push.
Content source: Google Drive or SharePoint documents, Document Authoring (DA), or AEM authoring via Universal Editor. Classic projects declare it in fstab.yaml; newer projects can use the configuration service (including "repoless" multi-site setups). Check current aem.live docs for which applies to your project before editing either.
URLs: https://main--<repo>--<owner>.aem.page (preview) and .aem.live (live); every branch gets https://<branch>--<repo>--<owner>.aem.page. Keep branch names short and lowercase (they become hostnames).
Authors use the Sidekick browser extension to Preview (→ .page), Publish (→ .live), and unpublish. Production CDN points at .live.
3. Page structure and content modelling
A document becomes sections (split by --- horizontal rules), each containing default content (headings, paragraphs, lists, images, links) and blocks (tables whose first row is the block name).
Section Metadata table inside a section sets section classes/data (e.g. Style | highlight). A Metadata table at the end of the page sets <meta> (title, description, image, template, custom keys read via getMetadata).
Block options in the header row become classes: Columns (wide, dark) → <div class="columns wide dark">.
Model for authors: prefer default content over blocks, few rows/columns, no invisible magic cells, names in plain language. If a block needs a manual to author, redesign it.
Reuse content with fragments (a fragment block referencing /fragments/...); header and footer are fragments loaded from /nav and /footer.
4. Writing blocks
blocks/cards/cards.js → export default function decorate(block) { ... }
blocks/cards/cards.css → all selectors scoped under .cards
Decorate the existing DOM; don't discard author content or rebuild it from hard-coded strings. Keep blocks small (a few hundred lines at most) and single-purpose; variants via classes, not new blocks.
Use aem.js helpers (createOptimizedPicture, loadCSS, loadScript, getMetadata, readBlockConfig, toClassName, decorateIcons). Never edit scripts/aem.js — it is the shared library upgraded from upstream; put project logic in scripts/scripts.js.
CSS: mobile-first, scoped to the block class, CSS custom properties from styles/styles.css; no global resets inside blocks.
Async decorate is fine; await only what's needed before first paint.
5. Loading phases and performance
Eager (loadEager): decorate the page and load the first section only, so the LCP element (usually the hero image or H1) renders with minimal JS/CSS. Nothing third-party here.
Lazy (loadLazy): remaining sections and blocks, header/footer, lazy-styles.css, fonts.
Delayed (scripts/delayed.js, a few seconds after load): analytics, tag managers, chat, consent, marketing pixels. This is where third-party scripts go.
Target a 100 Lighthouse/PageSpeed score on mobile. Protect LCP: don't lazy-load the first image, don't insert content above the fold after load, reserve space to avoid CLS, keep eager payload tiny (the boilerplate keeps pre-LCP payload in the ~100 KB range).
Auto-blocking (buildAutoBlocks in scripts.js) creates blocks from content patterns (e.g. hero from a leading picture + H1, fragments from links) so authors don't have to — keep it deterministic and cheap.
6. Spreadsheets, JSON and site config
Any sheet is served as JSON (/data/offices.json → { total, offset, limit, data: [...] }, ?offset=&limit=, multi-sheet via ?sheet=). Use it for lists, lookups and block data instead of hard-coding.
query-index: an index definition produces /query-index.json with page metadata for listings, search and sitemaps; filter client-side or build a per-section index for large sites.
Redirects sheet for 301s; response headers and other site settings via the project configuration (older projects used config sheets, newer ones the configuration service) — verify the current mechanism in aem.live docs.
Forms: use the current EDS forms approach (form block backed by a sheet, or AEM Forms for EDS) — confirm availability and submission handling before designing.
7. Development workflow
Local: npm i -g @adobe/aem-cli, then aem up → http://localhost:3000, serving local code with content proxied from preview.
npm run lint (ESLint airbnb-base + Stylelint) must pass; no build step, no transpile, ship ES modules as written.
PR workflow: push a branch, test on <branch>--<repo>--<owner>.aem.page, include before/after test URLs in the PR. The Code Sync / PSI checks run PageSpeed on those URLs — a PR that drops the score is not mergeable in spirit even if the check is overridden.
Deliverable format (block spec)
Block name and purpose, variants (class options).
Authoring table: rows/columns authors fill, with an example and what's optional.
Resulting DOM after decoration and the CSS/responsive behaviour.
Data: sheets/JSON/query-index used, with fields.
Performance: loading phase, LCP impact, images and third-party scripts.
Test URLs (branch preview) and lint/PSI results.
Anti-patterns to reject
Adding React/Vue, a bundler, TypeScript build or npm runtime dependencies to page delivery.
Editing scripts/aem.js; forking boilerplate internals instead of using scripts.js.
Third-party scripts in head.html or eager phase; anything that blocks or delays LCP.
Huge multi-purpose blocks; blocks that ignore author content and hard-code copy.
Author-hostile models: deep nested tables, positional magic, JSON in cells.
Global CSS leaking from blocks; unscoped selectors.
Content or config hard-coded in JS that belongs in a sheet or metadata.
Merging PRs without branch test URLs or with a PageSpeed regression.
---
name: aem-edge-delivery-services
description: Build and review sites on AEM Edge Delivery Services (aem.live, formerly Franklin/Helix) — boilerplate project setup, content sources, blocks, loading phases, spreadsheets/JSON, preview/live and the Sidekick, with a 100 Lighthouse score as the bar. Use this whenever the user works in an aem-boilerplate repo, writes or debugs a block (blocks/<name>/<name>.js with export default decorate(block)), edits scripts.js, delayed.js, styles.css or fstab.yaml, configures mountpoints or the configuration service, runs aem up, uses .aem.page/.aem.live or branch preview URLs, builds query-index, redirects or headers sheets, adds third-party scripts, fights LCP or PageSpeed failures on a PR, designs content models for Google Drive, SharePoint, Document Authoring or Universal Editor authoring, or asks whether EDS fits versus traditional AEM Sites. Also apply it when someone wants to add React, a bundler or npm build to an EDS project.
metadata:
technology: AEM
type: development
---
# AEM Edge Delivery Services
Edge Delivery is a "no build, no framework" platform: content is authored as documents or in Universal Editor, turned into semantic HTML at the edge, and decorated by small vanilla JS/CSS blocks loaded in strict phases. Every decision is judged by two questions: does it keep the page at a 100 Lighthouse score, and can an author create it without a developer?
## 1. When EDS fits
| Situation | Choose | Why |
|---|---|---|
| Marketing/content site, performance and time-to-launch matter most | **EDS** with document authoring | Fastest pages, simplest authoring, no build pipeline |
| Same, but authors need AEM governance, MSM/workflows, structured components | **EDS + Universal Editor (AEM authoring)** | AEM author UX, EDS delivery |
| Deep integration with AEM components, personalization on AEM publish, complex OSGi logic | **Traditional AEM Sites** | Server-side Java/HTL model and existing component library |
| App-like, heavily stateful UI | Headless/SPA, possibly embedded in EDS pages | EDS blocks are for content, not apps |
## 2. Project setup and content sources
- Start from the **aem-boilerplate** GitHub template (use the Universal Editor variant of the boilerplate when authoring in AEM); install the **AEM Code Sync** GitHub app on the repo — it syncs code to the edge on every push.
- Content source: Google Drive or SharePoint documents, **Document Authoring (DA)**, or AEM authoring via Universal Editor. Classic projects declare it in `fstab.yaml`; newer projects can use the **configuration service** (including "repoless" multi-site setups). Check current aem.live docs for which applies to your project before editing either.
```yaml
# fstab.yaml (document-based projects)
mountpoints:
/: https://drive.google.com/drive/folders/<folder-id>
```
- URLs: `https://main--<repo>--<owner>.aem.page` (preview) and `.aem.live` (live); every branch gets `https://<branch>--<repo>--<owner>.aem.page`. Keep branch names short and lowercase (they become hostnames).
- Authors use the **Sidekick** browser extension to Preview (→ `.page`), Publish (→ `.live`), and unpublish. Production CDN points at `.live`.
## 3. Page structure and content modelling
- A document becomes **sections** (split by `---` horizontal rules), each containing **default content** (headings, paragraphs, lists, images, links) and **blocks** (tables whose first row is the block name).
- `Section Metadata` table inside a section sets section classes/data (e.g. `Style | highlight`). A `Metadata` table at the end of the page sets `<meta>` (title, description, image, template, custom keys read via `getMetadata`).
- Block options in the header row become classes: `Columns (wide, dark)` → `<div class="columns wide dark">`.
- Model for authors: prefer default content over blocks, few rows/columns, no invisible magic cells, names in plain language. If a block needs a manual to author, redesign it.
- Reuse content with **fragments** (a `fragment` block referencing `/fragments/...`); header and footer are fragments loaded from `/nav` and `/footer`.
## 4. Writing blocks
```
blocks/cards/cards.js → export default function decorate(block) { ... }
blocks/cards/cards.css → all selectors scoped under .cards
```
```js
import { createOptimizedPicture } from '../../scripts/aem.js';
export default function decorate(block) {
const ul = document.createElement('ul');
[...block.children].forEach((row) => {
const li = document.createElement('li');
li.append(...row.children);
[...li.children].forEach((col) => {
col.className = col.querySelector('picture') ? 'cards-card-image' : 'cards-card-body';
});
ul.append(li);
});
ul.querySelectorAll('picture > img').forEach((img) => img.closest('picture')
.replaceWith(createOptimizedPicture(img.src, img.alt, false, [{ width: '750' }])));
block.replaceChildren(ul);
}
```
- Decorate the existing DOM; don't discard author content or rebuild it from hard-coded strings. Keep blocks small (a few hundred lines at most) and single-purpose; variants via classes, not new blocks.
- Use `aem.js` helpers (`createOptimizedPicture`, `loadCSS`, `loadScript`, `getMetadata`, `readBlockConfig`, `toClassName`, `decorateIcons`). **Never edit `scripts/aem.js`** — it is the shared library upgraded from upstream; put project logic in `scripts/scripts.js`.
- CSS: mobile-first, scoped to the block class, CSS custom properties from `styles/styles.css`; no global resets inside blocks.
- Async `decorate` is fine; `await` only what's needed before first paint.
## 5. Loading phases and performance
- **Eager** (`loadEager`): decorate the page and load the first section only, so the LCP element (usually the hero image or H1) renders with minimal JS/CSS. Nothing third-party here.
- **Lazy** (`loadLazy`): remaining sections and blocks, header/footer, `lazy-styles.css`, fonts.
- **Delayed** (`scripts/delayed.js`, a few seconds after load): analytics, tag managers, chat, consent, marketing pixels. This is where third-party scripts go.
- Target a **100 Lighthouse/PageSpeed score** on mobile. Protect LCP: don't lazy-load the first image, don't insert content above the fold after load, reserve space to avoid CLS, keep eager payload tiny (the boilerplate keeps pre-LCP payload in the ~100 KB range).
- **Auto-blocking** (`buildAutoBlocks` in `scripts.js`) creates blocks from content patterns (e.g. hero from a leading picture + H1, fragments from links) so authors don't have to — keep it deterministic and cheap.
## 6. Spreadsheets, JSON and site config
- Any sheet is served as JSON (`/data/offices.json` → `{ total, offset, limit, data: [...] }`, `?offset=&limit=`, multi-sheet via `?sheet=`). Use it for lists, lookups and block data instead of hard-coding.
- **query-index**: an index definition produces `/query-index.json` with page metadata for listings, search and sitemaps; filter client-side or build a per-section index for large sites.
- **Redirects** sheet for 301s; response **headers** and other site settings via the project configuration (older projects used config sheets, newer ones the configuration service) — verify the current mechanism in aem.live docs.
- Forms: use the current EDS forms approach (form block backed by a sheet, or AEM Forms for EDS) — confirm availability and submission handling before designing.
## 7. Development workflow
- Local: `npm i -g @adobe/aem-cli`, then `aem up` → `http://localhost:3000`, serving local code with content proxied from preview.
- `npm run lint` (ESLint airbnb-base + Stylelint) must pass; no build step, no transpile, ship ES modules as written.
- PR workflow: push a branch, test on `<branch>--<repo>--<owner>.aem.page`, include before/after test URLs in the PR. The Code Sync / PSI checks run PageSpeed on those URLs — a PR that drops the score is not mergeable in spirit even if the check is overridden.
## Deliverable format (block spec)
1. **Block name and purpose**, variants (class options).
2. **Authoring table**: rows/columns authors fill, with an example and what's optional.
3. **Resulting DOM** after decoration and the CSS/responsive behaviour.
4. **Data**: sheets/JSON/query-index used, with fields.
5. **Performance**: loading phase, LCP impact, images and third-party scripts.
6. **Test URLs** (branch preview) and lint/PSI results.
## Anti-patterns to reject
- Adding React/Vue, a bundler, TypeScript build or npm runtime dependencies to page delivery.
- Editing `scripts/aem.js`; forking boilerplate internals instead of using `scripts.js`.
- Third-party scripts in `head.html` or eager phase; anything that blocks or delays LCP.
- Huge multi-purpose blocks; blocks that ignore author content and hard-code copy.
- Author-hostile models: deep nested tables, positional magic, JSON in cells.
- Global CSS leaking from blocks; unscoped selectors.
- Content or config hard-coded in JS that belongs in a sheet or metadata.
- Merging PRs without branch test URLs or with a PageSpeed regression.