N
Naveenr.dev
Chapter 05
25 min read2026-06-30

EDS Debugging Guide

Common EDS issues and the debugging steps that identify them. Covers blocks not loading, image problems, Universal Editor failures, CSS issues, and content updates that do not appear as expected.

Content Objective

This chapter covers:

  • A systematic diagnostic framework for EDS issues
  • The exact cause of the five most common EDS bugs
  • How to diagnose Universal Editor crashes from the browser console
  • Why CSS variant rules silently fail and how to detect it
  • The root cause of "image not showing" in complex block variants
  • Why content updates don't appear and how to force a refresh
  • What DevTools panels to open for each type of issue

This chapter is different from the others. It is not conceptual. It is a reference guide for real bugs.

I am documenting these issues because I encountered them building a real production block. Not from a tutorial. Not from a synthetic example. These are the actual root causes from actual debugging sessions.

If you are building EDS blocks professionally, you will encounter these issues. Some of them will cost you hours if you don't know the cause. If you know the cause, they take minutes.

The Diagnostic Framework

Before jumping to solutions, build the habit of identifying which layer the issue is in.

Is the block HTML present in the DOM?
  └─ NO: Content/authoring issue OR fstab misconfiguration
  └─ YES ↓

Did the block JS load?
  └─ NO: File naming issue OR export issue
  └─ YES ↓

Did decorate() run?
  └─ NO: JS error in decorate OR wrong export type
  └─ YES ↓

Is the DOM structure correct after decoration?
  └─ NO: Logic bug in decorate()
  └─ YES ↓

Is the CSS rule matching?
  └─ NO: Selector specificity OR wrong compound vs descendant selector
  └─ YES ↓

Is the layout correct?
  └─ NO: Parent container constraints (overflow, position context)
  └─ YES: Check z-index, opacity, visibility

Working through this tree will locate the issue layer within 2-3 DevTools actions.

Issue 1: Block Not Loading at All

Symptom: The block element exists in the DOM but has no decoration — it still looks like a raw key-value table.

How to diagnose:

  1. Open DevTools → Network tab
  2. Filter by JS
  3. Search for your block name (hero, cards, etc.)
  4. Look for a request to blocks/{name}/{name}.js

Possible findings and solutions:

Finding: 404 on the JS file

The file could not be found. Check these in order:

blocks/
└── hero/
    ├── hero.js      ← Must match folder name exactly
    └── hero.css     ← Must match folder name exactly

If the folder is hero-banner but the file is hero.js, you get a 404. Rename the file to hero-banner.js.

Finding: JS file loaded but no decoration

Open the file in DevTools (click the 404 response or the loaded response), and check:

// This will silently fail — named export
export function decorate(block) { }

// This is required — default export
export default function decorate(block) { }

If you see a named export, that is your issue. The EDS loader calls module.default(block) — if module.default is undefined, the call fails silently.

Finding: JS file loaded, default export exists, but error in console

Click the error in the console to find the line. Common errors at this stage:

  • Cannot read properties of null — you queried an element that doesn't exist in the DOM
  • block.querySelectorAll is not a function — you are calling querySelectorAll on the wrong object
  • Unexpected token — syntax error in your JS file

Issue 2: Universal Editor Properties Panel Crashes

Symptom: After clicking a block or component in UE, the properties panel shows "Something went wrong" instead of the block's fields.

This is the most disruptive UE issue because it blocks all authoring on that page.

How to diagnose:

  1. Open browser DevTools → Console tab
  2. Look for one of these error patterns:

Error Pattern A: JSON Parse Error

SyntaxError: Expected ',' or '}' after property value in JSON at position 1234

Root cause: A JSON syntax error in one of your model files.

How to find it:

The console message usually includes a URL pointing to the JSON file. Open that file and look for:

// Common JSON errors:

// 1. Trailing comma after last item
{
  "fields": [
    { "name": "title" },  ← This trailing comma is illegal in JSON
  ]
}

// 2. Single quotes instead of double quotes
{
  'name': 'title'   ← JSON requires double quotes
}

// 3. Missing comma between items
{
  "fields": [
    { "name": "title" }
    { "name": "size" }   ← Missing comma before this line
  ]
}

