aem-eds-cli
v1.8.0
Published
CLI scaffolding tool for AEM Edge delivery services blocks with Universal editor authoring support
Maintainers
Readme
aem-eds-cli
The missing scaffolding tool for AEM Edge Delivery Services blocks with Universal Editor authoring support.
Adobe provides no official CLI for creating UE-compatible blocks. Writing block JSON manually takes ~58 minutes per block and is error-prone — wrong resourceType, missing child definitions, incorrect validation placement all cause silent UE failures. aem-eds-cli reduces this to 6 minutes with zero errors.
Features
- 5 block types — Simple, Simple with tabs, Container, Container with tabs, Section wrapper
- 14 UE field types — text, textarea, richtext, reference, aem-content, aem-content-fragment, aem-experience-fragment, boolean, select, multiselect, radio-group, checkbox-group, number, aem-tag
- Conditional field visibility — JSONLogic
conditionproperty — fields show/hide based on other field values - Variant select fields —
classesas a proper select field with named options - Options builder — named + value pairs for select, multiselect, radio-group, checkbox-group
- Default values — validated against field type, options list, and min/max
- Validation — required, minLength, maxLength, min, max, step, rootPath, customErrorMsg, readOnly, hidden, description
- Section wrapper —
section/v1/sectionresourceType with companion navigation block and automaticmodels/_section.jsonupdate - Correct JSON structure — flat
definitionsarray,block/itemchild resourceType, no:itemspre-population - ESLint-safe JS — generated stubs commit cleanly through Husky pre-commit hooks
- README per block — UE-specific step-by-step authoring guide for content authors
list/remove/--dry-runcommands
Installation
# Use without installing (recommended)
npx aem-eds-cli create carousel
# Global install
npm install -g aem-eds-cli
aem-eds-cli create carousel
# Project dev dependency
npm install --save-dev aem-eds-cliCommands
aem-eds-cli create [block-name] # Scaffold a new block interactively
aem-eds-cli remove <block-name> # Delete block folder and all its files
aem-eds-cli list # Show all blocks with file status
aem-eds-cli create carousel --dry-run # Preview without writing filesBlock Types
Run aem-eds-cli create and choose from 5 block types:
1. Simple Fixed fields — hero, banner, teaser
2. Simple with tabs Fixed fields grouped into UE panel tabs
3. Container Repeating child items — carousel, accordion
4. Container with tabs Container where parent config has tab groups
5. Section wrapper Section-level tab panel (section/v1/section)Block Type 1 — Simple
Use for: Hero, banner, teaser, quote — fixed fields, no repeating items.
Example: Hero block
Block name: hero
Block type: 1 (Simple)
Fields:
eyebrow → text
heading → text required, maxLength: 100
description → richtext
ctaLabel → text
ctaUrl → aem-content rootPath: /content/my-site
image → reference
theme → select options: Light→light, Dark→dark
default: light
Variants? n
Conditions? nGenerated files:
blocks/hero/
hero.js ← export default function decorate(block) { }
hero.css ← .hero { display: block; }
_hero.json ← definitions + models + filters
README.md ← UE authoring guideGenerated _hero.json:
{
"definitions": [{
"title": "Hero",
"id": "hero",
"plugins": { "xwalk": { "page": {
"resourceType": "core/franklin/components/block/v1/block",
"template": { "name": "Hero", "model": "hero" }
}}}
}],
"models": [{
"id": "hero",
"fields": [
{ "component": "text", "name": "eyebrow", "label": "Eyebrow" },
{ "component": "text", "name": "heading", "label": "Heading",
"required": true, "validation": { "maxLength": 100 } },
{ "component": "richtext", "name": "description", "label": "Description" },
{ "component": "text", "name": "ctaLabel", "label": "CTA Label" },
{ "component": "aem-content","name": "ctaUrl", "label": "CTA URL",
"validation": { "rootPath": "/content/my-site" } },
{ "component": "reference", "name": "image", "label": "Image",
"multi": false },
{ "component": "select", "name": "theme", "label": "Theme",
"options": [{"name":"Light","value":"light"},{"name":"Dark","value":"dark"}],
"value": "light" }
]
}],
"filters": []
}Authoring in UE:
Click + in Section → Hero
Click on block → Properties panel:
Eyebrow [ ]
Heading * [ ]
Description [ rich text editor ]
CTA Label [ ]
CTA URL [ page picker ]
Image [ DAM picker ]
Theme [ Light ▾ ]
→ Save → PublishExample: Pricing Banner
Block name: pricing-banner
Block type: 1 (Simple)
Fields:
badge → text
heading → text required
price → text required
period → select options: Monthly→monthly, Yearly→yearly
default: monthly
features → richtext
ctaLabel → text
ctaUrl → aem-content
highlighted → boolean default: false
Variants? nBlock Type 2 — Simple with Tabs
Use for: Blocks with many fields — group into labelled tabs in the UE properties panel.
Example: Talent Hub (Profile + Skills tabs)
Block name: talent-hub
Block type: 2 (Simple with tabs)
Tab 1: Profile
name → text required
age → number min:18, max:60
gender → radio-group options: Male→male, Female→female, Other→other
bio → richtext
photo → reference
Tab 2: Skills
skills → checkbox-group options: AEM→aem, EDS→eds, React→react
experience → number min:0, max:40, default: 2
links → aem-content multi: true
Variants? n
Conditions? nUE Properties panel:
[ Profile ] [ Skills ]
Name * [ ]
Age [ 28 ↕ ]
Gender ● Male ○ Female ○ Other
Bio [ rich text ]
Photo [ DAM picker ]Clicking Skills tab switches to skills fields.
Example: Product Card (Content + Media tabs)
Block name: product-card
Block type: 2 (Simple with tabs)
Tab 1: Content
title → text required, maxLength: 80
description → richtext
price → text
badge → select options: New→new, Sale→sale, Featured→featured
Tab 2: Media
image → reference
videoUrl → aem-content
Tab 3: Actions
ctaLabel → text
ctaUrl → aem-content
secondaryLabel → text
secondaryUrl → aem-contentBlock Type 3 — Container
Use for: Blocks with repeating child items — each child has its own properties panel.
Example: Carousel with config
Block name: carousel
Block type: 3 (Container)
Phase 1 — Parent config:
interval → number default: 5, min: 1, max: 10
autoplay → boolean default: true
Phase 2 — Child item name: carousel-slide
image → reference
caption → text
ctaLabel → text
ctaUrl → aem-content
Variants? n
Conditions? nGenerated _carousel.json:
{
"definitions": [
{
"title": "Carousel", "id": "carousel",
"plugins": { "xwalk": { "page": {
"resourceType": "core/franklin/components/block/v1/block",
"template": { "name": "Carousel", "model": "carousel", "filter": "carousel" }
}}}
},
{
"title": "Carousel Slide", "id": "carousel-slide",
"plugins": { "xwalk": { "page": {
"resourceType": "core/franklin/components/block/v1/block/item",
"template": { "name": "Carousel Slide", "model": "carousel-slide" }
}}}
}
],
"models": [
{ "id": "carousel", "fields": [
{ "component": "number", "name": "interval", "value": 5, "min": 1, "max": 10 },
{ "component": "boolean", "name": "autoplay", "value": true }
]},
{ "id": "carousel-slide", "fields": [
{ "component": "reference", "name": "image", "multi": false },
{ "component": "text", "name": "caption" },
{ "component": "text", "name": "ctaLabel" },
{ "component": "aem-content", "name": "ctaUrl" }
]}
],
"filters": [{ "id": "carousel", "components": ["carousel-slide"] }]
}Authoring in UE:
Click + in Section → Carousel
Click on block → Properties:
Interval [ 5 ↕ ] (1–10)
Autoplay [ on ]
Click + INSIDE Carousel → Carousel Slide
Click on slide → Properties:
Image [ DAM picker ]
Caption [ ]
CTA Label[ ]
CTA URL [ page picker ]
Repeat + for each slide.Example: Cards block with conditional fields
Block name: cards
Block type: 3 (Container)
Phase 1 — Parent config:
layoutVariant → select
Grid → grid
Carousel (Peek) → carousel
Carousel (Side/Center)→ side-carousel
default: grid
visibleCards → select
2 → 2 / 3 → 3 / 4 → 4
default: 4
autoplay → boolean default: false
infiniteLoop → boolean default: false
sectionContent → richtext
backgroundImage → reference multi: false
Phase 2 — Child item name: card
image → reference multi: false
title → text
description → richtext
ctaLabel → text (label: CTA Link Text)
ctaUrl → aem-content (label: CTA Link URL)
Variants? n
Add conditional visibility? y
visibleCards → shows when layoutVariant === carousel
autoplay → shows when layoutVariant is carousel OR side-carousel
infiniteLoop → shows when layoutVariant is carousel OR side-carouselIn UE — Grid selected:
Layout Variant [ Grid ▾ ]
Section Content [ richtext ]
Background Image [ DAM picker ]visibleCards, autoplay, infiniteLoop hidden
In UE — Carousel (Peek) selected:
Layout Variant [ Carousel (Peek) ▾ ]
Visible Cards [ 4 ▾ ] ← appears
Autoplay [ off ] ← appears
Infinite Loop [ off ] ← appears
Section Content [ richtext ]
Background Image [ DAM picker ]Block Type 4 — Container with Tabs
Use for: Container blocks where the parent config has many fields — group parent config into tabs.
Example: Media Tabs
Block name: media-tabs
Block type: 4 (Container with tabs)
Phase 1 — Parent config (tabbed):
Tab 1: Config
defaultTab → number default: 1, min: 1
theme → select options: Light→light, Dark→dark
Tab 2: Layout
alignment → radio-group options: Left→left, Right→right
columns → number default: 3, min: 1, max: 4
Phase 2 — Child item name: media-tab
tabLabel → text required
image → reference
content → richtext
ctaLabel → text
ctaUrl → aem-content
Variants? y
Compact → compact
Full → full
default: fullExample: FAQ Block
Block name: faq
Block type: 4 (Container with tabs)
Phase 1 — Parent config (tabbed):
Tab 1: Appearance
theme → select options: Light→light, Dark→dark default: light
iconStyle → select options: Arrow→arrow, Plus→plus default: arrow
Tab 2: Behaviour
allowMultiple → boolean default: false
firstOpen → boolean default: true
animate → boolean default: true
Phase 2 — Child item name: faq-item
question → text required
answer → richtext required
category → select options: General→general, Technical→technical
featured → boolean default: falseBlock Type 5 — Section Wrapper
Use for: Tab panel where each section IS a tab. Different blocks can go inside each tab.
Example: Tab Panel
Block name: tab-panel
Block type: 5 (Section wrapper)
Tab label field name: tab-label
Tab label field label: Tab Label
Required: y
Custom error message: Please enter a tab label
Allowed blocks inside (pick from project):
hero, banner, carousel, cards, accordion
Create companion navigation block? y
Companion name: tab-list
Fields:
listPosition → select
Left-aligned → left
Center-aligned → center
default: leftGenerated _tab-panel.json:
{
"definitions": [
{
"title": "Tab Panel", "id": "tab-panel",
"plugins": { "xwalk": { "page": {
"resourceType": "core/franklin/components/section/v1/section",
"template": {
"name": "Tab Panel", "model": "tab-panel",
"filter": "tab-panel", "tab-label": "Tab Label"
}
}}}
},
{
"title": "Tab List", "id": "tab-list",
"plugins": { "xwalk": { "page": {
"resourceType": "core/franklin/components/block/v1/block",
"template": { "name": "Tab List", "model": "tab-list" }
}}}
}
],
"models": [
{ "id": "tab-panel", "fields": [
{ "component": "text", "name": "tab-label", "label": "Tab Label", "required": true,
"validation": { "customErrorMsg": "Please enter a tab label" }}
]},
{ "id": "tab-list", "fields": [
{ "component": "select", "name": "listPosition", "label": "List Position",
"options": [{"name":"Left-aligned","value":"left"},{"name":"Center-aligned","value":"center"}],
"value": "left" }
]}
],
"filters": [{
"id": "tab-panel",
"components": ["hero","banner","carousel","cards","accordion","tab-list"]
}]
}Automatically updates models/_section.json — adds tab-panel to section filter so it appears in UE section picker.
Authoring in UE:
Click + at section level → Tab Panel
Set Tab Label: "Products"
Click + INSIDE → Tab List (sets nav position)
Click + INSIDE → Hero, Cards (content blocks)
Click + at section level → Tab Panel (Tab 2)
Set Tab Label: "Services"
Click + INSIDE → Tab List
Click + INSIDE → Carousel
Each section = one tab. No limit.Conditional Fields
Fields can show or hide based on the value of a select, radio-group, or boolean field. After defining all fields the scaffold asks:
Add conditional visibility to any fields? [y/N]: y
Which field should show/hide conditionally?
1. layoutVariant (select)
2. visibleCards (select)
3. autoplay (boolean)
4. infiniteLoop (boolean)
Pick field: 2
Based on which field:
1. layoutVariant (select)
3. autoplay (boolean)
Pick controlling field: 1
Condition type:
1. === equals one value
2. or equals any of multiple values
3. !== not equal to
Type: 1
Available values for "layoutVariant":
1. Grid → grid
2. Carousel (Peek) → carousel
3. Carousel (Side) → side-carousel
Select value: 2
✔ "visibleCards" shows when "layoutVariant" === carouselGenerated JSON:
{
"component": "select",
"name": "visibleCards",
"label": "Visible Cards",
"condition": {
"===": [{ "var": "layoutVariant" }, "carousel"]
}
}OR condition (multiple values):
{
"component": "boolean",
"name": "autoplay",
"condition": {
"or": [
{ "===": [{ "var": "layoutVariant" }, "carousel"] },
{ "===": [{ "var": "layoutVariant" }, "side-carousel"] }
]
}
}Field Types Reference
| Type | Component | multi | Notes |
|---|---|---|---|
| text | text | ✔ | Single line |
| textarea | text-area | ✖ | Multi-line plain text |
| richtext | richtext | ✖ | WYSIWYG editor |
| reference | reference | ✔ | DAM asset picker |
| aem-content | aem-content | ✔ | Page / link picker |
| aem-content-fragment | aem-content-fragment | ✖ | CF picker |
| aem-experience-fragment | aem-experience-fragment | ✖ | XF picker |
| boolean | boolean | ✖ | Toggle on/off |
| select | select | ✖ | Single choice dropdown |
| multiselect | multiselect | ✖ | Multi choice dropdown |
| radio-group | radio-group | ✖ | Radio buttons |
| checkbox-group | checkbox-group | ✖ | Checkboxes |
| number | number | ✖ | Numeric input |
| aem-tag | aem-tag | ✖ | cq:tags picker |
Validation Reference
| Property | Applies to | Where |
|---|---|---|
| required | all | top-level |
| readOnly | all | top-level |
| hidden | all | top-level |
| description | all | top-level |
| min / max / step | number | top-level |
| minLength / maxLength | text, textarea | inside validation |
| rootPath | aem-content, aem-content-fragment | inside validation |
| customErrorMsg | all | inside validation |
After scaffolding
# 1. Add block to section filter (simple/container blocks only)
# Open component-filters.json → add block name to section entry
# 2. Rebuild global JSON files
npm run build:json
# 3. Commit and push
git add .
git commit -m "feat: add carousel block"
git pushSection wrapper blocks automatically update
models/_section.json. All other block types require a one-line manual edit tocomponent-filters.json.
Why not Adobe's tools?
| Tool | What it does | Generates UE JSON | |---|---|---| | AEM Boilerplate XWalk | Project template | ✖ | | AEM CLI | Local dev server | ✖ | | Block Collection | Copy-paste library | ✖ | | GitHub Copilot | Code suggestions | Partial | | aem-eds-cli | Block scaffolding | ✔ Complete |
Requirements
- Node.js 16+
- AEM Edge Delivery Services project with Universal Editor setup
npm run build:jsonscript (standard in AEM boilerplate)
License
Apache 2.0
