Edge Delivery Services · 10 of 29
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.
This chapter covers:
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.
Before jumping to solutions, build the habit of identifying which layer the issue is in. Walk down the layers in order and stop at the first "no" — that is your issue layer.
fstab misconfiguration. If yes, continue.decorate() run? If no, there is a JS error in decorate or the wrong export type. If yes, continue.decorate(). If yes, continue.z-index, opacity, and visibility.Working through this list will locate the issue layer within 2-3 DevTools actions.
Symptom: The block element exists in the DOM but has no decoration — it still looks like a raw key-value table.
How to diagnose:
hero, cards, etc.)blocks/{name}/{name}.jsPossible findings and solutions:
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.
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.
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 DOMblock.querySelectorAll is not a function — you are calling querySelectorAll on the wrong objectUnexpected token — syntax error in your JS fileSymptom: 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:
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: 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: 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
}
}
}
}
}
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.
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:
.hero.block element)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.
You wrote the correct compound CSS selector, but the variant class is never added to the block.
How to diagnose:
class attribute: class="hero block hero-tall" — the variant should appear hereCommon 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.
/* 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.
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.
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:
position, width, height ruleswidth and height — if they are 0, this is the issueFix: 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.
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:
width and heightFix: 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);
}
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:
overflow: hidden on the parentoverflow: hidden is clipping itFix decision:
overflow: hidden ruleSymptom: 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:
.aem.page.localhost:3000 to see the update.How to force it:
localhost:3000If you still see old content after previewing, do a hard refresh: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows).
Symptom: The block element exists with the correct classes, the CSS file is present, but no styles appear.
How to diagnose:
blocks/{name}/{name}.cssPossible findings:
Same as JS file naming issue — the CSS file must be named identically to the folder.
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.
Computed styles show something unexpected. Check for:
display: none or visibility: hidden from a parentcolor being the same as background-color (text invisible)height: 0 or width: 0 making the element collapseopacity: 0z-index causing the element to be behind another elementSymptom: Everything works perfectly on localhost:3000 but the deployed preview (.aem.page) looks broken.
Root causes:
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.
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.
// 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.
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
{ "==": [{ "var": "field" }, "value"] } formatkey-value: true and filter cannot coexist — restructure the block if you need both.hero.hero-tall) — descendant selectors never matchposition: absolute without explicit dimensions = 0×0 — causes images to collapseThe next chapter, EDS Capabilities and Limits, steps back from individual bugs to the bigger question: what EDS can and cannot do on its own — DA and Universal Editor authoring, experimentation, the Query API, multi-locale and multi-brand setups — so you judge the tool by what it was built for instead of what it was not.
(The full production CSS guide — token systems, cascade order, custom properties as theming, responsive breakpoints, and reuse without a build tool — is Chapter 16 — CSS Architecture.)
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.