N
Naveenr.dev
Chapter 05
16 min read•2026-06-30
📖 Edge Delivery Services SeriesChapter 05 · 27 chapters

EDS Blocks — The Fundamental Development Model

Understanding how EDS blocks should be designed around content structure, DOM contracts, rows and cells, optional content, repeated items, variants, configuration, asynchronous behavior, and clear component boundaries.

Content Objective

This chapter covers:

  • Why a block is a contract between authored content and frontend behavior
  • How to design around required, optional, and repeated content before writing DOM logic
  • Reading EDS rows and cells without assuming every block shares one shape
  • When to preserve delivered HTML, images, and links instead of rebuilding them
  • Using variants for controlled differences versus creating a new block
  • Separating authored content from runtime API data, and handling async UI states
  • Where accessibility, performance, and error isolation belong in block design

Introduction

Chapter 4 gave us a working Hero.

That was enough to understand the basic implementation:

  • content reaches the page
  • the block is discovered
  • its JavaScript and CSS load
  • decorate(block) receives the existing DOM
  • JavaScript enhances that DOM

A real project quickly gets more complicated.

A Cards block may contain ten cards. A CTA may be optional. An image may be missing. Rich text can contain several elements instead of one predictable paragraph. The same block may need two approved visual variants. Another block may load runtime data from an API.

At that point, knowing how to write:

javascript
export default function decorate(block) {
  // ...
}

is not enough. We need a consistent way to design blocks. For me, that starts with one rule:

A block should have a clear content contract and a clear frontend responsibility.

Everything else in this chapter follows from that.

1. Think of a Block as a Contract

A block is not just:

text
blocks/example/example.js

It sits between authored content and frontend behavior. The author provides content in an expected structure. EDS delivers that structure. The block implementation interprets it. CSS presents it.

If the implementation assumes one structure while the authoring model produces another, the block becomes fragile.

So before implementing a block, I want to know:

  • what content it accepts
  • what content is required
  • what content is optional
  • what can repeat
  • what variants are allowed
  • what behavior belongs to the block
  • what data comes from somewhere else

That is the block contract.

2. Content Structure Comes Before DOM Transformation

Suppose we need a Cards block. The requirement is:

  • image
  • title
  • description
  • CTA
  • multiple cards

Before thinking about JavaScript, I would represent the content conceptually as repeated items.

ImageTitleDescriptionCTA
Image AProduct ADescription ALearn more
Image BProduct BDescription BLearn more
Image CProduct CDescription CLearn more

Now the implementation problem is clearer. The block does not contain one large arbitrary HTML structure. It contains repeated card items with a known content shape. That distinction affects how we read the DOM.

3. Rows and Cells

EDS block content is commonly delivered as rows containing cells. A simplified block might look like:

html
<div class="cards block">
  <div>
    <div>
      <picture>...</picture>
    </div>
    <div>
      <h3>Product A</h3>
      <p>Description A</p>
      <p><a href="/a">Learn more</a></p>
    </div>
  </div>

  <div>
    <div>
      <picture>...</picture>
    </div>
    <div>
      <h3>Product B</h3>
      <p>Description B</p>
      <p><a href="/b">Learn more</a></p>
    </div>
  </div>
</div>

The outer children represent repeated rows. Each row contains cells. That gives us something useful to work with:

javascript
export default function decorate(block) {
  [...block.children].forEach((row) => {
    const cells = [...row.children];

    console.log(cells);
  });
}

Before changing the DOM, I inspect those rows and cells.

4. Don't Assume Every Block Uses the Same Shape

The Cards example has repeated rows. A Hero may not. An Accordion might represent each question and answer as a row. A comparison table may have a completely different structure. A form block may rely on structured configuration.

So I would not build one generic utility that assumes:

javascript
const [image, text] = row.children;

works for every block. Shared utilities are useful when the structure is genuinely shared. They become harmful when they hide different content contracts behind one abstraction.

5. Text Is Not Always a String

One mistake I avoid is treating authored text as though every cell contains:

text
"Some text"

A cell may contain actual HTML elements. For example:

html
<div>
  <h3>Product title</h3>
  <p>First paragraph.</p>
  <p>Second paragraph.</p>
  <ul>
    <li>Feature one</li>
    <li>Feature two</li>
  </ul>
</div>

If I do this:

javascript
const text = cell.textContent;

