Edge Delivery Services · 5 of 28
What an Edge Delivery Services block is, how its authored content becomes HTML, and how to think about blocks when coming from traditional AEM development.
In Chapter 4, I followed a page from Universal Editor authoring to delivered HTML and then into the browser. We also saw that the blocks/ directory contains frontend implementations, but I deliberately left one question open: what exactly is a block?
For this chapter, I want to understand that unit of development before getting into loading functions or building a complete component. I'll use a simple Hero as a learning example and compare the block model with the traditional AEM component model I already know.
The goal is not to memorize a JavaScript API. I want to recognize what a block owns, then work through a small, reproducible Hero example: verify the block loads, inspect its content, make a minimal enhancement, and test the result.
When I first see a folder such as blocks/hero/, it is tempting to think of the block as the JavaScript file inside it. But that is only the implementation side.
A useful way to think about an EDS block is as a recognizable section of delivered page content with optional frontend enhancement and block-specific styling.
There are three related pieces:
These pieces meet in the browser, but they do not have the same responsibility. A block's JavaScript should not be mistaken for the source of all its content.
In the Universal Editor approach explored in this series, the authoring configuration also matters. It controls which components and fields an author can use. We'll examine that contract properly in Chapter 7.
Suppose I want a Hero containing an image, a heading, a short description, and a link. The exact fields and delivered structure depend on the component model and content, so this is a conceptual example, not a claim that every EDS Hero produces identical HTML.
A simplified delivered block might resemble:
<div class="hero block">
<div>
<div>
<picture>
<img src="/media/example-hero.jpg" alt="A sample landscape">
</picture>
</div>
</div>
<div>
<div>
<h1>Explore the collection</h1>
<p>Find something that suits your needs.</p>
<p><a href="/collection">View collection</a></p>
</div>
</div>
</div>
Notice what is already there. The image, heading, description, and link are content in the document. They do not need to be invented by the block JavaScript.
The JavaScript may rearrange elements, add a wrapper needed for layout, apply behavior, or leave the markup mostly unchanged. CSS can then handle the presentation.
That gives me a useful starting rule:
Inspect the delivered HTML before deciding what the block JavaScript needs to do.
We introduced this in Chapter 4. Here, it becomes part of how I define a block rather than only a debugging technique.
Consider this repository structure:
blocks/
hero/
hero.js
hero.css
cards/
cards.js
cards.css
With the conventional EDS block-loading pattern, a block identified as hero is associated with the implementation under blocks/hero/. A cards block is associated with blocks/cards/.
That means the name is more than a folder label. It is part of the connection between the delivered page and the frontend code.
If the page identifies a block as hero, but the repository uses a different block name, the expected implementation may not load. Projects can customize loading behavior, so the exact resolution should always be checked against their scripts.
For learning purposes, I would keep the names aligned and predictable. A block name should tell me what kind of content or experience it represents, not which page happens to contain it.
I find it useful to separate content, structure, behavior, and presentation.
| Concern | Typical owner | Hero example |
|---|---|---|
| Content | Authoring/content source | Heading, image, description, CTA text |
| Initial HTML | EDS content delivery | Rows, cells, headings, links, images |
| Enhancement | blocks/hero/hero.js | Adjusting markup or adding interaction |
| Block presentation | blocks/hero/hero.css | Layout, spacing, responsive behavior |
| Authoring choices | Universal Editor configuration | Available fields and allowed placement |
| Page-wide behavior | Shared page scripts and styles | Loading coordination and global typography |
This separation is important because not every change belongs in hero.js.
If the heading text is wrong, I would inspect the authored content first. If the heading exists but is in an unexpected element, I would inspect the delivered markup and authoring model. If the Hero layout breaks on a narrow screen, I would start with the CSS. If an interactive control is not responding, I would investigate the JavaScript.
A good block has a clear responsibility. It should not become the place where unrelated page-wide concerns accumulate.
This is where the comparison becomes useful for an AEM developer.
In a traditional AEM Sites implementation, I might think about a component through its resource type, dialog, HTL, Sling Model, and client libraries. The server-side rendering path produces the component's HTML, and the browser may then run client-side behavior.
With EDS and Universal Editor, the responsibilities are arranged differently. AEM manages the authored content, the EDS delivery path produces the page HTML, and the block implementation enhances that HTML in the browser.
| Traditional AEM mental model | EDS block mental model |
|---|---|
| Component resource type and rendering script | Delivered block identity and frontend implementation |
| HTL and Sling Models often shape server-rendered output | Delivered HTML is the starting input for block JavaScript |
| Client libraries can provide component styling and behavior | Block-specific CSS and JavaScript commonly live under blocks/ |
| Dialogs configure author-editable properties | Universal Editor component definitions, models, and filters configure authoring |
This is a comparison of common development patterns, not a claim that every traditional AEM component uses every technology in the left column.
The biggest shift for me is where I look first. Instead of immediately searching for the HTL that should generate a particular element, I inspect what HTML the EDS content flow actually delivered.
Not necessarily. If the delivered markup already provides the required content and semantics, CSS may be enough for the visual presentation. Some blocks need JavaScript because they must transform the initial markup or add interaction; others need very little enhancement.
I would avoid adding JavaScript simply because a block has a corresponding folder. The requirement should determine the amount of code.
For the Hero example, the browser already understands an h1, a link, and an image. A basic layout may be achievable with CSS. A more complex Hero might need DOM adjustments to achieve the intended structure, but those adjustments should have a reason.
This also helps avoid a common mistake: rebuilding valid HTML with JavaScript when it is already present in the document.
A Hero block should not be written only for one page title or one image. Different pages can use the same block implementation with different authored values.
For example, the same Hero could appear on a category landing page and an information page. The content changes, while the underlying block contract remains recognizable.
Reusability does not mean a block must support every possible layout. If two experiences have fundamentally different content structures or behaviors, forcing them into one giant block can make the implementation harder to understand.
A practical design question is: are these two sections the same type of content with a different presentation, or are they genuinely different components?
Block variants can help when the underlying content model is similar but the presentation changes. We will discuss variants with concrete implementation examples later, rather than adding their configuration details here.
Suppose I write JavaScript that assumes the first row always contains an image and the second row always contains text. That may work for my initial sample, but it can fail if the author leaves an optional field empty or the delivered markup differs from what I expected.
Before treating those positions as a stable contract, I would check the component model and inspect several valid authored states. I would also consider whether a missing optional element should be handled gracefully.
The lesson is not to avoid selectors. It is to make selectors and transformations based on observed and intended markup, rather than an imagined structure.
Chapter 8 will focus specifically on row/cell structures, selectors, and DOM transformations. For now, I want to establish why that work matters.
One practical habit from the Hero example is to query inside the block element, rather than searching the entire document for a heading or link.
export default function decorate(block) {
const title = block.querySelector('h1, h2');
const link = block.querySelector('a');
if (title) title.classList.add('hero-title');
if (link) link.classList.add('button');
}
This is only a small illustration of the block boundary, not a complete Hero implementation. A page may have several headings, links, or even multiple instances of the same block. Using block.querySelector() keeps the operation local to the instance being enhanced. We'll explore more precise row and cell selection in Chapter 8.
I also want to preserve the semantics EDS already delivers. If a CTA is an <a>, styling it like a button does not require replacing it with a <div>. A real link already has useful navigation and keyboard behavior. The same principle applies to headings and images: enhance what is there, and restructure only when the requirement calls for it.
Notice the if (link) check. An optional CTA may be absent, so the block should not throw an error just because a link was not authored. Detailed testing and failure diagnosis belong in later chapters, but this small defensive habit is worth introducing now.
Before writing a complete decorator, I would start with the smallest possible implementation in blocks/hero/hero.js:
export default function decorate(block) {
console.log('Hero block:', block);
}
Then I would open a page containing the Hero and check the browser's Console and Network panels. If the log appears with the expected element, the block was found and its JavaScript entry point executed. If it does not, I would check the delivered block class, the file path, the network response, and any console errors before debugging DOM selectors.
This does not prove that the final Hero is correct. It gives me a small working baseline before adding behavior.
Once I can inspect the delivered HTML, I can enhance only the elements the design needs. For example:
export default function decorate(block) {
const picture = block.querySelector('picture');
const title = block.querySelector('h1, h2');
const cta = block.querySelector('a');
if (picture) picture.closest('div')?.classList.add('hero-media');
if (title) title.classList.add('hero-title');
if (cta) cta.classList.add('button', 'primary');
}
This example is intentionally small. It preserves the existing picture, heading, and link rather than replacing them with generic elements. The selectors are illustrative: in a real exercise, I would inspect the actual markup and confirm that the first matching link is the intended CTA.
A matching blocks/hero/hero.css could begin with:
.hero .hero-media picture,
.hero .hero-media img {
display: block;
width: 100%;
}
.hero .hero-title {
margin: 0;
}
These rules stay under .hero so they do not accidentally change headings and images elsewhere on the site. This is a starting point for inspecting how CSS and decoration work together, not a finished responsive Hero design.
After the basic example works, I would try the same block with no CTA, a longer heading, a missing description if the authoring model permits it, and images with different aspect ratios. I would also inspect it at mobile and desktop widths.
The first failure might be a JavaScript error caused by a missing element, but it might just as easily be text overflow or an image crop that looks wrong. Required authoring fields and defensive frontend code solve different problems: validation guides authors, while the browser code should still handle unexpected content safely.
For a quick check, I follow the order of execution: is the block in the delivered HTML, did hero.js load, did the decorator run, did the selectors match, and did the CSS apply? Chapter 10 will expand this into a full troubleshooting method.
If I were learning this from a public EDS starter, I would use a small page containing one simple block and answer these questions in order:
I would compare the page with JavaScript enabled and disabled where practical, and inspect the DOM and network requests in browser DevTools. The aim is to observe the boundary between delivered content and browser enhancement, not to copy a particular starter's implementation blindly.
This exercise is intentionally small. We do not need a complete Universal Editor component implementation yet; that is the focus of Chapter 9.
A block has an identity, content, markup, and an implementation boundary. That is enough for the mental model in this chapter.
I am leaving the detailed mechanics of block discovery, loading stages, and the decoration lifecycle for Chapter 6 — EDS Block Development Model. Here, the small decorator and browser checks are enough to make the concept concrete. The authoring JSON files belong in Chapter 7 — Universal Editor Models, and detailed DOM transformations belong in Chapter 8 — Understanding Block DOM.
Keeping these topics separate should make it easier to follow the series without explaining the same code three times.
blocks/.In Chapter 6 — EDS Block Development Model, I'll move from the block's anatomy to its execution: how a page identifies blocks, how the relevant JavaScript and CSS are loaded, what decorate(block) receives, and how block enhancement fits into the page-loading lifecycle.
title: Understanding EDS Blocks description: What an Edge Delivery Services block is, how its authored content becomes HTML, and how to think about blocks when coming from traditional AEM development. date: '2026-10-13' chapter: '05' category: eds topics:
In Chapter 4, I followed a page from Universal Editor authoring to delivered HTML and then into the browser. We also saw that the blocks/ directory contains frontend implementations, but I deliberately left one question open: what exactly is a block?
For this chapter, I want to understand that unit of development before getting into loading functions or building a complete component. I'll use a simple Hero as a learning example and compare the block model with the traditional AEM component model I already know.
The goal is not to memorize a JavaScript API. I want to recognize what a block owns, then work through a small, reproducible Hero example: verify the block loads, inspect its content, make a minimal enhancement, and test the result.
When I first see a folder such as blocks/hero/, it is tempting to think of the block as the JavaScript file inside it. But that is only the implementation side.
A useful way to think about an EDS block is as a recognizable section of delivered page content with optional frontend enhancement and block-specific styling.
There are three related pieces:
These pieces meet in the browser, but they do not have the same responsibility. A block's JavaScript should not be mistaken for the source of all its content.
In the Universal Editor approach explored in this series, the authoring configuration also matters. It controls which components and fields an author can use. We'll examine that contract properly in Chapter 7.
Suppose I want a Hero containing an image, a heading, a short description, and a link. The exact fields and delivered structure depend on the component model and content, so this is a conceptual example, not a claim that every EDS Hero produces identical HTML.
A simplified delivered block might resemble:
<div class="hero block">
<div>
<div>
<picture>
<img src="/media/example-hero.jpg" alt="A sample landscape">
</picture>
</div>
</div>
<div>
<div>
<h1>Explore the collection</h1>
<p>Find something that suits your needs.</p>
<p><a href="/collection">View collection</a></p>
</div>
</div>
</div>
Notice what is already there. The image, heading, description, and link are content in the document. They do not need to be invented by the block JavaScript.
The JavaScript may rearrange elements, add a wrapper needed for layout, apply behavior, or leave the markup mostly unchanged. CSS can then handle the presentation.
That gives me a useful starting rule:
Inspect the delivered HTML before deciding what the block JavaScript needs to do.
We introduced this in Chapter 4. Here, it becomes part of how I define a block rather than only a debugging technique.
Consider this repository structure:
blocks/
hero/
hero.js
hero.css
cards/
cards.js
cards.css
With the conventional EDS block-loading pattern, a block identified as hero is associated with the implementation under blocks/hero/. A cards block is associated with blocks/cards/.
That means the name is more than a folder label. It is part of the connection between the delivered page and the frontend code.
If the page identifies a block as hero, but the repository uses a different block name, the expected implementation may not load. Projects can customize loading behavior, so the exact resolution should always be checked against their scripts.
For learning purposes, I would keep the names aligned and predictable. A block name should tell me what kind of content or experience it represents, not which page happens to contain it.
I find it useful to separate content, structure, behavior, and presentation.
| Concern | Typical owner | Hero example |
|---|---|---|
| Content | Authoring/content source | Heading, image, description, CTA text |
| Initial HTML | EDS content delivery | Rows, cells, headings, links, images |
| Enhancement | blocks/hero/hero.js | Adjusting markup or adding interaction |
| Block presentation | blocks/hero/hero.css | Layout, spacing, responsive behavior |
| Authoring choices | Universal Editor configuration | Available fields and allowed placement |
| Page-wide behavior | Shared page scripts and styles | Loading coordination and global typography |
This separation is important because not every change belongs in hero.js.
If the heading text is wrong, I would inspect the authored content first. If the heading exists but is in an unexpected element, I would inspect the delivered markup and authoring model. If the Hero layout breaks on a narrow screen, I would start with the CSS. If an interactive control is not responding, I would investigate the JavaScript.
A good block has a clear responsibility. It should not become the place where unrelated page-wide concerns accumulate.
This is where the comparison becomes useful for an AEM developer.
In a traditional AEM Sites implementation, I might think about a component through its resource type, dialog, HTL, Sling Model, and client libraries. The server-side rendering path produces the component's HTML, and the browser may then run client-side behavior.
With EDS and Universal Editor, the responsibilities are arranged differently. AEM manages the authored content, the EDS delivery path produces the page HTML, and the block implementation enhances that HTML in the browser.
| Traditional AEM mental model | EDS block mental model |
|---|---|
| Component resource type and rendering script | Delivered block identity and frontend implementation |
| HTL and Sling Models often shape server-rendered output | Delivered HTML is the starting input for block JavaScript |
| Client libraries can provide component styling and behavior | Block-specific CSS and JavaScript commonly live under blocks/ |
| Dialogs configure author-editable properties | Universal Editor component definitions, models, and filters configure authoring |
This is a comparison of common development patterns, not a claim that every traditional AEM component uses every technology in the left column.
The biggest shift for me is where I look first. Instead of immediately searching for the HTL that should generate a particular element, I inspect what HTML the EDS content flow actually delivered.
Not necessarily. If the delivered markup already provides the required content and semantics, CSS may be enough for the visual presentation. Some blocks need JavaScript because they must transform the initial markup or add interaction; others need very little enhancement.
I would avoid adding JavaScript simply because a block has a corresponding folder. The requirement should determine the amount of code.
For the Hero example, the browser already understands an h1, a link, and an image. A basic layout may be achievable with CSS. A more complex Hero might need DOM adjustments to achieve the intended structure, but those adjustments should have a reason.
This also helps avoid a common mistake: rebuilding valid HTML with JavaScript when it is already present in the document.
A Hero block should not be written only for one page title or one image. Different pages can use the same block implementation with different authored values.
For example, the same Hero could appear on a category landing page and an information page. The content changes, while the underlying block contract remains recognizable.
Reusability does not mean a block must support every possible layout. If two experiences have fundamentally different content structures or behaviors, forcing them into one giant block can make the implementation harder to understand.
A practical design question is: are these two sections the same type of content with a different presentation, or are they genuinely different components?
Block variants can help when the underlying content model is similar but the presentation changes. We will discuss variants with concrete implementation examples later, rather than adding their configuration details here.
Suppose I write JavaScript that assumes the first row always contains an image and the second row always contains text. That may work for my initial sample, but it can fail if the author leaves an optional field empty or the delivered markup differs from what I expected.
Before treating those positions as a stable contract, I would check the component model and inspect several valid authored states. I would also consider whether a missing optional element should be handled gracefully.
The lesson is not to avoid selectors. It is to make selectors and transformations based on observed and intended markup, rather than an imagined structure.
Chapter 8 will focus specifically on row/cell structures, selectors, and DOM transformations. For now, I want to establish why that work matters.
One practical habit from the Hero example is to query inside the block element, rather than searching the entire document for a heading or link.
export default function decorate(block) {
const title = block.querySelector('h1, h2');
const link = block.querySelector('a');
if (title) title.classList.add('hero-title');
if (link) link.classList.add('button');
}
This is only a small illustration of the block boundary, not a complete Hero implementation. A page may have several headings, links, or even multiple instances of the same block. Using block.querySelector() keeps the operation local to the instance being enhanced. We'll explore more precise row and cell selection in Chapter 8.
I also want to preserve the semantics EDS already delivers. If a CTA is an <a>, styling it like a button does not require replacing it with a <div>. A real link already has useful navigation and keyboard behavior. The same principle applies to headings and images: enhance what is there, and restructure only when the requirement calls for it.
Notice the if (link) check. An optional CTA may be absent, so the block should not throw an error just because a link was not authored. Detailed testing and failure diagnosis belong in later chapters, but this small defensive habit is worth introducing now.
Before writing a complete decorator, I would start with the smallest possible implementation in blocks/hero/hero.js:
export default function decorate(block) {
console.log('Hero block:', block);
}
Then I would open a page containing the Hero and check the browser's Console and Network panels. If the log appears with the expected element, the block was found and its JavaScript entry point executed. If it does not, I would check the delivered block class, the file path, the network response, and any console errors before debugging DOM selectors.
This does not prove that the final Hero is correct. It gives me a small working baseline before adding behavior.
Once I can inspect the delivered HTML, I can enhance only the elements the design needs. For example:
export default function decorate(block) {
const picture = block.querySelector('picture');
const title = block.querySelector('h1, h2');
const cta = block.querySelector('a');
if (picture) picture.closest('div')?.classList.add('hero-media');
if (title) title.classList.add('hero-title');
if (cta) cta.classList.add('button', 'primary');
}
This example is intentionally small. It preserves the existing picture, heading, and link rather than replacing them with generic elements. The selectors are illustrative: in a real exercise, I would inspect the actual markup and confirm that the first matching link is the intended CTA.
A matching blocks/hero/hero.css could begin with:
.hero .hero-media picture,
.hero .hero-media img {
display: block;
width: 100%;
}
.hero .hero-title {
margin: 0;
}
These rules stay under .hero so they do not accidentally change headings and images elsewhere on the site. This is a starting point for inspecting how CSS and decoration work together, not a finished responsive Hero design.
After the basic example works, I would try the same block with no CTA, a longer heading, a missing description if the authoring model permits it, and images with different aspect ratios. I would also inspect it at mobile and desktop widths.
The first failure might be a JavaScript error caused by a missing element, but it might just as easily be text overflow or an image crop that looks wrong. Required authoring fields and defensive frontend code solve different problems: validation guides authors, while the browser code should still handle unexpected content safely.
For a quick check, I follow the order of execution: is the block in the delivered HTML, did hero.js load, did the decorator run, did the selectors match, and did the CSS apply? Chapter 10 will expand this into a full troubleshooting method.
If I were learning this from a public EDS starter, I would use a small page containing one simple block and answer these questions in order:
I would compare the page with JavaScript enabled and disabled where practical, and inspect the DOM and network requests in browser DevTools. The aim is to observe the boundary between delivered content and browser enhancement, not to copy a particular starter's implementation blindly.
This exercise is intentionally small. We do not need a complete Universal Editor component implementation yet; that is the focus of Chapter 9.
A block has an identity, content, markup, and an implementation boundary. That is enough for the mental model in this chapter.
I am leaving the detailed mechanics of block discovery, loading stages, and the decoration lifecycle for Chapter 6 — EDS Block Development Model. Here, the small decorator and browser checks are enough to make the concept concrete. The authoring JSON files belong in Chapter 7 — Universal Editor Models, and detailed DOM transformations belong in Chapter 8 — Understanding Block DOM.
Keeping these topics separate should make it easier to follow the series without explaining the same code three times.
blocks/.In Chapter 6 — EDS Block Development Model, I'll move from the block's anatomy to its execution: how a page identifies blocks, how the relevant JavaScript and CSS are loaded, what decorate(block) receives, and how block enhancement fits into the page-loading lifecycle.
Once that is clear, we'll be ready to connect frontend blocks with Universal Editor models and build a complete learning example in the chapters that follow.
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.