Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    samircaus

    aem-block-development

    samircaus/aem-block-development
    Coding

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Technical implementation guide for AEM Edge Delivery blocks - DOM manipulation, decoration patterns, and JavaScript best practices...

    SKILL.md

    AEM Block Development

    Overview

    This skill covers the technical implementation of AEM blocks AFTER you have:

    1. Checked for existing blocks (aem-block-reusability skill)
    2. Designed the content structure (aem-content-modeling skill)

    Key principle: Content structure drives implementation. Don't design structure around your code convenience.

    Block Fundamentals

    Block Structure

    Every block consists of:

    • JavaScript file: blocks/blockname/blockname.js - Decoration logic
    • CSS file: blocks/blockname/blockname.css - Scoped styles
    • Content: HTML structure delivered by AEM backend

    The Decorate Function

    /**
     * loads and decorates the block
     * @param {Element} block The block element
     */
    export default async function decorate(block) {
      // 1. Load dependencies (if needed)
      // 2. Extract configuration from block (if applicable)
      // 3. Transform DOM
      // 4. Add event listeners
      // 5. Set loaded status (if needed)
    }
    

    This function is called automatically by the AEM page loading system when the block is encountered.

    Inspecting Block HTML

    ALWAYS inspect the HTML before writing code. Don't make assumptions.

    Methods to Inspect

    1. Local curl (fastest)

    # View full page HTML
    curl http://localhost:3000/path/to/test-page
    
    # View page as plain.html (stripped to content only)
    curl http://localhost:3000/path/to/test-page.plain.html
    

    2. Browser console

    // Log the block element
    console.log(block);
    
    // Log the HTML structure
    console.log(block.outerHTML);
    
    // Log all rows
    console.log([...block.children]);
    

    3. Preview environment

    # View from preview environment
    curl https://branch--repo--owner.aem.page/path/to/test-page.plain.html
    

    Understanding Block HTML Structure

    AEM converts authored tables into HTML with this structure:

    Authored content:

    | Block Name |
    |------------|
    | value1 | value2 | value3 |
    | value4 | value5 | value6 |
    

    Delivered HTML:

    <div class="block-name">
      <div>
        <div>value1</div>
        <div>value2</div>
        <div>value3</div>
      </div>
      <div>
        <div>value4</div>
        <div>value5</div>
        <div>value6</div>
      </div>
    </div>
    

    Key observations:

    • Block name becomes class="block-name" (lowercase, spaces to hyphens)
    • Each table row becomes a <div> containing child <div>s
    • Each table cell becomes a <div>
    • Images become <picture> elements with <img> inside
    • Links become <a> elements
    • Text content is preserved with semantic HTML (paragraphs, headings, lists, etc.)

    DOM Manipulation Patterns

    Reading Block Structure

    Get all rows:

    const rows = [...block.children];
    

    Get cells from a row:

    const cells = [...row.children];
    

    Get specific row/cell:

    const firstRow = block.children[0];
    const firstCell = firstRow.children[0];
    

    Get text content:

    const text = cell.textContent.trim();
    

    Get image:

    const picture = cell.querySelector('picture');
    const img = cell.querySelector('img');
    

    Get link:

    const link = cell.querySelector('a');
    const href = link?.href;
    const linkText = link?.textContent;
    

    Common Transformation Patterns

    Pattern 1: Simple row iteration

    export default async function decorate(block) {
      const rows = [...block.children];
    
      rows.forEach((row) => {
        const cells = [...row.children];
        // Transform each row
        row.classList.add('item');
      });
    }
    

    Pattern 2: Extract configuration from first row

    export default async function decorate(block) {
      const config = {};
      const configRow = block.children[0];
      const cells = [...configRow.children];
    
      // First cell = key, second = value (config block pattern)
      config[cells[0].textContent.trim()] = cells[1].textContent.trim();
    
      configRow.remove(); // Remove config row after extraction
    
      // Process remaining rows
      [...block.children].forEach((row) => {
        // Use config to influence decoration
      });
    }
    

    Pattern 3: Rebuild DOM structure

    export default async function decorate(block) {
      const rows = [...block.children];
    
      // Create new structure
      const container = document.createElement('div');
      container.classList.add('container');
    
      rows.forEach((row) => {
        const cells = [...row.children];
    
        // Build new element from cell data
        const item = document.createElement('div');
        item.classList.add('item');
    
        const img = cells[0].querySelector('img');
        if (img) item.append(img);
    
        const text = cells[1].textContent.trim();
        const p = document.createElement('p');
        p.textContent = text;
        item.append(p);
    
        container.append(item);
      });
    
      // Replace block contents
      block.textContent = '';
      block.append(container);
    }
    

    Pattern 4: Handle optional fields gracefully

    export default async function decorate(block) {
      const rows = [...block.children];
    
      rows.forEach((row) => {
        const cells = [...row.children];
    
        // Required field
        const title = cells[0]?.textContent.trim();
        if (!title) return; // Skip malformed rows
    
        // Optional fields - check existence
        const img = cells[1]?.querySelector('img');
        const link = cells[2]?.querySelector('a');
    
        // Build element, include optional parts only if present
        const item = document.createElement('div');
        if (img) item.append(img.cloneNode(true));
    
        const heading = document.createElement('h3');
        heading.textContent = title;
    
        if (link) {
          link.textContent = '';
          link.append(heading);
          item.append(link);
        } else {
          item.append(heading);
        }
    
        row.replaceWith(item);
      });
    }
    

    Working with Images

    Get optimized image:

    const picture = cell.querySelector('picture');
    const img = cell.querySelector('img');
    
    // Images from AEM are already optimized with:
    // - Multiple source formats (webp, etc.)
    // - Responsive srcset
    // - Lazy loading attributes
    
    // Just move/clone the picture element:
    newElement.append(picture);
    

    Modify image attributes:

    const img = cell.querySelector('img');
    if (img) {
      img.alt = 'Descriptive alt text';
      img.loading = 'lazy'; // Usually already set
    }
    

    Extract image URL:

    const img = cell.querySelector('img');
    const imageSrc = img?.src;
    

    Working with Links

    Extract link data:

    const link = cell.querySelector('a');
    if (link) {
      const href = link.href;
      const text = link.textContent.trim();
      const title = link.title;
    }
    

    Wrap content in link:

    const link = cell.querySelector('a');
    if (link) {
      const href = link.href;
      link.textContent = ''; // Clear existing content
      link.append(newContent); // Add new content
    }
    

    Create new link:

    const a = document.createElement('a');
    a.href = url;
    a.textContent = 'Click here';
    a.title = 'Descriptive title';
    

    Working with Rich Content

    Get formatted content (preserving HTML):

    // Cell may contain paragraphs, headings, lists, etc.
    const content = cell.innerHTML;
    
    // Or clone the content
    const contentClone = cell.cloneNode(true);
    newElement.append(contentClone);
    

    Get plain text (stripping HTML):

    const plainText = cell.textContent.trim();
    

    Block Variants

    Variants modify block behavior without changing the JavaScript file.

    Authored:

    | Block Name (Variant1, Variant2) |
    |---------------------------------|
    | content |
    

    Delivered HTML:

    <div class="block-name variant1 variant2">
      <div>content</div>
    </div>
    

    Detect variants in JavaScript:

    export default async function decorate(block) {
      const isVariant1 = block.classList.contains('variant1');
      const isVariant2 = block.classList.contains('variant2');
    
      if (isVariant1) {
        // Apply variant1-specific behavior
      }
    
      if (isVariant2) {
        // Apply variant2-specific behavior
      }
    }
    

    CSS for variants:

    /* Base block styles */
    .block-name {
      /* Default styles */
    }
    
    /* Variant-specific styles */
    .block-name.variant1 {
      /* Override for variant1 */
    }
    
    .block-name.variant2 {
      /* Override for variant2 */
    }
    

    CSS Scoping Rules

    CRITICAL: All CSS selectors MUST be scoped to the block class.

    This prevents style conflicts between blocks and ensures maintainability.

    ❌ Wrong - Unscoped Selectors

    .search-input {
      padding: 1rem;
    }
    
    .item-label {
      font-weight: bold;
    }
    

    Problem: These selectors will match ANY element with these classes across the entire page, causing conflicts.

    ✅ Correct - Fully Scoped Selectors

    .block-name .search-input {
      padding: 1rem;
    }
    
    .block-name .item-label {
      font-weight: bold;
    }
    
    /* For elements you create dynamically */
    .block-name input {
      padding: 1rem;
    }
    

    Why this matters:

    • Prevents conflicts with other blocks
    • Makes CSS predictable and maintainable
    • Follows CSS specificity best practices
    • Essential for composable architecture

    Rule: Every selector must start with .block-name (your block's class).

    Async Operations and Loading

    Loading External Dependencies

    Load library (if absolutely necessary):

    export default async function decorate(block) {
      // Only if truly needed - avoid dependencies when possible
      const lib = await import('./lib.js');
    
      // Use library
      lib.doSomething(block);
    }
    

    Fetching Data

    Fetch JSON data:

    export default async function decorate(block) {
      const endpoint = block.querySelector('a')?.href;
    
      if (endpoint) {
        try {
          const response = await fetch(endpoint);
          const data = await response.json();
    
          // Use data to populate block
          data.items.forEach((item) => {
            // Create elements from data
          });
        } catch (error) {
          console.error('Failed to load data:', error);
          // Show error state or fallback
        }
      }
    }
    

    Loading State

    Add loading indicator:

    export default async function decorate(block) {
      // Add loading class
      block.classList.add('loading');
    
      // Perform async operation
      await doSomethingAsync();
    
      // Remove loading class
      block.classList.remove('loading');
      block.classList.add('loaded');
    }
    

    CSS for loading state:

    .block-name.loading {
      opacity: 0.5;
      pointer-events: none;
    }
    
    .block-name.loading::after {
      content: 'Loading...';
      display: block;
    }
    

    Event Handlers

    Adding Event Listeners

    Click events:

    export default async function decorate(block) {
      const button = block.querySelector('.button');
    
      button.addEventListener('click', (e) => {
        e.preventDefault();
        // Handle click
      });
    }
    

    Event delegation (for dynamic content):

    export default async function decorate(block) {
      block.addEventListener('click', (e) => {
        const button = e.target.closest('.button');
        if (button) {
          e.preventDefault();
          // Handle button click
        }
      });
    }
    

    Keyboard accessibility:

    button.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        // Handle activation
      }
    });
    

    Responsive Design

    Mobile-First Approach

    CSS breakpoints (from AGENTS.md):

    /* Mobile styles (default) */
    .block-name {
      display: block;
    }
    
    /* Tablet (600px+) */
    @media (min-width: 600px) {
      .block-name {
        display: flex;
      }
    }
    
    /* Desktop (900px+) */
    @media (min-width: 900px) {
      .block-name {
        max-width: 1200px;
      }
    }
    
    /* Large desktop (1200px+) */
    @media (min-width: 1200px) {
      .block-name {
        /* Large screen styles */
      }
    }
    

    JavaScript for Responsive Behavior

    Detect viewport size (if necessary):

    export default async function decorate(block) {
      const isMobile = window.matchMedia('(max-width: 599px)').matches;
      const isTablet = window.matchMedia('(min-width: 600px) and (max-width: 899px)').matches;
      const isDesktop = window.matchMedia('(min-width: 900px)').matches;
    
      // Apply behavior based on viewport
      if (isMobile) {
        // Mobile-specific behavior
      }
    }
    

    Listen for viewport changes:

    const mediaQuery = window.matchMedia('(min-width: 900px)');
    
    function handleViewportChange(e) {
      if (e.matches) {
        // Desktop behavior
      } else {
        // Mobile/tablet behavior
      }
    }
    
    mediaQuery.addEventListener('change', handleViewportChange);
    handleViewportChange(mediaQuery); // Initial check
    

    Accessibility

    ARIA Labels and Roles

    Add ARIA attributes:

    const button = document.createElement('button');
    button.setAttribute('aria-label', 'Close dialog');
    button.setAttribute('aria-expanded', 'false');
    

    For custom interactive elements:

    const customButton = document.createElement('div');
    customButton.setAttribute('role', 'button');
    customButton.setAttribute('tabindex', '0');
    customButton.addEventListener('click', handleClick);
    customButton.addEventListener('keydown', (e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        handleClick();
      }
    });
    

    Focus Management

    Manage focus for interactive elements:

    const dialog = document.createElement('div');
    dialog.setAttribute('role', 'dialog');
    dialog.setAttribute('aria-modal', 'true');
    
    // When opening dialog
    dialog.style.display = 'block';
    dialog.querySelector('button').focus();
    
    // Trap focus within dialog
    dialog.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        // Trap focus logic
      }
    });
    

    Performance Considerations

    Minimize DOM Operations

    Bad - multiple reflows:

    // ❌ WRONG - causes multiple reflows
    rows.forEach((row) => {
      row.style.width = '100px';
      row.style.height = '50px';
      row.classList.add('item');
    });
    

    Good - batch operations:

    // ✅ CORRECT - minimize reflows
    const fragment = document.createDocumentFragment();
    
    rows.forEach((row) => {
      row.style.cssText = 'width: 100px; height: 50px';
      row.classList.add('item');
      fragment.append(row);
    });
    
    block.append(fragment);
    

    Lazy Loading

    Lazy load images (usually already handled by AEM):

    const img = cell.querySelector('img');
    if (img && !img.loading) {
      img.loading = 'lazy';
    }
    

    Lazy load functionality:

    export default async function decorate(block) {
      // Set up structure immediately
      block.classList.add('ready');
    
      // Lazy load expensive operations
      const observer = new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            // Load expensive functionality when visible
            loadExpensiveFeature(block);
            observer.disconnect();
          }
        });
      });
    
      observer.observe(block);
    }
    

    Avoid Dependencies

    Prefer vanilla JavaScript over libraries:

    // ❌ WRONG - unnecessary dependency
    import $ from 'jquery';
    $('.block-name').addClass('active');
    
    // ✅ CORRECT - vanilla JS
    document.querySelectorAll('.block-name').forEach((el) => {
      el.classList.add('active');
    });
    

    Use native browser APIs:

    // Native fetch instead of axios
    fetch(url).then(r => r.json()).then(data => { /* ... */ });
    
    // Native DOM manipulation instead of jQuery
    element.classList.add('class');
    element.addEventListener('click', handler);
    

    Common Patterns and Examples

    Example 1: Simple Card Grid

    Content structure:

    | Cards |
    |-------|
    | image1.jpg | Title 1 | Description 1 |
    | image2.jpg | Title 2 | Description 2 |
    

    JavaScript:

    export default async function decorate(block) {
      const rows = [...block.children];
    
      rows.forEach((row) => {
        const cells = [...row.children];
        const picture = cells[0].querySelector('picture');
        const title = cells[1].textContent.trim();
        const description = cells[2].textContent.trim();
    
        // Build card structure
        row.classList.add('card');
        row.innerHTML = '';
    
        if (picture) {
          const imageWrapper = document.createElement('div');
          imageWrapper.classList.add('card-image');
          imageWrapper.append(picture);
          row.append(imageWrapper);
        }
    
        const content = document.createElement('div');
        content.classList.add('card-content');
    
        const h3 = document.createElement('h3');
        h3.textContent = title;
        content.append(h3);
    
        const p = document.createElement('p');
        p.textContent = description;
        content.append(p);
    
        row.append(content);
      });
    }
    

    CSS:

    .cards {
      display: grid;
      gap: 1rem;
      grid-template-columns: 1fr;
    }
    
    @media (min-width: 600px) {
      .cards {
        grid-template-columns: repeat(2, 1fr);
      }
    }
    
    @media (min-width: 900px) {
      .cards {
        grid-template-columns: repeat(3, 1fr);
      }
    }
    
    .cards .card {
      border: 1px solid #ddd;
      border-radius: 8px;
      overflow: hidden;
    }
    
    .cards .card-image img {
      width: 100%;
      height: auto;
      display: block;
    }
    
    .cards .card-content {
      padding: 1rem;
    }
    

    Example 2: Tabbed Content

    Content structure:

    | Tabs |
    |------|
    | Tab 1 | Content for tab 1 |
    | Tab 2 | Content for tab 2 |
    

    JavaScript:

    export default async function decorate(block) {
      const rows = [...block.children];
    
      // Create tab buttons container
      const tabButtons = document.createElement('div');
      tabButtons.classList.add('tabs-buttons');
    
      // Create content container
      const tabContents = document.createElement('div');
      tabContents.classList.add('tabs-contents');
    
      rows.forEach((row, index) => {
        const cells = [...row.children];
        const tabLabel = cells[0].textContent.trim();
        const tabContent = cells[1];
    
        // Create button
        const button = document.createElement('button');
        button.classList.add('tab-button');
        button.textContent = tabLabel;
        button.setAttribute('aria-selected', index === 0 ? 'true' : 'false');
        button.setAttribute('role', 'tab');
    
        // Create content panel
        const panel = document.createElement('div');
        panel.classList.add('tab-panel');
        panel.setAttribute('role', 'tabpanel');
        panel.append(tabContent);
    
        if (index === 0) {
          button.classList.add('active');
          panel.classList.add('active');
        } else {
          panel.style.display = 'none';
        }
    
        // Click handler
        button.addEventListener('click', () => {
          // Deactivate all
          tabButtons.querySelectorAll('.tab-button').forEach((btn) => {
            btn.classList.remove('active');
            btn.setAttribute('aria-selected', 'false');
          });
          tabContents.querySelectorAll('.tab-panel').forEach((p) => {
            p.classList.remove('active');
            p.style.display = 'none';
          });
    
          // Activate clicked
          button.classList.add('active');
          button.setAttribute('aria-selected', 'true');
          panel.classList.add('active');
          panel.style.display = 'block';
        });
    
        tabButtons.append(button);
        tabContents.append(panel);
      });
    
      // Replace block content
      block.textContent = '';
      block.append(tabButtons);
      block.append(tabContents);
    }
    

    Example 3: Hero with Background Image

    Content structure:

    | Hero |
    |------|
    | Headline text |
    | Subheadline text |
    | cta-button.jpg |
    | background.jpg |
    

    JavaScript:

    export default async function decorate(block) {
      const rows = [...block.children];
    
      const headline = rows[0]?.textContent.trim();
      const subheadline = rows[1]?.textContent.trim();
      const ctaImage = rows[2]?.querySelector('picture');
      const backgroundImage = rows[3]?.querySelector('img');
    
      // Set background
      if (backgroundImage) {
        block.style.backgroundImage = `url(${backgroundImage.src})`;
        block.style.backgroundSize = 'cover';
        block.style.backgroundPosition = 'center';
      }
    
      // Build content
      block.innerHTML = '';
    
      const content = document.createElement('div');
      content.classList.add('hero-content');
    
      if (headline) {
        const h1 = document.createElement('h1');
        h1.textContent = headline;
        content.append(h1);
      }
    
      if (subheadline) {
        const p = document.createElement('p');
        p.textContent = subheadline;
        content.append(p);
      }
    
      if (ctaImage) {
        content.append(ctaImage);
      }
    
      block.append(content);
    }
    

    Testing Your Block

    Local Testing

    1. Create test content in drafts/agent/blockname-test.html (see aem-development-workflow skill)
    2. Start server with HTML folder: aem up --html-folder=./drafts/agent
    3. Test at http://localhost:3000/drafts/agent/blockname-test
    4. Use browser DevTools to inspect and debug

    Console Debugging

    export default async function decorate(block) {
      console.log('Block element:', block);
      console.log('Block HTML:', block.outerHTML);
      console.log('Rows:', [...block.children]);
    
      // Your decoration code
    }
    

    Responsive Testing

    • Use browser DevTools device emulation
    • Test at 599px (mobile), 600px (tablet), 900px (desktop), 1200px+ (large)
    • Test with Puppeteer/Playwright if available

    Common Mistakes to Avoid

    ❌ Assuming Field Presence

    Wrong:

    const title = row.children[1].textContent; // Crashes if cell doesn't exist
    

    Correct:

    const title = row.children[1]?.textContent.trim() || 'Default';
    

    ❌ Modifying Original Elements Destructively

    Wrong:

    const img = cell.querySelector('img');
    cell.innerHTML = ''; // Destroys the image
    // Now img is detached from DOM
    

    Correct:

    const img = cell.querySelector('img');
    const imgClone = img.cloneNode(true); // Or move it before clearing
    cell.innerHTML = '';
    cell.append(imgClone);
    

    ❌ Not Handling Empty Blocks

    Wrong:

    export default async function decorate(block) {
      const firstRow = block.children[0]; // Crashes if no rows
      const title = firstRow.children[0].textContent;
    }
    

    Correct:

    export default async function decorate(block) {
      if (!block.children.length) return; // Handle empty block
    
      const firstRow = block.children[0];
      if (!firstRow?.children.length) return; // Handle empty row
    
      const title = firstRow.children[0]?.textContent.trim();
    }
    

    ❌ Assuming Specific Content Format

    Wrong:

    // Assumes cell contains plain text
    const name = cell.textContent;
    

    Correct:

    // Check what's actually in the cell
    const link = cell.querySelector('a');
    const name = link ? link.textContent.trim() : cell.textContent.trim();
    

    ❌ Breaking Accessibility

    Wrong:

    const div = document.createElement('div');
    div.addEventListener('click', handleClick); // Not keyboard accessible
    

    Correct:

    const button = document.createElement('button');
    button.addEventListener('click', handleClick); // Keyboard accessible by default
    

    Quick Reference

    DOM Traversal

    [...block.children]              // All rows
    [...row.children]                // All cells in row
    cell.querySelector('img')        // Find image
    cell.querySelector('a')          // Find link
    cell.querySelector('picture')    // Find picture element
    cell.textContent.trim()          // Get text content
    

    DOM Creation

    document.createElement('div')
    element.classList.add('class')
    element.setAttribute('attr', 'value')
    element.append(child)
    element.textContent = 'text'
    element.innerHTML = '<p>html</p>'
    

    Common Operations

    element.remove()                 // Remove from DOM
    element.replaceWith(newElement)  // Replace element
    element.cloneNode(true)          // Deep clone
    block.textContent = ''           // Clear contents
    

    The Bottom Line

    Block development workflow:

    1. Inspect the HTML - Use curl/console.log, don't assume
    2. Handle variations gracefully - Authors will omit/add fields
    3. Transform the DOM - Extract data, rebuild structure
    4. Add interactivity - Event listeners, ARIA attributes
    5. Style responsively - Mobile-first CSS with breakpoints
    6. Test thoroughly - Local HTML, responsive, accessibility

    Remember:

    • Content structure comes FIRST (designed before coding)
    • Authors may deviate from structure - code defensively
    • Vanilla JS only - avoid dependencies
    • Mobile-first, responsive design
    • Accessibility is not optional

    Always refer to:

    • aem-content-modeling skill for structure design
    • aem-development-workflow skill for testing and PR process
    • aem-block-reusability skill before starting implementation
    Recommended Servers
    Vercel Grep
    Vercel Grep
    Repository
    samircaus/elsie
    Files