I flatten all of that structure into plain text. That may destroy paragraphs, links, lists, emphasis, and headings.

Sometimes plain text is exactly what I need. But it should be a deliberate decision.

6. Preserve Useful HTML

If authored rich content already has useful semantics, I prefer preserving it. Suppose a description cell contains:

html
<p>Designed for <strong>everyday use</strong>.</p>
<p><a href="/details">Read details</a></p>

I would not automatically rebuild that using:

javascript
description.innerHTML = `<p>${description.textContent}</p>`;

The content already has structure. The block should transform only what its UI requires. This reduces unnecessary DOM work and helps preserve author intent.

7. Images Are Already Structured Content

Images are another place where we should inspect before rebuilding. An authored image may already arrive as:

html
<picture>
  ...
  <img src="..." alt="..." width="..." height="...">
</picture>

If that markup already supports the delivery behavior we need, I keep it. I might move the <picture> into another wrapper for layout:

javascript
const picture = row.querySelector('picture');

if (picture) {
  picture.closest('div')?.classList.add('card-image');
}

But I don't replace it with:

javascript
const img = document.createElement('img');

unless the requirement actually calls for that. The existing image markup may already contain useful responsive and delivery behavior.

The same rule applies to links. If authored content gives us:

html
<a href="/products/item-a">Learn more</a>

we already have navigation semantics. The block may decorate it:

javascript
link.classList.add('button', 'secondary');

but it should not turn it into a clickable <div>. This matters for keyboard navigation, browser behavior, accessibility, SEO, and maintainability. EDS does not change normal web semantics.

9. Optional Content Is Part of the Contract

Suppose our card supports:

  • image — optional
  • title — required
  • description — optional
  • CTA — optional

The implementation should reflect that.

Unsafe:

javascript
const link = row.querySelector('a');
link.classList.add('button');

Safer:

javascript
const link = row.querySelector('a');

if (link) {
  link.classList.add('button');
}

But defensive JavaScript is only half of the design. The authoring model should also express what is required. If a Card without a title is invalid, prevent that state at authoring level where possible. The frontend should not be responsible for inventing missing business content.

10. Missing Content Should Not Mean Broken JavaScript

A block can degrade gracefully. Suppose a Card has no image. Instead of throwing an exception, the block can simply render as a text-only card. If there is no CTA, the card can still display its title and description.

Whether those states are allowed depends on the content contract. The key is that the JavaScript should not fail unexpectedly because a field that was designed as optional is absent.

11. Repeated Content

Blocks such as Cards, Accordion, Tabs, Logo List, and Related Links often contain repeated items. For these blocks, I usually start from:

javascript
[...block.children]

because the direct children often represent the repeated rows. For example:

javascript
export default function decorate(block) {
  [...block.children].forEach((row) => {
    row.classList.add('card');
  });
}

Then I inspect each row independently. That keeps one bad assumption from affecting the entire block.

12. Don't Hardcode the Number of Items

I would avoid logic such as:

javascript
const first = block.children[0];
const second = block.children[1];
const third = block.children[2];

unless the component genuinely requires exactly three items. If the author can add multiple Cards, the implementation should work with the collection:

javascript
const cards = [...block.children];

cards.forEach((card) => {
  // decorate each card
});

The content model should define limits where limits are needed. JavaScript should not accidentally create them.

13. Rich Content Changes DOM Assumptions

Suppose a Card description originally contains one paragraph:

html
<p>Description</p>

A developer may write:

javascript
const description = cell.children[1];

Later, the author adds:

html
<p>Description</p>
<ul>
  <li>Feature A</li>
  <li>Feature B</li>
</ul>

Now positional assumptions may no longer mean what we thought. This is why rich text fields need special care. Before relying on child indexes, understand whether the content contract guarantees that position. If not, use a more stable way to identify what you need.

14. DOM Indexes Are Not Automatically Wrong

I also don't treat:

javascript
row.children[0]

as inherently bad. If the content model explicitly guarantees cell 1 = image and cell 2 = content, then those indexes represent a real contract.

The problem is not using indexes. The problem is using indexes without knowing whether the structure is guaranteed. That distinction matters.

15. Variants

Sooner or later, someone will ask:

Can the same block have another style?

For example:

text
Cards
Cards (featured)
Cards (compact)

A variant can be useful when the content structure and responsibility remain the same but the presentation changes in a controlled way. The block may receive an additional class such as:

html
<div class="cards featured block">

Then CSS can handle the variation:

css
.cards.featured {
  ...
}

and JavaScript can respond only if behavior genuinely differs:

javascript
if (block.classList.contains('featured')) {
  // variant-specific enhancement
}

16. A Variant Should Not Hide a Different Component

Variants become a problem when one block starts representing unrelated experiences. Suppose cards has:

text
cards
cards (featured)
cards (carousel)
cards (comparison)
cards (product-grid)
cards (timeline)

At that point, I would question whether these are really variants of one component. If the content model, behavior, accessibility, and layout are fundamentally different, separate blocks may be clearer.

I use variants for controlled differences. I don't use them to avoid creating a component with a different responsibility.

17. Content vs Configuration

Another common requirement is block configuration. For example, an author may need to choose:

text
Theme: light | dark

or:

text
Alignment: left | center

Those values affect presentation. They are different from business content such as heading, description, image, and CTA. That distinction is useful when designing the model.

Content answers: what are we saying? Configuration answers: how should this approved component behave or appear? I try not to mix the two unnecessarily.

18. Do Not Turn the Content Model Into CSS Controls

A model can technically expose many presentation options. That does not mean it should. For example, I would avoid author fields such as:

text
Padding top
Padding bottom
Font size
Heading color
Background hex value
Border radius
Image width
Image height

for a normal enterprise block. Now the author is effectively configuring CSS. That makes design consistency, responsive behavior, accessibility, and redesigns harder.

Instead, expose approved design choices such as:

text
Theme: Default | Dark

and let the implementation own the actual CSS values.

19. Block Metadata and Configuration

Some blocks need small pieces of configuration rather than visible content. For example:

text
Layout: 3-column
Theme: dark

The important question is whether that configuration belongs to the block, the page, the site, or the application. If a setting only affects one Cards block, block-level configuration may make sense. If it controls the whole page, putting it inside one block probably does not. Configuration should live at the scope where it applies.

20. Synchronous Blocks Are the Simplest Case

Our Hero is mostly synchronous. The browser receives the content. decorate(block) works with the DOM. The block is ready.

Many content blocks should stay this simple — Hero, Quote, Callout, Text/Image, static Cards. If a block does not need runtime data, I would not add asynchronous behavior to it. Every network dependency adds another failure mode.

21. Async Blocks

Some blocks genuinely need runtime data — store availability, stock status, current account information, search results, dynamic recommendations. Then decorate() may need asynchronous work:

javascript
export default async function decorate(block) {
  const response = await fetch('/api/example');
  const data = await response.json();

  // update block
}

Once we do this, the block contract is no longer only about authored content. It also has a runtime data contract. That changes the design.

22. Async Blocks Need UI States

An API-backed block should not assume every request succeeds instantly. At minimum, I think about loading, success, empty, and error states. For example:

javascript
block.classList.add('is-loading');

try {
  const response = await fetch('/api/example');

  if (!response.ok) {
    throw new Error(`Request failed: ${response.status}`);
  }

  const data = await response.json();

  block.classList.remove('is-loading');

  if (!data.items?.length) {
    block.classList.add('is-empty');
    return;
  }

  // render/update success state
} catch (error) {
  block.classList.remove('is-loading');
  block.classList.add('has-error');

  console.error('Block request failed', error);
}

The exact implementation will depend on the block. The important part is acknowledging that network behavior introduces states.

23. Keep Authored Content and Runtime Data Separate

Suppose we build a Product Card. The author may control the promotional heading, description, campaign image, and CTA label. The commerce system may own price, availability, SKU, and inventory.

I would not copy price into AEM just because the Product Card needs to display it. That creates two sources of truth. Instead, the block can combine authored marketing content with runtime commerce data while keeping ownership clear. We will cover this architecture properly in the API integration chapter.

24. Don't Put Secrets in a Block

If an API requires a client_secret, a private API token, or service credentials, those values do not belong in:

text
blocks/product/product.js

Browser JavaScript is public. Anything shipped to the browser can be inspected. The block should only call APIs it is allowed to call from the browser, or use an appropriate server-side/edge integration layer. We will go deeper into this when we cover API integration and security.

25. Blocks Should Not Know Too Much About Each Other

Suppose a Product Cards block needs to change the Hero. I would be careful with code such as:

javascript
document.querySelector('.hero')
  .classList.add('has-product-cards');