// 4. Unclosed bracket
{
  "fields": [
    { "name": "title" }
  ]
  // ← Missing closing }

Prevention: Use JSON.parse() in the browser console to validate your JSON before pushing, or use the JSONLint VS Code extension.

Error Pattern B: JSONLogic Error

Error: Unrecognized operation: size

Root cause: A condition field in your block model uses shorthand syntax instead of JSONLogic.

// CAUSES CRASH — shorthand is not JSONLogic
"condition": { "size": "tall" }

// CORRECT — JSONLogic format
"condition": { "==": [{ "var": "size" }, "tall"] }

The error message tells you the field name that was used as an operator. In the example above, size was interpreted as an operator name instead of a variable reference.

All conditions in UE model files must use JSONLogic syntax.

Error Pattern C: Missing Required Field

Error: Required field 'id' is missing from component definition

Root cause: A block definition in component-definition.json is missing a required property. Every block definition needs at minimum: title, id, and the plugins.xwalk.page structure.

// Minimum valid block definition
{
  "title": "My Block",   ← required
  "id": "my-block",      ← required, must be unique
  "plugins": {
    "xwalk": {
      "page": {
        "resourceType": "core/franklin/components/block/v1/block",
        "template": {
          "name": "my-block",   ← must match block folder name
          "model": "my-block"   ← must match an entry in component-models.json
        }
      }
    }
  }
}

Error Pattern D: key-value + filter Conflict

Symptom: UE crashes immediately when the page loads (not just on click). No specific error message visible.

Root cause: A block template has both "key-value": true and a filter reference.

// CAUSES UE CRASH — do not use both together
{
  "template": {
    "name": "hero-banner",
    "model": "hero-banner",
    "key-value": true,
    "filter": "hero-banner"   ← Cannot coexist with key-value: true
  }
}

Fix: Remove either key-value: true or the filter. For blocks that need both named fields AND dynamic child items, you must restructure the block design — for example, keep the parent block as key-value: true without a filter, and create a separate child block (without key-value: true) that uses a filter.

Issue 3: CSS Variant Rule Has No Effect

Symptom: You added a CSS rule for a variant (e.g., .hero.hero-tall) but the page looks exactly the same. Inspecting in DevTools shows the rule is not matching.

This issue is one of the most common in EDS CSS development and one of the least obvious.

How to diagnose:

  1. Open DevTools → Elements tab
  2. Click on the block root element (the .hero.block element)
  3. In the Styles panel, search for your variant rule
  4. If the rule appears but is crossed out (specificity override), your selector matches but something more specific overrides it
  5. If the rule does not appear at all, your selector does not match

Root Cause A: Descendant Selector vs Compound Selector

This is the most common cause.

/* This means: an element with class hero-tall INSIDE an element with class hero */
/* The variant class is on the hero root, not inside it — this never matches */
.hero .hero-tall { }

/* This means: an element that has BOTH class hero AND class hero-tall */
/* This is correct — the variant is added to the block root */
.hero.hero-tall { }

How to tell which you have: Count the spaces. .hero .hero-tall (space between) is descendant. .hero.hero-tall (no space) is compound.

Why this happens: In traditional AEM development with BEM, you write .hero--tall or .hero__content--tall. In EDS, variants are added to the block root element itself, not to a child element. The CSS pattern is fundamentally different.

Check every variant rule you write. If the variant class is added by your JS to block.classList.add('hero-tall'), the CSS selector must be .hero.hero-tall, not .hero .hero-tall.

Root Cause B: Variant Class Not Being Added

You wrote the correct compound CSS selector, but the variant class is never added to the block.

How to diagnose:

  1. Open DevTools → Elements tab
  2. Find the block root div
  3. Check its class attribute: class="hero block hero-tall" — the variant should appear here
  4. If the variant class is missing, the JS is not adding it

Common JS mistake:

export default function decorate(block) {
  // Size comes from authored content
  const size = props.size?.textContent.trim();

  // BUG: if size is 'Tall' (capital T) from UE, this adds 'hero-Tall'
  block.classList.add(`hero-${size}`);
}

The fix:

const size = props.size?.textContent.trim().toLowerCase() || 'tall';
block.classList.add(`hero-${size}`);

Always normalize authored values to lowercase before using them in class names.

Root Cause C: Media Query Mismatch

/* You wrote the rule for desktop */
@media (min-width: 900px) {
  .hero.hero-tall .hero-section { height: 640px; }
}

/* But you are testing on a 768px viewport */

In DevTools → Elements → Styles, media query rules that don't match the current viewport have a grey "not applicable" indicator. Toggle the device toolbar or resize the window.

Issue 4: Image Not Showing in a Variant Block

Symptom: A variant of your block (e.g., "image-in-circle") should display an image, but the image is invisible — no img tag error, no 404, just nothing visible.

This is the most complex debugging scenario in this chapter because there are multiple independent root causes that each produce the same symptom.

Root Cause A: Base CSS Position Rule Collapsing the Container

Scenario: Your block has a base rule for the standard image layout:

.hero .hero-media img,
.hero .hero-media picture {
  position: absolute;
  top: 0; left: 0;
  width: 100%;
  height: 100%;
}

Your circle variant reuses the class hero-media:

// BUG: this element gets the base .hero-media rules
mediaCol.className = 'hero-media hero-media-circle';

What happens: The circle container needs a concrete width and height set externally. But because the position: absolute rule from the base styles applies (the container has class hero-media), the image takes 100% of its nearest positioned ancestor — which is the circle container itself. If the circle container has no explicit size (its size is determined by content), it collapses to 0×0. The image is 100% of 0 = 0px. The image is invisible.

How to diagnose:

  1. DevTools → Elements → click the image element
  2. In Styles, look for conflicting position, width, height rules
  3. In Computed tab, look at the actual rendered width and height — if they are 0, this is the issue

Fix: Remove the base class from the variant container:

// CORRECT: no base class, variant class only
mediaCol.className = 'hero-media-circle';

Now the base .hero .hero-media rules do not apply to this container.

Root Cause B: Position Context Chain Collapse

Scenario: Your variant element uses position: absolute to place itself:

.hero.hero-image-in-circle .hero-media-circle {
  position: absolute;
  left: 100%;
}

position: absolute removes the element from normal flow. If the element has no explicit width or height, its size is 0×0. Then left: 100% of 0 = 0px. The element is at position 0,0 with no size, and the image inside it inherits the same 0×0 constraint.

How to diagnose:

  1. DevTools → Elements → click the container
  2. In Computed tab, look at width and height
  3. If both are 0, this is the issue

Fix: Keep the element in normal flow and use margin or padding to position it:

/* Instead of absolute positioning */
.hero.hero-image-in-circle .hero-media-circle {
  flex: 0 0 50%;          /* In-flow, sized by flex */
  position: relative;     /* Normal flow */
}

.hero.hero-image-in-circle .hero-media-circle img {
  /* Use margin to offset the image within its container */
  margin-left: calc(100% - 360px);
}

Root Cause C: Overflow Hidden on Parent Clipping the Element

Scenario: The image is visible in the Computed tab (has non-zero dimensions), but is not visible in the browser.

/* Parent has overflow hidden to clip the overflowing circle */
.hero-section {
  overflow: hidden;
}

/* Child is positioned correctly but partially outside the bounds */
.hero-media-circle {
  /* circle center at right edge of column */
  /* right half extends beyond column width */
}

In this case, the image IS rendering, but it is being clipped by overflow: hidden on an ancestor element.

How to diagnose:

  1. DevTools → Elements → Styles → temporarily disable overflow: hidden on the parent
  2. If the image appears, overflow: hidden is clipping it

Fix decision:

  • If the overflow was intentional (to clip the circle as a design choice), the image is working correctly — the "invisible" portion is by design
  • If the overflow is not intentional, move or remove the overflow: hidden rule

Issue 5: Content Not Updating After Author Edits

Symptom: An author edited the page content in Universal Editor or Document Authoring, but localhost:3000 still shows the old content. Refreshing does not help.

Root cause: The local development server proxies HTML from the preview URL (.aem.page), not directly from AEM. Content must be pushed to .aem.page before the local server can pick it up.

Workflow:

Author edits in UE or DA
      ↓
Content saved to AEM author instance
      ↓ (NOT automatically pushed to preview)
Author clicks Preview in Sidekick
      ↓
Content pushed to .aem.page
      ↓
Local server can now fetch the updated content
      ↓
Developer refreshes localhost:3000 to see update

How to force it:

  1. In Universal Editor or Sidekick, click Preview
  2. Wait for the preview URL to reflect the change (open it directly to confirm)
  3. Refresh localhost:3000

If you still see old content after previewing, do a hard refresh: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows).

