eds-block-development
Guide for developing EDS blocks using vanilla JavaScript, Content Driven Development, and block decoration patterns. Covers block structure, decorate function, content extraction, DOM manipulation, and EDS best practices for Adobe Edge Delivery Services.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: eds-block-development
description: Guide for developing EDS blocks using vanilla JavaScript, Content Driven Development, and block decoration patterns. Covers block structure, decorate function, content extraction, DOM manipulation, and EDS best practices for Adobe Edge Delivery Services.
---
# EDS Block Development Guide
## ⚠️ CRITICAL WARNING: EDS Reserved Class Names
**BEFORE WRITING ANY CODE, READ THIS:**
EDS automatically adds these class names to your blocks:
- `.{blockname}-container` - Added to parent `<section>` element
- `.{blockname}-wrapper` - Added to block's parent `<div>` wrapper
**❌ NEVER use these suffixes in your CSS or JavaScript:**
```css
/* ❌ PRODUCTION BUG - Will break entire page */
.overlay-container { position: fixed; opacity: 0; }
/* ✅ SAFE - Use different suffix */
.overlay-backdrop { position: fixed; opacity: 0; }
```
**Safe suffixes:** `-backdrop`, `-panel`, `-inner`, `-grid`, `-list`, `-content`, `-dialog`, `-popup`
See [CSS Best Practices](#critical-avoid-eds-reserved-class-names) section below for full details.
---
## Purpose
Guide developers through creating and modifying Adobe Edge Delivery Services (EDS) blocks following vanilla JavaScript patterns, Content Driven Development principles, and EDS best practices.
## When to Use This Skill
Automatically activates when:
- Creating new blocks in `/blocks/`
- Modifying existing block JavaScript (`.js` files)
- Implementing block decoration patterns
- Working with EDS content structures
- Using keywords: "block", "decorate", "EDS block"
---
## Quick Start: Block Structure
### File Organization
Every EDS block follows this structure:
```
blocks/your-block/
├── your-block.js # Decoration logic (REQUIRED)
├── your-block.css # Block-specific styles (REQUIRED)
├── README.md # Usage documentation (REQUIRED)
├── EXAMPLE.md # Google Docs example (REQUIRED)
└── test.html # Development test file (RECOMMENDED)
```
**Critical naming convention:** File names must match the block name exactly (kebab-case).
---
## Google Docs Table Structure (CRITICAL)
### How EDS Recognizes Blocks
**The first row of a table in Google Docs is the block name that drives everything:**
1. **First row (header row)** = Block name (e.g., "overlay", "cards", "hero")
- EDS uses this name to load `/blocks/{name}/{name}.js`
- EDS uses this name to load `/blocks/{name}/{name}.css`
- This triggers the `decorate()` function
- **WITHOUT THIS, YOUR BLOCK WILL NOT LOAD**
2. **Subsequent rows** = Block content (data rows)
- Row 2, Row 3, etc. become `block.children[0]`, `block.children[1]`, etc.
- Your `decorate()` function processes these rows
### Example: Google Docs Table
```
| overlay | ← HEADER ROW (block name) - CRITICAL!
|----------------|
| Learn More | ← Row 2 becomes block.children[0]
| Welcome! ... | ← Row 3 becomes block.children[1]
```
**What EDS does:**
1. Sees header row "overlay"
2. Loads `/blocks/overlay/overlay.js`
3. Loads `/blocks/overlay/overlay.css`
4. Calls `decorate(blockElement)`
5. Your code processes rows 2 and 3
### Common Mistake
❌ **WRONG** - No header row:
```
| Learn More |
| Welcome! ... |
```
Result: Block not recognized, CSS/JS not loaded, no decoration happens
✅ **CORRECT** - Header row with block name:
```
| overlay | ← Must match /blocks/overlay/
|----------------|
| Learn More |
| Welcome! ... |
```
---
## The Decorate Function Pattern
All EDS blocks export a default `decorate` function that receives the block element:
```javascript
export default function decorate(block) {
// 1. Configuration (at the top)
const config = {
animationDuration: 300,
maxItems: 10,
errorMessage: 'Failed to load content'
};
// 2. Extract content from EDS structure
const rows = Array.from(block.children);
const content = rows.map(row => {
const cells = Array.from(row.children);
return cells.map(cell => cell.textContent.trim());
});
// 3. Create new DOM structure
const container = document.createElement('div');
container.className = 'your-block-wrapper';
// 4. Build your component
content.forEach(([title, description]) => {
const item = document.createElement('div');
item.className = 'your-block-item';
item.innerHTML = `
<h3>${title}</h3>
<p>${description}</p>
`;
container.appendChild(item);
});
// 5. Setup event handlers
container.querySelectorAll('.your-block-item').forEach(item => {
item.addEventListener('click', () => {
console.log('Item clicked');
});
});
// 6. Replace block content
block.textContent = '';
block.appendChild(container);
}
```
---
## Content Extraction Patterns
### Basic Two-Column Pattern
```javascript
export default function decorate(block) {
const rows = Array.from(block.children);
const items = rows.map(row => {
const [titleCell, descriptionCell] = row.children;
return {
title: titleCell?.textContent?.trim() || '',
description: descriptionCell?.textContent?.trim() || ''
};
});
// Use the items...
}
```
### Picture Extraction Pattern
```javascript
function extractPicture(cell) {
const picture = cell.querySelector('picture');
if (!picture) return null;
return {
img: picture.querySelector('img'),
sources: Array.from(picture.querySelectorAll('source'))
};
}
export default function decorate(block) {
const rows = Array.from(block.children);
rows.forEach(row => {
const [imageCell, contentCell] = row.children;
const picture = extractPicture(imageCell);
if (picture) {
// Use the picture element
}
});
}
```
### Link Extraction Pattern
```javascript
function extractLink(cell) {
const link = cell.querySelector('a');
return link ? {
href: link.href,
text: link.textContent.trim(),
target: link.target
} : null;
}
```
---
## DOM Manipulation Best Practices
### 1. Clear the Block First
```javascript
export default function decorate(block) {
// Extract data first
const data = extractContent(block);
// Clear the block
block.textContent = '';
// Add new content
const container = createNewStructure(data);
block.appendChild(container);
}
```
### 2. Use Document Fragments for Multiple Elements
```javascript
function createItems(data) {
const fragment = document.createDocumentFragment();
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
fragment.appendChild(element);
});
return fragment;
}
export default function decorate(block) {
const data = extractContent(block);
block.textContent = '';
block.appendChild(createItems(data));
}
```
### 3. Minimize DOM Manipulation
```javascript
// ❌ BAD - Multiple reflows
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
block.appendChild(element); // Triggers reflow each time
});
// ✅ GOOD - Single reflow
const fragment = document.createDocumentFragment();
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
fragment.appendChild(element);
});
block.appendChild(fragment); // Single reflow
```
---
## Error Handling
### Basic Error Handling
```javascript
export default function decorate(block) {
try {
const config = { /* ... */ };
const content = extractContent(block);
if (!content || content.length === 0) {
throw new Error('No content found');
}
const container = createStructure(content);
block.textContent = '';
block.appendChild(container);
} catch (error) {
console.error('Block decoration failed:', error);
block.innerHTML = '<p class="error-message">Unable to load content</p>';
}
}
```
### Async Operations
```javascript
export default async function decorate(block) {
try {
// Show loading state
block.innerHTML = '<p class="loading">Loading...</p>';
// Fetch data
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// Clear and render
block.textContent = '';
block.appendChild(createStructure(data));
} catch (error) {
console.error('Failed to load data:', error);
block.innerHTML = '<p class="error-message">Failed to load content</p>';
}
}
```
---
## CSS Best Practices
### Block-Specific Naming
```css
/* Namespace all classes with block name */
.your-block-wrapper {
/* Container styles */
}
.your-block-item {
/* Item styles */
}
.your-block-title {
/* Title styles */
}
/* Use BEM naming for variants */
.your-block-item--featured {
/* Featured variant */
}
.your-block-item__icon {
/* Item element */
}
```
### ⚠️ CRITICAL: Avoid EDS Reserved Class Names
**EDS automatically adds these classes:**
- `.{blockname}-wrapper` - Added to the block's parent `<div>` wrapper
- `.{blockname}-container` - Added to the parent `<section>` element
- `.block` - Added to all block elements
- `.section` - Added to all section elements
- `.button-container` - Added to parent elements of buttons
- `.default-content-wrapper` - Added to default content wrappers
**DO NOT use these class names in your CSS or JavaScript:**
```css
/* ❌ BAD - Conflicts with EDS automatic naming */
.overlay-container {
position: fixed; /* Will be applied to the section, breaking layout */
}
.cards-wrapper {
display: grid; /* Will conflict with EDS's .cards-wrapper on block element */
}
/* ✅ GOOD - Use different suffixes */
.overlay-backdrop {
position: fixed; /* Safe - won't conflict */
}
.cards-grid {
display: grid; /* Safe - different name */
}
.overlay-modal-container {
/* Safe - more specific name */
}
```
**Why this matters:**
- EDS's `decorateBlock()` adds `.{blockname}-wrapper` to block parent divs (line 682)
- EDS's `decorateBlock()` adds `.{blockname}-container` to parent sections (line 684)
- EDS's `decorateBlock()` adds `.block` to all block elements (line 677)
- EDS's `decorateSections()` adds `.section` to all sections (line 503)
- EDS's `decorateButtons()` adds `.button-container` to button parents (lines 430, 439, 448)
- If your CSS uses these same class names, styles will be applied to the wrong elements
- This can cause invisible pages, broken layouts, or unexpected behavior
**Additional conflicts to avoid:**
```css
/* ❌ Never style these EDS-generated classes with layout-breaking properties */
.block {
position: fixed; /* Will break ALL blocks on the page */
}
.section {
display: none; /* Will hide ALL sections */
}
.button-container {
position: absolute; /* Will break ALL button layouts */
}
```
**Safe naming patterns:**
- `.{blockname}-backdrop`
- `.{blockname}-modal`
- `.{blockname}-content`
- `.{blockname}-inner`
- `.{blockname}-grid`
- `.{blockname}-list`
- `.{blockname}-panel`
- `.{blockname}-overlay`
**Reference:** See `scripts/aem.js`:
- Lines 674-686: `decorateBlock()` - adds wrapper/container classes
- Lines 489-530: `decorateSections()` - adds section classes
- Lines 421-453: `decorateButtons()` - adds button-container classes
### Mobile-First Responsive Design
```css
/* Base styles (mobile) */
.your-block-item {
padding: 1rem;
margin-bottom: 1rem;
}
/* Tablet */
@media (min-width: 600px) {
.your-block-item {
padding: 1.5rem;
}
}
/* Desktop */
@media (min-width: 900px) {
.your-block-item {
padding: 2rem;
}
}
```
### Use CSS Variables
```css
.your-block {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--body-font-family);
padding: var(--spacing-m);
}
```
---
## Accessibility
### Semantic HTML
```javascript
export default function decorate(block) {
const container = document.createElement('nav'); // Use semantic elements
container.setAttribute('aria-label', 'Block navigation');
const list = document.createElement('ul');
items.forEach(item => {
const li = document.createElement('li');
const button = document.createElement('button');
button.textContent = item.text;
button.setAttribute('aria-label', `Open ${item.text}`);
li.appendChild(button);
list.appendChild(li);
});
container.appendChild(list);
block.textContent = '';
block.appendChild(container);
}
```
### Keyboard Navigation
```javascript
export default function decorate(block) {
const items = block.querySelectorAll('.your-block-item');
items.forEach((item, index) => {
// Make items focusable
item.setAttribute('tabindex', '0');
// Handle keyboard events
item.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
item.click();
}
if (e.key === 'ArrowDown' && items[index + 1]) {
items[index + 1].focus();
}
if (e.key === 'ArrowUp' && items[index - 1]) {
items[index - 1].focus();
}
});
});
}
```
---
## Performance Optimization
### 1. Lazy Loading Images
```javascript
export default function decorate(block) {
const images = block.querySelectorAll('img');
images.forEach(img => {
img.setAttribute('loading', 'lazy');
});
}
```
### 2. Debouncing Event Handlers
```javascript
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
export default function decorate(block) {
const handleResize = debounce(() => {
// Resize logic
}, 250);
window.addEventListener('resize', handleResize);
}
```
### 3. Use requestIdleCallback for Non-Critical Work
```javascript
export default function decorate(block) {
// Critical rendering
const container = createStructure(data);
block.appendChild(container);
// Non-critical work
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
// Analytics, non-critical enhancements, etc.
trackBlockView(block);
});
} else {
setTimeout(() => {
trackBlockView(block);
}, 1);
}
}
```
---
## Testing Your Block
### Create test.html
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Your Block Test</title>
<!-- EDS Core Styles -->
<link rel="stylesheet" href="/styles/styles.css">
<!-- Block CSS is loaded automatically by loadBlock() -->
<style>
/* EDS pattern - ensure body appears */
body.appear {
display: block;
}
</style>
</head>
<body>
<!-- Note: Only use block name class, .block is added automatically -->
<div class="your-block">
<div>
<div>Title 1</div>
<div>Description 1</div>
</div>
<div>
<div>Title 2</div>
<div>Description 2</div>
</div>
</div>
<script type="module">
import { loadBlock } from '/scripts/aem.js';
// Add body.appear class (required to show page)
document.body.classList.add('appear');
// Load and decorate the block
const block = document.querySelector('.your-block');
// Add .block class to mimic EDS production behavior
block.classList.add('block');
await loadBlock(block);
</script>
</body>
</html>
```
**Important:**
- The `.block` class is added by the script (`block.classList.add('block')`) to mimic EDS production behavior
- In production, EDS's `decorateBlock()` function automatically adds this class
- Your HTML only needs the block name class (e.g., `class="your-block"`)
### Test with Development Server
```bash
npm run debug
```
Access your test at: `http://localhost:3000/blocks/your-block/test.html`
---
## Common Patterns
### Configuration Object
```javascript
export default function decorate(block) {
// Configuration at the top
const config = {
autoplay: block.dataset.autoplay === 'true',
delay: parseInt(block.dataset.delay) || 3000,
animation: block.dataset.animation || 'fade'
};
// Use config throughout
}
```
### Data Attributes for Options
```javascript
export default function decorate(block) {
// Read options from data attributes
const layout = block.dataset.layout || 'grid';
const columns = parseInt(block.dataset.columns) || 3;
// Apply classes based on options
block.classList.add(`layout-${layout}`);
block.style.setProperty('--columns', columns);
}
```
### Helper Functions
```javascript
// Helper functions outside decorate
function createCard(data) {
const card = document.createElement('div');
card.className = 'card';
card.innerHTML = `
<h3>${data.title}</h3>
<p>${data.description}</p>
`;
return card;
}
export default function decorate(block) {
const data = extractContent(block);
const container = document.createElement('div');
data.forEach(item => {
container.appendChild(createCard(item));
});
block.textContent = '';
block.appendChild(container);
}
```
---
## Common Mistakes to Avoid
### ❌ CRITICAL: Don't Use EDS Reserved Class Names
```javascript
// ❌ BAD - Class name conflicts with EDS automatic naming
function createOverlay(content) {
const backdrop = document.createElement('div');
backdrop.className = 'overlay-container'; // EDS adds this to parent section!
return backdrop;
}
// ✅ GOOD - Use different class name
function createOverlay(content) {
const backdrop = document.createElement('div');
backdrop.className = 'overlay-backdrop'; // Safe - won't conflict
return backdrop;
}
```
**Never use these patterns in your code:**
- `.{blockname}-container` - Reserved by EDS for parent sections
- `.{blockname}-wrapper` - Reserved by EDS for block elements
**This mistake caused a production bug:** Using `.overlay-container` in CSS with `position: fixed; z-index: 999; opacity: 0;` made entire pages invisible because EDS added `overlay-container` class to the parent section, applying those styles to the wrong element.
### ❌ Don't Forget to Clear the Block
```javascript
// ❌ BAD - Original content remains
export default function decorate(block) {
const container = document.createElement('div');
block.appendChild(container); // Adds to existing content
}
// ✅ GOOD - Clear first
export default function decorate(block) {
const data = extractContent(block);
block.textContent = ''; // Clear first
block.appendChild(container);
}
```
### ❌ Don't Use innerHTML for User Content
```javascript
// ❌ BAD - XSS vulnerability
export default function decorate(block) {
const userInput = block.textContent;
block.innerHTML = `<div>${userInput}</div>`; // Dangerous!
}
// ✅ GOOD - Use textContent or createElement
export default function decorate(block) {
const userInput = block.textContent;
const div = document.createElement('div');
div.textContent = userInput; // Safe
block.textContent = '';
block.appendChild(div);
}
```
### ❌ Don't Forget Error Handling
```javascript
// ❌ BAD - No error handling
export default async function decorate(block) {
const response = await fetch('/api/data');
const data = await response.json();
renderData(block, data);
}
// ✅ GOOD - Proper error handling
export default async function decorate(block) {
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
renderData(block, data);
} catch (error) {
console.error('Failed to load:', error);
block.innerHTML = '<p>Failed to load content</p>';
}
}
```
---
## Related Documentation
- **[Block Architecture Standards](../../../docs/for-ai/implementation/block-architecture-standards.md)** - Comprehensive architecture guide
- **[Frontend Guidelines](../../../docs/for-ai/guidelines/frontend-guidelines.md)** - JavaScript and CSS standards
- **[EDS Native