Now Product Cards knows about Hero. That creates coupling between two separate blocks. Sometimes cross-component coordination is required, but it should be deliberate. If every block directly manipulates other blocks, the page becomes difficult to maintain. Prefer clear boundaries and shared page-level coordination when cross-block behavior is genuinely needed.

26. Shared Utilities

As more blocks are built, some code will repeat — formatting dates, parsing configuration, API request helpers, analytics helpers, DOM utilities. That is when shared utilities can help. For example:

javascript
import { formatDate } from '../../scripts/utils.js';

But I don't extract code only because two lines look similar. A shared utility becomes another dependency. It should represent behavior that is actually shared and stable.

27. Avoid the Generic Block Framework

One architecture I would avoid is building a large internal framework before the project needs one. For example:

javascript
class BaseBlock {
  parseRows() {}
  parseConfig() {}
  createImage() {}
  createButton() {}
  render() {}
  track() {}
  fetchData() {}
}

and then forcing every block to extend it. That may feel organized initially. But now every block depends on a custom abstraction that developers must understand before they can change a simple component. EDS blocks are intentionally lightweight. I would keep that advantage until repeated real requirements justify abstraction.

28. Block Error Isolation

One block should not prevent the rest of the page from working. This becomes especially important for dynamic blocks. Suppose Product Availability fails. The Hero, Navigation, Cards, and Footer should still work.

That means block implementations should avoid uncaught errors, global state corruption, modifying unrelated DOM, and blocking page initialization unnecessarily. A failure should remain as local to the block as possible.

29. Accessibility Is Part of Block Design

Accessibility should not be added after the block is visually finished. For interactive blocks, I think about it while designing the behavior.

For an Accordion: what is the interactive element, can it be reached by keyboard, how is expanded state communicated, and what happens to focus?

For Tabs: what are the tab semantics, how does keyboard navigation work, and how are panels associated?

For Cards: are links understandable, are headings structured correctly, and are images meaningful or decorative?

The exact requirements differ by component. But accessibility belongs to the block contract.

30. Performance Is Also a Block Responsibility

EDS gives us a strong delivery model, but an individual block can still be expensive. A block can hurt performance by importing a large library, loading unnecessary JavaScript, loading oversized images, making several API requests, initializing below-the-fold behavior immediately, loading third-party scripts, or causing layout shifts.

When reviewing a block, I ask:

What does this block make the browser download and execute?

That question is just as important as whether the block works.

31. Testing a Block

For each block, I want to test more than the ideal authoring state. A practical test set includes:

Content — all fields populated, optional fields missing, long text, short text, multiple repeated items, minimum items, maximum expected items.

Layout — mobile, tablet, desktop.

Behavior — keyboard interaction, links, dynamic states, repeated interaction.

Failure — missing image, API failure, empty API response, unexpected content where reasonable.

Performance — unnecessary requests, large dependencies, image loading, layout shifts.

Not every block needs every test. The test cases should match its contract.

32. Debug the Contract Before the Code

Suppose a Cards block breaks when the author adds a fourth card. The immediate reaction may be: there is a JavaScript bug. Maybe.

But first I check the contract. Was the block supposed to support multiple cards? Did the model allow four? Did CSS assume exactly three columns? Did JavaScript access only the first three rows? Was the fourth item structurally different?

A surprising amount of component debugging is really contract debugging. The code may be doing exactly what it was written to do against an assumption nobody documented.

33. When to Create a New Block

I usually consider a separate block when the requirement has a different content structure, semantic purpose, interaction model, accessibility model, data source, or lifecycle.

For example, these may all visually contain cards: Article Cards, Product Availability, Comparison Table, Carousel. That does not automatically mean they should all be variants of:

text
cards

Visual similarity is not enough. The responsibility of the component matters more.

34. When to Use a Variant

A variant is more appropriate when the block keeps the same fundamental contract. For example:

text
cards
cards (featured)

may still use the same card content, the same links, the same semantics, and the same basic behavior, with a controlled presentation difference. That is a much cleaner variant relationship.

35. A Block Design Decision Table

Before implementing a new block, I find this kind of check useful:

QuestionWhy it matters
What content does it own?Defines the content contract
What is required?Prevents invalid states
What is optional?Drives defensive handling
What repeats?Defines row/item handling
Is rich text allowed?Changes DOM assumptions
Does it need variants?Controls approved presentation
Does it need runtime data?Introduces async behavior
Who owns runtime data?Prevents duplicated sources of truth
Does it need interaction?Drives JS and accessibility
Can CSS solve the layout?Avoids unnecessary JS
Does it depend on other blocks?Reveals coupling
What can fail?Defines error handling
What does it load?Reveals performance cost

This is enough for most block discussions. I don't need a large architecture document for every component.

36. Visualizing the Block Contract

This chapter is really about the boundaries around a block. The block sits in the center. On the input side it receives authored content, block configuration, and runtime data. Inside, it works with the delivered DOM, decorate(block), block CSS, and interaction. On the output side it produces semantic UI, responsive layout, and user interaction. Four constraints sit under all of it: accessibility, performance, error handling, and the content contract.

The EDS block contract: authored content, block configuration, and runtime data flowing into the block (delivered DOM, decorate(block), block CSS, interaction) and out to semantic UI, responsive layout, and user interaction, bounded by accessibility, performance, error handling, and the content contract
The EDS block contract: authored content, block configuration, and runtime data flowing into the block (delivered DOM, decorate(block), block CSS, interaction) and out to semantic UI, responsive layout, and user interaction, bounded by accessibility, performance, error handling, and the content contract

37. Developer Perspective

Once several blocks exist, consistency matters more than clever JavaScript. I want another developer to open a block and quickly answer:

  • what content does this block expect?
  • which parts are optional?
  • what does decorate() change?
  • where are the styles?
  • does it call an API?
  • what happens if that API fails?
  • does it have variants?
  • does it depend on anything else?

If those answers require reading hundreds of lines of code, the block probably needs simplification.

38. Architect Perspective

At architecture level, blocks become the reusable vocabulary of the website. If that vocabulary is poorly designed, the authoring system becomes difficult too. Too few blocks can produce giant components with dozens of variants. Too many blocks can produce duplicated implementations with tiny differences.

The goal is not maximum reuse. The goal is clear responsibilities. For each block, I want a stable purpose, a stable content contract, controlled variants, predictable behavior, clear ownership, and limited dependencies. That makes both authoring and development easier to govern.

39. What I Learned After Moving Beyond the Hero

The Hero made block development look simple because its content was predictable. Repeated content, optional fields, rich text, variants, and runtime data expose the real design decisions.

The most useful lesson is that decorate(block) should not become the place where every uncertainty is solved. A good block starts with a good contract. Once the contract is clear, DOM parsing becomes simpler, CSS becomes more predictable, optional states are easier to handle, variants stay controlled, tests become obvious, and debugging becomes faster.

The JavaScript is usually the last part of that design, not the first.

Key Takeaways

  • Treat every block as a contract between authored content and frontend behavior.
  • Define required, optional, repeated, and runtime data before writing transformation logic.
  • Rows and cells are useful structural boundaries, but not every block has the same shape.
  • Rich content should not automatically be flattened into text.
  • Preserve useful semantic HTML whenever possible.
  • Reuse delivered image and link markup instead of rebuilding it without a reason.
  • Optional content should not crash the block.
  • Required content should be enforced through the authoring contract where appropriate.
  • Repeated blocks should not accidentally hardcode a fixed item count.
  • DOM indexes are acceptable when the content contract actually guarantees the structure.
  • Use variants for controlled differences, not fundamentally different components.
  • Do not expose raw CSS controls to authors when approved design variants can solve the requirement.
  • Keep block configuration at the scope where it belongs.
  • Async blocks need loading, success, empty, and failure handling.
  • Keep authored content separate from runtime business data.
  • Never place private credentials in browser block code.
  • Avoid unnecessary dependencies between blocks.
  • Extract shared utilities only when behavior is genuinely shared.
  • Do not build a large internal block framework before real requirements justify it.
  • Accessibility, error handling, and performance are part of block design.
  • Create a new block when the responsibility or contract changes substantially.
  • A clear block contract usually produces simpler JavaScript.

Next Steps

We now understand how to design a block from the frontend side. The next step is to connect that block properly to the authoring experience.

In the next chapter, Universal Editor, Models, and Component Definitions, we look at component definitions, component models, and component filters; field types; required and optional fields; image fields; rich text; CTA modeling; variants; component placement; naming alignment; model changes and frontend compatibility; and authoring UX versus frontend UX.

That is where the content contract we discussed in this chapter becomes an actual Universal Editor configuration.

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.