Issue 6: Block Renders But CSS Is Not Applied

Symptom: The block element exists with the correct classes, the CSS file is present, but no styles appear.

How to diagnose:

  1. DevTools → Network tab → filter by CSS
  2. Look for blocks/{name}/{name}.css

Possible findings:

CSS file 404

Same as JS file naming issue — the CSS file must be named identically to the folder.

CSS file loaded but rules are crossed out in DevTools

Higher specificity rules are overriding yours. Use the Styles panel's specificity display to identify which rule is winning and increase your selector's specificity, or restructure the rule.

CSS file loaded, rules not crossed out, but not visible

Computed styles show something unexpected. Check for:

  • display: none or visibility: hidden from a parent
  • color being the same as background-color (text invisible)
  • height: 0 or width: 0 making the element collapse
  • opacity: 0
  • z-index causing the element to be behind another element

Issue 7: Block Works Locally But Not on Preview

Symptom: Everything works perfectly on localhost:3000 but the deployed preview (.aem.page) looks broken.

Root causes:

Cache serving old code

The preview URL caches JS and CSS. After pushing to GitHub, the edge network should update, but there can be a delay.

Add ?nocache=1 to the preview URL or use the Sidekick to explicitly preview the page.

ESLint/Stylelint errors preventing deployment

The aem-code-sync GitHub App runs linters on push. If lint errors are found, the files may not be deployed.

