| 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. |
| mx | {"canonicalUri":"https://raw.githubusercontent.com/ddttom/allaboutv2/main/.claude/skills/eds-block-development/SKILL.md"} |
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:
.overlay-container { position: fixed; opacity: 0; }
.overlay-backdrop { position: fixed; opacity: 0; }
Safe suffixes: -backdrop, -panel, -inner, -grid, -list, -content, -dialog, -popup
See CSS Best Practices 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)
└── block-architecture.md # Technical architecture (OPTIONAL - for complex blocks)
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:
-
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
-
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:
- Sees header row "overlay"
- Loads
/blocks/overlay/overlay.js
- Loads
/blocks/overlay/overlay.css
- Calls
decorate(blockElement)
- 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:
export default function decorate(block) {
const config = {
animationDuration: 300,
maxItems: 10,
errorMessage: 'Failed to load content'
};
const rows = Array.from(block.children);
const content = rows.map(row => {
const cells = Array.from(row.children);
return cells.map(cell => cell.textContent.trim());
});
const container = document.createElement('div');
container.className = 'your-block-wrapper';
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);
});
container.querySelectorAll('.your-block-item').forEach(item => {
item.addEventListener('click', () => {
console.log('Item clicked');
});
});
block.textContent = '';
block.appendChild(container);
}
Configuration Object Architecture
⚠️ CRITICAL: Separation of Concerns
Every block must distinguish between:
- Global Constants - Developer-facing messages and module-level state
- Runtime Configuration - User-facing settings, durations, URLs, icons, text
- Metadata Overrides - Notebook-specific configuration from
.ipynb metadata
Global Constants Pattern
Place at the top of your JavaScript file (before all other code):
const BLOCKNAME_ERRORS = {
CONFIG_MISSING: 'Incomplete code: Configuration object missing. Block cannot initialize.',
INVALID_DATA: 'Invalid data format. Expected array of objects.',
FETCH_FAILED: 'Failed to fetch remote data.',
};
const BLOCKNAME_CACHE = new Map();
Why this matters:
- Error messages are developer-facing - they help developers debug issues
- Placing them at the top makes them easily searchable with Ctrl+F
- Separates concerns: developers modify global constants, users/metadata configure runtime behavior
- Never put error messages in the config object - they're not runtime configuration
Runtime Configuration Object
Define inside decorate() function:
export default function decorate(block) {
const config = {
errorMessage: 'Failed to load content',
loadingMessage: 'Loading...',
animationDuration: 300,
defaultTimeout: 5000,
maxItems: 10,
maxRetries: 3,
apiEndpoint: '/api/data.json',
fallbackUrl: 'https://example.com/fallback',
icons: {
close: '×',
arrow: '▶',
warning: '⚠',
},
text: {
submitButton: 'Submit',
cancelButton: 'Cancel',
confirmMessage: 'Are you sure?',
},
};
const metadata = block.dataset;
const timeout = metadata.timeout ? parseInt(metadata.timeout) : config.defaultTimeout;
}
What belongs in config object:
- ✅ Durations and timing values
- ✅ API endpoints and URLs
- ✅ UI text and icons
- ✅ Limits and thresholds
- ✅ User-facing messages
- ❌ Error messages (use global constants)
- ❌ Module-level caches (use module constants)
Metadata Override Pattern
For .ipynb notebooks:
export default function decorate(block) {
const config = { };
const notebookData = JSON.parse(block.textContent || '{}');
const metadata = notebookData.metadata || {};
const splashDuration = metadata['splash-duration']
? metadata['splash-duration'] * 1000
: config.defaultSplashDuration;
const autorun = metadata.autorun === true;
showSplashScreen(splashDuration);
}
For EDS blocks with data attributes:
export default function decorate(block) {
const config = { };
const layout = block.dataset.layout || 'grid';
const columns = parseInt(block.dataset.columns) || config.defaultColumns;
block.classList.add(`layout-${layout}`);
block.style.setProperty('--columns', columns);
}
Dependency Injection for Config
When config is needed in helper functions, use dependency injection:
function createOverlay(data) {
const icon = config.icons.close;
}
function createOverlay(data, config) {
const icon = config.icons.close;
}
export default function decorate(block) {
const config = { };
const overlay = createOverlay(data, config);
}
For nested function calls:
function createOverlay(data, config) {
if (!config) {
console.error('[BLOCK] CRITICAL: Config object missing in createOverlay');
const errorDiv = document.createElement('div');
errorDiv.className = 'block-error';
errorDiv.textContent = BLOCKNAME_ERRORS.CONFIG_MISSING;
return errorDiv;
}
const content = renderContent(data, config);
const footer = renderFooter(config);
}
function renderContent(data, config) {
const icon = config.icons.arrow;
}
export default function decorate(block) {
const config = { };
const overlay = createOverlay(data, config);
}
Config Validation Pattern
CRITICAL: Never use hardcoded fallbacks with || operator:
function createOverlay(data, config = null) {
const icon = config?.icons?.close || '×';
const timeout = config?.timeout || 5000;
}
function createOverlay(data, config = null) {
if (!config) {
console.error('[BLOCK] CRITICAL: Config object missing');
const errorDiv = document.createElement('div');
errorDiv.textContent = BLOCKNAME_ERRORS.CONFIG_MISSING;
return errorDiv;
}
const icon = config.icons.close;
const timeout = config.timeout;
}
Why this matters:
- Fallbacks hide configuration issues
- Block should fail explicitly if config missing
- Makes bugs obvious during development
- Forces proper dependency injection
Only acceptable use of fallback:
const layout = config ? config.layout : 'grid';
const duration = metadata['duration'] || config.defaultDuration;
Refactoring Existing Blocks
When reviewing or modifying existing blocks, check for:
-
Hardcoded values scattered throughout code
- Extract to config object at top of
decorate()
- Group by category (messages, timing, limits, etc.)
-
Error messages in function bodies
- Move to
BLOCKNAME_ERRORS global constant at file top
- Update all usages to reference global constant
-
Functions accessing config globally
- Add
config parameter to function signature
- Pass config explicitly from call sites
- Add guard clause for config validation
-
Hardcoded fallbacks using || operator
- Replace with guard clauses that fail explicitly
- Use ternary only for intentional defaults
Refactoring checklist:
function helper() {
const icon = '×';
const timeout = 5000;
if (error) {
showError('Failed');
}
}
const BLOCK_ERRORS = {
FETCH_FAILED: 'Failed to load data',
};
export default function decorate(block) {
const config = {
icons: { close: '×' },
timeout: 5000,
};
helper(config);
}
function helper(config) {
if (!config) {
console.error(BLOCK_ERRORS.CONFIG_MISSING);
return;
}
const icon = config.icons.close;
const timeout = config.timeout;
if (error) {
showError(BLOCK_ERRORS.FETCH_FAILED);
}
}
Real-World Example: ipynb-viewer
See blocks/ipynb-viewer/ipynb-viewer.js for comprehensive implementation:
- Global constants (lines 14-24): Error messages and module cache
- Config object (lines 4073-4105): Runtime settings organized by category
- Metadata overrides (lines 4120-4125): Splash duration from notebook metadata
- Dependency injection (lines 2236, 3349): Config passed through function chain
- Guard clauses (lines 2238-2245, 3351-3362): Fail explicitly if config missing
Key architectural decisions:
- Error messages promoted to global constants (easily searchable)
- All hardcoded values moved to config object
- Config passed explicitly through all function calls
- No
|| fallbacks - functions fail with clear errors
- Metadata overrides use ternary for intentional defaults
Content Extraction Patterns
Basic Two-Column Pattern
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() || ''
};
});
}
Picture Extraction Pattern
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) {
}
});
}
Link Extraction Pattern
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
export default function decorate(block) {
const data = extractContent(block);
block.textContent = '';
const container = createNewStructure(data);
block.appendChild(container);
}
2. Use Document Fragments for Multiple Elements
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
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
block.appendChild(element);
});
const fragment = document.createDocumentFragment();
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item;
fragment.appendChild(element);
});
block.appendChild(fragment);
Error Handling
Basic Error Handling
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
export default async function decorate(block) {
try {
block.innerHTML = '<p class="loading">Loading...</p>';
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
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
.your-block-wrapper {
}
.your-block-item {
}
.your-block-title {
}
.your-block-item--featured {
}
.your-block-item__icon {
}
⚠️ 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:
.overlay-container {
position: fixed;
}
.cards-wrapper {
display: grid;
}
.overlay-backdrop {
position: fixed;
}
.cards-grid {
display: grid;
}
.overlay-modal-container {
}
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:
.block {
position: fixed;
}
.section {
display: none;
}
.button-container {
position: absolute;
}
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
.your-block-item {
padding: 1rem;
margin-bottom: 1rem;
}
@media (min-width: 600px) {
.your-block-item {
padding: 1.5rem;
}
}
@media (min-width: 900px) {
.your-block-item {
padding: 2rem;
}
}
Use CSS Variables
.your-block {
background-color: var(--background-color);
color: var(--text-color);
font-family: var(--body-font-family);
padding: var(--spacing-m);
}
⚠️ CRITICAL: Verify HTML Structure Before Writing CSS
NEVER assume what HTML elements exist - always inspect the actual DOM structure first.
Why this matters:
- Different parsers generate different HTML structures
- CSS targeting non-existent elements wastes debugging time
- Assumptions about
<p> tags, <div> wrappers, or structure can be wrong
Real-world example (ipynb-viewer parseMarkdown):
The parseMarkdown() function converts double newlines to <br><br> tags, NOT <p> tags:
html = html.replace(/\n\n+/g, '<br><br>');
html = html.replace(/\n/g, ' ');
Problem pattern:
.your-block p {
margin: 0.15rem 0;
}
.your-block br {
margin: 0.15rem 0;
line-height: 0.15rem;
}
.your-block br + br {
display: none;
}
Best practices when writing CSS:
- Always verify HTML structure - Use DevTools to inspect actual DOM
- Don't assume - Just because you expect
<p> tags doesn't mean they exist
- Check the renderer - Look at how content is generated (parseMarkdown, renderCell, etc.)
- Test in isolation - Add a bright background color to verify selector matches
- Read the source - 5 minutes reading code beats 1 hour of trial-and-error
How to verify:
- Open browser DevTools (F12)
- Inspect the content area element
- Look at the actual HTML generated by your parser/renderer
- Check what elements exist (br vs p vs div vs section)
- Write CSS rules for the actual structure, not the assumed structure
Impact:
- Before verification: Wasted hours debugging "why isn't my CSS working?"
- After verification: Immediate fix by targeting correct elements
- Root cause: Assumptions about HTML structure without inspection
Documentation: See LEARNINGS.md section "ipynb-viewer: parseMarkdown() Uses
Tags, Not
Tags"
Accessibility
Semantic HTML
export default function decorate(block) {
const container = document.createElement('nav');
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
export default function decorate(block) {
const items = block.querySelectorAll('.your-block-item');
items.forEach((item, index) => {
item.setAttribute('tabindex', '0');
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
export default function decorate(block) {
const images = block.querySelectorAll('img');
images.forEach(img => {
img.setAttribute('loading', 'lazy');
});
}
2. Debouncing Event Handlers
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(() => {
}, 250);
window.addEventListener('resize', handleResize);
}
3. Use requestIdleCallback for Non-Critical Work
export default function decorate(block) {
const container = createStructure(data);
block.appendChild(container);
if ('requestIdleCallback' in window) {
requestIdleCallback(() => {
trackBlockView(block);
});
} else {
setTimeout(() => {
trackBlockView(block);
}, 1);
}
}
Testing Your Block
Create test.html
⚠️ CRITICAL: Correct EDS HTML Structure
The HTML structure in test.html must EXACTLY match how EDS transforms Google Docs tables:
Block Element (has block name class)
└── Row(s) (direct children, one <div> per row)
└── Cell(s) (children of row, one <div> per cell)
Example - Two-column block with one row:
<div class="your-block">
<div>
<div>Cell 1 content</div>
<div>Cell 2 content</div>
</div>
</div>
Example - Two-column block with multiple rows:
<div class="your-block">
<div>
<div>Row 1, Cell 1</div>
<div>Row 1, Cell 2</div>
</div>
<div>
<div>Row 2, Cell 1</div>
<div>Row 2, Cell 2</div>
</div>
</div>
❌ COMMON MISTAKE - Extra wrapper div:
<div class="your-block">
<div>
<div>
<div>Cell 1</div>
<div>Cell 2</div>
</div>
</div>
</div>
Complete test.html Template
<!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>
<link rel="stylesheet" href="/styles/styles.css">
<style>
body.appear {
display: block;
}
body {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.test-section {
margin: 2rem 0;
padding: 1.5rem;
border: 2px solid #ccc;
border-radius: 8px;
background: #f9f9f9;
}
.test-section h2 {
margin-top: 0;
}
</style>
</head>
<body>
<h1>Your Block Test Page</h1>
<div class="test-section">
<h2>Test Case 1: Basic Two-Column Block</h2>
<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>
</div>
<div class="test-section">
<h2>Test Case 2: With Images</h2>
<div class="your-block">
<div>
<div>
<picture>
<img src="https://via.placeholder.com/300x200" alt="Test Image">
</picture>
</div>
<div>Content with image</div>
</div>
</div>
</div>
<div class="test-section">
<h2>Test Case 3: Block Variant</h2>
<div class="your-block variant-name">
<div>
<div>Variant content</div>
<div>Testing variant styling</div>
</div>
</div>
</div>
<script type="module">
import { loadBlock } from '/scripts/aem.js';
document.body.classList.add('appear');
const blocks = document.querySelectorAll('.your-block');
console.log(`Testing ${blocks.length} block(s)...`);
for (const block of blocks) {
try {
block.classList.add('block');
block.dataset.blockName = 'your-block';
await loadBlock(block);
console.log('✅ Block loaded:', block.className);
} catch (error) {
console.error('❌ Block failed:', block.className, error);
}
}
console.log('All blocks loaded!');
</script>
</body>
</html>
Important Notes:
- Block Structure: Each block must have rows as direct children, and cells as children of rows
.block class: Added by script to mimic EDS production (where decorateBlock() adds it automatically)
block.dataset.blockName: CRITICAL - Must be set before calling loadBlock(), otherwise you'll get "undefined" errors
body.appear: REQUIRED - EDS hides body by default, this class makes it visible
- Block loading order: Add
body.appear class BEFORE calling loadBlock()
- Multiple blocks: Use
querySelectorAll() and loop to test multiple instances
- Console logging: Add logs to track loading progress and catch errors
- Block wrappers: If your block uses
document.querySelector('.{blockname}-wrapper') (e.g., for expressions plugin), wrap each block in <div class="{blockname}-wrapper"> in test.html
Common Errors:
- If you see
/blocks/undefined/undefined.js 404, you forgot to set block.dataset.blockName!
- If you see
Cannot read properties of null (reading 'firstChild') in expressions.js, you need to wrap blocks in .{blockname}-wrapper divs
Common HTML Structure Mistakes
❌ WRONG - Extra nesting:
<div class="your-block">
<div><div><div>Content</div></div></div>
</div>
❌ WRONG - Missing row wrapper:
<div class="your-block">
<div>Cell 1</div>
<div>Cell 2</div>
</div>
✅ CORRECT - Proper structure:
<div class="your-block">
<div>
<div>Cell 1</div>
<div>Cell 2</div>
</div>
</div>
✅ CORRECT - With wrapper (when block uses .{blockname}-wrapper selector):
<div class="your-block-wrapper">
<div class="your-block">
<div>
<div>Cell 1</div>
<div>Cell 2</div>
</div>
</div>
</div>
When to use wrappers in test.html:
- Your block uses
document.querySelector('.{blockname}-wrapper') in its JavaScript
- Your block depends on external plugins (like expressions) that expect wrappers
- In production, EDS automatically wraps blocks in
.{blockname}-wrapper divs
- Without the wrapper, code that queries for it will get
null and may error
Debugging Tips for test.html
If your test.html doesn't work, check:
-
Structure: Use browser DevTools to inspect the DOM structure
- Right-click block → Inspect Element
- Verify: Block → Row(s) → Cell(s)
-
Classes: Check that .block class was added
- Should see:
<div class="your-block block">
-
Console errors: Open DevTools Console (F12)
- Look for JavaScript errors
- Check if
loadBlock() succeeded
-
Network tab: Check if CSS/JS files loaded
- Should see:
/blocks/your-block/your-block.css
- Should see:
/blocks/your-block/your-block.js
-
Block scoping: Ensure your JS uses block parameter, not global selectors
const cells = block.querySelectorAll('div > div');
const cells = document.querySelectorAll('.your-block div > div');
Test with Development Server
npm run debug
Access your test at: http://localhost:3000/blocks/your-block/test.html
Block Variations
⚠️ CRITICAL: Single JavaScript File for All Variations
MANDATORY RULE: Each block must have exactly ONE JavaScript file, regardless of how many variations it supports.
EDS blocks should NEVER have multiple JavaScript files like:
- ❌
blockname.js, blockname-variation1.js, blockname-variation2.js
- ❌
view-myblog.js, view-myblog-ai.js
Instead, all variation logic must be handled within the single JavaScript file using class detection:
export default async function decorate(block) {
const isVariationA = block.classList.contains('variation-a');
const isVariationB = block.classList.contains('variation-b');
if (isVariationA) {
const data = await fetchAndFilterData();
renderVariationA(block, data);
} else if (isVariationB) {
renderVariationB(block);
} else {
renderStandard(block);
}
}
Why Single File Architecture
- Maintainability: All logic for a block is in one place
- EDS Convention: The system expects one JS file per block
- Performance: Avoids loading multiple files for the same block
- Consistency: Follows the same pattern as CSS variations
- Simplicity: Easier to understand and debug
Real-World Example
A blog block with an AI filter variation:
✅ CORRECT:
blocks/view-myblog/
├── view-myblog.js # Single file with both standard and AI filtering
├── view-myblog.css
└── README.md
❌ INCORRECT:
blocks/view-myblog/
├── view-myblog.js
├── view-myblog-ai.js # DON'T DO THIS
├── view-myblog.css
└── README.md
Implementation pattern:
export default async function decorate(block) {
const isAIVariation = block.classList.contains('ai');
const rawData = await fetchData();
const processedData = isAIVariation ? filterAIContent(rawData) : rawData;
const title = isAIVariation ? 'Latest AI Posts' : 'Latest Posts';
render(block, processedData, title);
}
function filterAIContent(data) {
return data.filter(post =>
post.url.includes('/ai/') ||
post.title.toLowerCase().includes('ai')
);
}
How Authors Use Variations
In Google Docs:
| view-myblog (ai) |
|------------------|
This creates:
<div class="view-myblog ai block">
</div>
Your single JavaScript file detects the ai class and applies appropriate logic.
Common Patterns
Configuration Object
export default function decorate(block) {
const config = {
autoplay: block.dataset.autoplay === 'true',
delay: parseInt(block.dataset.delay) || 3000,
animation: block.dataset.animation || 'fade'
};
}
Data Attributes for Options
export default function decorate(block) {
const layout = block.dataset.layout || 'grid';
const columns = parseInt(block.dataset.columns) || 3;
block.classList.add(`layout-${layout}`);
block.style.setProperty('--columns', columns);
}
Helper Functions
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
function createOverlay(content) {
const backdrop = document.createElement('div');
backdrop.className = 'overlay-container';
return backdrop;
}
function createOverlay(content) {
const backdrop = document.createElement('div');
backdrop.className = 'overlay-backdrop';
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.
⚠️ CRITICAL: When to Use and When to Avoid Global Selectors
Understanding the distinction between block-scoped and document-level operations is essential for EDS block development.
❌ NEVER Use Global Selectors for Block-Scoped Operations
This is the most common bug in EDS blocks!
When decorating a block, NEVER query for the block itself or its children using global selectors. ALWAYS use the block parameter.
export default function decorate(block) {
const bioElement = document.querySelector('.bio');
if (!bioElement.classList.contains('hide-author')) {
const imgElement = document.querySelector('.bio.block img');
const bioBlock = document.querySelector('.bio.block');
bioBlock.appendChild(authorElement);
}
}
Why this is wrong:
document.querySelector('.bio') always returns the FIRST matching element on the page
- If you have multiple bio blocks, they ALL use the first block's configuration
- The second, third, etc. blocks won't work correctly
- Image link conversion fails because it checks the wrong block
export default function decorate(block) {
if (!block.classList.contains('hide-author')) {
const imgElement = block.querySelector('img');
block.appendChild(authorElement);
}
}
Why this is correct:
- The
block parameter is the specific block being decorated
- Each block operates independently
- Multiple blocks on the same page work correctly
- Each block can have different configurations
Real-world bug example:
const bioElement = document.querySelector('.bio');
if (!bioElement.classList.contains('hide-author')) {
}
The fix:
if (!block.classList.contains('hide-author')) {
}
✅ WHEN Global Selectors Are Appropriate
Global selectors are INTENTIONAL and necessary for document-level operations.
Some blocks legitimately need to operate at the document level, not just within their own scope. These are typically structural blocks that affect page-wide behavior.
Document-level blocks include:
- Header/Navigation - Controls body scroll, global keyboard events, responsive layout
- Index/Table of Contents - Scans all page headings to build navigation
- Showcaser/Code Display - Collects all code snippets from the entire page
Example: Index Block (Document-Level)
export default function decorate(block) {
const headers = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
headers.forEach((header, index) => {
header.id = `header-${index}`;
});
const nav = block.querySelector('.index-content');
}
Example: Header Block (Document-Level)
export default function decorate(block) {
const mobileMedia = window.matchMedia('(min-width: 900px)');
function toggleMenu(open) {
document.body.style.overflowY = open ? 'hidden' : '';
}
window.addEventListener('keydown', (e) => {
if (e.code === 'Escape') {
toggleMenu(false);
}
});
}
When to use document-level selectors:
- ✅ Querying page metadata:
document.querySelector('meta[name="author"]')
- ✅ Controlling document body:
document.body.style.overflowY
- ✅ Global event listeners:
window.addEventListener('keydown', ...)
- ✅ Responsive queries:
window.matchMedia('(min-width: 900px)')
- ✅ Page-wide element collection:
document.querySelectorAll('h1, h2, h3, h4, h5, h6')
- ✅ Document structure access:
document.querySelector('header')
Defensive documentation pattern:
Always add a comment explaining intentional global selector usage:
const elements = document.querySelector[All](...);
For meta tags specifically:
const author = document.querySelector('meta[name="author"]');
Rule of Thumb
Inside decorate(block) function:
- ✅
block.querySelector() - ALWAYS correct for block-scoped queries
- ✅
block.classList - ALWAYS correct for block-scoped classes
- ✅
block.appendChild() - ALWAYS correct for block-scoped DOM manipulation
- ❌
document.querySelector('.your-block') - NEVER correct (use block parameter)
- ✅
document.querySelector('meta[name="author"]') - OK for document-level metadata
- ✅
document.querySelectorAll('h1, h2, h3, h4, h5, h6') - OK for document-level queries
- ✅
window.matchMedia() - OK for responsive behavior
- ✅
document.body - OK for document-level control
Key distinction: Are you querying/modifying the block itself (use block parameter) or the document/page (global selectors are intentional)?
❌ Don't Forget to Clear the Block
export default function decorate(block) {
const container = document.createElement('div');
block.appendChild(container);
}
export default function decorate(block) {
const data = extractContent(block);
block.textContent = '';
block.appendChild(container);
}
❌ Don't Use innerHTML for User Content
export default function decorate(block) {
const userInput = block.textContent;
block.innerHTML = `<div>${userInput}</div>`;
}
export default function decorate(block) {
const userInput = block.textContent;
const div = document.createElement('div');
div.textContent = userInput;
block.textContent = '';
block.appendChild(div);
}
❌ Don't Forget Error Handling
export default async function decorate(block) {
const response = await fetch('/api/data');
const data = await response.json();
renderData(block, data);
}
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>';
}
}
❌ CRITICAL: Don't Forget to Update ALL Block Documentation
This is a common mistake that causes incomplete documentation after architectural changes.
When making architectural changes to a block (refactoring, adding major features, changing patterns), you must update ALL documentation files, not just some of them.
Required documentation files for EVERY block:
-
blocks/{blockname}/README.md - Primary user-facing documentation
- Overview and features
- Usage examples
- Configuration options
- Architecture sections (if applicable)
-
blocks/{blockname}/block-architecture.md - Technical documentation
- Architecture patterns
- Module descriptions
- Code organization
- Version history
-
CHANGELOG.md - Project-wide changelog
- Document all changes with details
- Include architectural benefits
- List affected files
-
CLAUDE.md - Project guide for AI assistants
- Critical patterns section
- Reference to block features
- Links to documentation
-
README.md (project root) - Project overview
- Update block descriptions
- Add new features to lists
-
docs/for-ai/index.md - AI documentation index
- Add entries for new documentation
- Update existing references
Real-world mistake:
After implementing a unified overlay architecture refactor for ipynb-viewer:
- ✅ Updated: CHANGELOG.md, CLAUDE.md, project README.md, docs/for-ai/index.md
- ✅ Created: overlay/README.md, summary document, progress tracking
- ✅ Updated: block-architecture.md
- ❌ FORGOT: blocks/ipynb-viewer/README.md (the main block README!)
Impact:
- Users see outdated documentation
- New architecture not discoverable
- Inconsistent documentation across files
- Confusion about which version is current
Checklist for architectural changes:
blocks/{blockname}/README.md
blocks/{blockname}/block-architecture.md
CHANGELOG.md
CLAUDE.md
README.md
docs/for-ai/index.md
Before committing, ask yourself:
- Did I update the main block README.md?
- Did I update block-architecture.md?
- Did I add a CHANGELOG.md entry?
- Did I update CLAUDE.md with critical patterns?
- Did I update the project README.md?
- Did I update docs/for-ai/index.md?
Why this matters:
- Documentation is the first thing developers read
- Incomplete docs waste time (users don't know about new features)
- Inconsistent docs cause confusion (which version is correct?)
- Main README.md is often viewed first - must be current
Pattern to follow:
Related production bug: After implementing unified overlay refactor (8 modules, complete architecture change), the main blocks/ipynb-viewer/README.md was not updated, leaving users with no knowledge of the new architecture until a code review caught the omission.
When Struggling to Find Answers
If you're having trouble understanding a block's implementation or finding specific technical details:
-
Check for block-specific architecture documentation:
- Look for
blocks/{blockname}/block-architecture.md in the block folder
- This file contains detailed technical architecture specific to that block
- Example:
blocks/ipynb-viewer/block-architecture.md documents the ipynb-viewer's rendering pipeline, processing order, and critical implementation details
-
Why this matters:
- Block-specific architecture docs contain implementation details not found in general EDS guides
- They document processing order, critical timing issues, and block-specific patterns
- They often include solutions to previously encountered bugs and edge cases
-
When to read block-architecture.md:
- When implementing similar functionality in a new block
- When debugging issues in an existing block
- When making modifications to complex blocks
- When you need to understand the "why" behind implementation decisions
Example: The ipynb-viewer block has extensive architecture documentation covering:
- Markdown parsing pipeline and processing order
- Code block restoration timing (critical for preventing rendering bugs)
- Smart link resolution patterns
- Overlay system architecture
JavaScript Code Quality & ESLint Standards
Critical ESLint Rules to Follow
The project uses Airbnb JavaScript style guide with custom rules. Follow these patterns to avoid lint errors:
1. No Unused Variables
function processData(data, context) {
const result = data.map(item => item.value);
return result;
}
function processData(data, _context) {
const result = data.map(item => item.value);
return result;
}
function processData(data) {
const result = data.map(item => item.value);
return result;
}
Why this matters: Unused variables clutter code and may indicate bugs or incomplete implementations.
2. No Variable Shadowing
export default function decorate(block) {
const rows = Array.from(block.children);
rows.forEach((row) => {
function processBlock(block) {
return block.textContent;
}
});
}
export default function decorate(block) {
const rows = Array.from(block.children);
rows.forEach((row) => {
function processBlock(element) {
return element.textContent;
}
});
}
Why this matters: Variable shadowing makes code confusing and error-prone. It's unclear which variable you're referencing.
3. No Unary Operators (++ and --)
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
}
let count = 0;
count++;
for (let i = 0; i < items.length; i += 1) {
processItem(items[i]);
}
let count = 0;
count += 1;
items.forEach((item) => processItem(item));
Why this matters: Unary operators can be confusing and lead to subtle bugs with pre/post increment behavior.
4. No Lonely If (else with single if)
if (condition1) {
doSomething();
} else {
if (condition2) {
doSomethingElse();
}
}
if (condition1) {
doSomething();
} else if (condition2) {
doSomethingElse();
}
Why this matters: Reduces unnecessary nesting and improves readability.
5. Avoid Await in Loop (When Possible)
for (const file of files) {
await processFile(file);
}
await Promise.all(files.map(file => processFile(file)));
for (const file of files) {
await updateFile(file);
}
Why this matters: Await in loops is often unintentional and causes unnecessary slowness. However, it's acceptable when you need sequential processing (e.g., file writes to avoid race conditions).
6. ParseInt Always Needs Radix
const number = parseInt(str);
const number = parseInt(str, 10);
Why this matters: Without radix, parseInt('08') may parse as octal (base 8) instead of decimal.
7. Use Number.isNaN Instead of isNaN
if (isNaN(value)) {
}
if (Number.isNaN(value)) {
}
Why this matters: Global isNaN('hello') returns true because it coerces to number first. Number.isNaN() is strict.
8. Escape Special Characters in Regex
const pattern = /file.txt/;
const pattern = /file\.txt/;
Why this matters: Unescaped dots match any character, not literal dots.
9. Object Formatting (object-curly-newline)
const cell = { cell_type: 'code', source: ['console.log("test");'], metadata: {}, outputs: [] };
const cell = {
cell_type: 'code',
source: ['console.log("test");'],
metadata: {},
outputs: [],
};
const point = { x: 10, y: 20 };
Why this matters: Consistent formatting improves readability for complex objects.
10. Import/Require Patterns
import { helper } from '../../../../../../../scripts/scripts.js';
import { helper } from '../../scripts/scripts.js';
import { helper } from '../external/scripts.js';
Why this matters: Incorrect import paths cause runtime errors.
Quick Checklist Before Committing
✅ Run linter: npm run lint:js
✅ No unused variables - Remove or prefix with _
✅ No shadowing - Use specific variable names
✅ Use += 1 instead of ++
✅ Use else if instead of else { if }
✅ Always specify radix in parseInt()
✅ Use Number.isNaN() not isNaN()
✅ Escape special regex characters
✅ Format long objects with line breaks
When to Disable Rules
Only disable rules with clear justification:
for (const file of files) {
await writeFile(file);
}
Common Patterns That Pass Linting
items.forEach((item, index) => {
processItem(item);
});
button.addEventListener('click', (_e) => {
handleClick();
});
function createOverlay(content, _options) {
return buildOverlay(content);
}
Auto-Fix with ESLint
Some errors can be auto-fixed:
npm run lint:js -- --fix
npx eslint blocks/your-block/your-block.js --fix
Auto-fixable rules include:
- Trailing commas
- Quote style
- Indentation
- Some spacing issues
Not auto-fixable (require manual fixes):
- Unused variables
- Variable shadowing
- Unary operators
- Most logic issues
Related Documentation
Next Steps
- Read the Content Driven Development skill for workflow guidance
- Create your block structure with proper file organization
- Implement the decorate function following these patterns
- Create a test.html file to test locally
- Run tests and verify functionality
- Document your block in README.md and EXAMPLE.md
Remember: EDS blocks are simple, performant, and follow vanilla JavaScript patterns. Avoid frameworks, keep dependencies minimal, and focus on clean, maintainable code.