Check the GitHub Actions log (if enabled) or add linting to your local workflow:

npm run lint         # Check both JS and CSS
npm run lint:js      # Check JS only
npm run lint:css     # Check CSS only

Fix all lint errors before pushing.

Relative paths breaking on preview

// Works locally (proxied through localhost:3000)
const response = await fetch('/blocks/hero/hero-data.json');

// Works on both local and preview
const { default: data } = await import('/blocks/hero/hero-data.json', { assert: { type: 'json' } });

Always use absolute paths from root (/blocks/...) rather than relative paths (./hero-data.json). Relative paths can resolve differently depending on the current URL.

The Debug Checklist

When something is wrong in EDS, run through this checklist in order:

□ Is the block folder name in kebab-case lowercase?
□ Does the JS file name exactly match the folder name?
□ Does the CSS file name exactly match the folder name?
□ Is the JS export a default export?
□ Are all JSON files valid (no trailing commas, double quotes)?
□ Do all conditions use JSONLogic format?
□ Is key-value: true used together with a filter? (not allowed)
□ Are variant classes added to block.classList (not a child element)?
□ Are CSS variant selectors compound (.hero.hero-tall) not descendant (.hero .hero-tall)?
□ Are authored values normalized to lowercase before use as class names?
□ Has the author clicked Preview after editing content?
□ For image issues: check Computed tab for width/height = 0
□ For image issues: check if a base CSS rule is being inherited by the variant container

Key Takeaways

  • Systematic diagnosis: identify the layer (content / JS loading / JS execution / DOM structure / CSS matching / layout) before jumping to solutions
  • Default export is mandatory — named exports cause silent block failure
  • JSON syntax errors crash the entire UE properties panel — not just the broken block
  • JSONLogic conditions must use { "==": [{ "var": "field" }, "value"] } format
  • key-value: true and filter cannot coexist — restructure the block if you need both
  • Variant CSS requires compound selectors (.hero.hero-tall) — descendant selectors never match
  • Base CSS classes on variant containers cause inheritance conflicts — use separate class names
  • position: absolute without explicit dimensions = 0×0 — causes images to collapse
  • Preview must be clicked before local dev server shows content changes
  • Lint errors on push prevent code deployment — always lint before pushing

Next Steps

In Chapter 10 — CSS Architecture, we cover the full guide to writing production CSS for EDS blocks.

We cover:

  • How to design your block's CSS token system
  • The cascade order for base styles, variant overrides, and responsive rules
  • Using CSS custom properties as a theming system
  • The responsive breakpoints and mobile-first approach
  • Writing CSS that survives when blocks are placed in different section contexts
  • Reusing CSS across multiple blocks without a build tool

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.