Edge Delivery Services · 6 of 28
Designing EDS blocks around content contracts, rows and cells, optional fields, repeated content, variants, runtime data, and maintainable boundaries.
In Chapter 5, I used a Hero to understand what an EDS block receives and how decorate(block) can enhance the HTML already delivered to the browser. That was a useful starting point, but a single Hero does not expose many of the design questions that appear once a block is reused.
A Cards block may have two items or twelve. An image or CTA might be optional. A description might contain rich text rather than one paragraph. Authors might need a compact layout, while another block might need data from an external API.
The important question becomes: what content shape does this block promise to support, and what responsibility does the frontend take for that shape?
This chapter uses a hypothetical Cards block and independently written examples to explain the development decisions. It builds on the technical lessons from my EDS exploration without reproducing project-specific code or configurations.
The folder is straightforward:
blocks/
└── cards/
├── cards.js
└── cards.css
The more important part is the agreement between authoring and rendering. The authoring model determines what can be entered. EDS delivers a corresponding HTML structure. The decorator interprets that structure, and CSS controls the presentation.
Before writing a new block, I want answers to seven questions:
Those answers form the content contract. The contract should be checked against the actual delivered HTML rather than assumed from the editor configuration alone.
Imagine a Cards block with an image, title, description, and CTA per item. A content example might be:
| Image | Title | Description | CTA |
|---|---|---|---|
| Image A | Product A | Description A | Learn more |
| Image B | Product B | Description B | Learn more |
| Image C | Product C | Description C | Learn more |
This table is a planning aid, not a guarantee of the HTML that EDS will produce. The exact structure depends on the authoring model and content. Inspect the delivered markup before implementing the parser.
A simplified delivered block could look like this:
<div class="cards block">
<div>
<div><picture>...</picture></div>
<div>
<h3>Product A</h3>
<p>Description A</p>
<p><a href="/products/a">Learn more</a></p>
</div>
</div>
<div>
<div><picture>...</picture></div>
<div>
<h3>Product B</h3>
<p>Description B</p>
<p><a href="/products/b">Learn more</a></p>
</div>
</div>
</div>
Here the direct children of .cards represent rows, and each row contains cells. That gives the decorator a predictable starting point:
export default function decorate(block) {
const rows = [...block.children];
rows.forEach((row) => {
const cells = [...row.children];
console.log('Cards row:', cells);
});
}
The first run is deliberately observational. It verifies the delivered shape before transformation. A Hero, Accordion, Tabs, or comparison block might use a different shape. I would not create a universal row parser that assumes every block contains [image, text] unless that structure is genuinely shared and guaranteed.
Consider:
const [imageCell, contentCell] = row.children;
This is reasonable if the model guarantees two cells in that order. It is fragile if authors can add, remove, or reorder cells. Indexes are not automatically bad; undocumented positional assumptions are.
Rich text introduces another risk. A description can contain several paragraphs, a list, and links:
<div>
<p>Designed for <strong>everyday use</strong>.</p>
<ul>
<li>Feature one</li>
<li>Feature two</li>
</ul>
<p><a href="/details">Read details</a></p>
</div>
Reading cell.textContent flattens that structure into plain text. Sometimes that is the desired output, but not for a rich description. Likewise, reconstructing the content through an HTML string can discard semantics and create avoidable safety problems.
Where possible, preserve existing elements and move or style them. If the content already contains a <picture>, keep its responsive markup. If the CTA is an <a>, keep its navigation and keyboard behavior. A block can change layout without destroying meaningful HTML.
For our Cards example, suppose the contract is:
| Field | Contract | Expected behavior |
|---|---|---|
| Title | Required | The card needs a meaningful title |
| Image | Optional | Text-only layout remains usable |
| Description | Optional | No empty placeholder required |
| CTA | Optional | Card can render without a link |
A decorator should not crash because an optional CTA is absent:
export default function decorate(block) {
[...block.children].forEach((row) => {
row.classList.add('card');
const link = row.querySelector('a');
if (link) link.classList.add('button', 'secondary');
const picture = row.querySelector('picture');
if (picture) picture.closest('div')?.classList.add('card-image');
});
}
This is intentionally modest: it decorates existing HTML rather than reconstructing a complete card. A production implementation would also verify the title and any other requirements of its actual model.
Authoring validation and frontend resilience solve different problems. The authoring model should enforce required content where supported. The frontend should still handle missing or unexpected content without breaking the rest of the page. It should not invent a product title or silently turn invalid business content into valid content.
A repeated Cards block should normally work with the collection of rows rather than hardcoding three items:
const cards = [...block.children];
cards.forEach((card) => {
card.classList.add('card');
});
Avoid writing logic for children[0], children[1], and children[2] unless the contract explicitly requires exactly three entries. If a design has minimum or maximum item counts, define those limits intentionally and test them.
A useful test is to add a fourth card. If it fails, check more than JavaScript: the authoring model, delivered rows, CSS grid assumptions, and selectors may all be involved.
Suppose authors need standard, featured, and compact Cards. If each uses the same content structure and semantic purpose, variants can be a good fit:
<div class="cards featured block">...</div>
.cards.featured .card {
/* approved featured presentation */
}
JavaScript should branch on a variant only when behavior actually changes:
if (block.classList.contains('featured')) {
// Only behavior that genuinely differs for this variant
}
But consider a single cards block that gradually gains carousel, comparison, product-grid, and timeline variants. Those may require different data structures, interactions, accessibility behavior, and lifecycles. At that point, I would question whether they should be separate blocks.
Visual similarity is not sufficient reason to share a block. Reuse should follow a stable responsibility and content contract.
A title, description, image, and CTA are content. A choice such as Theme: Default | Dark or Alignment: Left | Center is presentation configuration.
I prefer exposing a small set of approved options instead of turning the authoring model into a CSS control panel with arbitrary font sizes, spacing, image dimensions, and color values. Otherwise, each page can become a separate design implementation that is difficult to maintain responsively.
Configuration should also live at the right scope. A setting for one Cards block belongs with that block; a site-wide theme should not depend on a single instance of Cards.
The Universal Editor configuration that enforces these choices is the focus of Chapter 7.
A static Hero or Cards block can often be enhanced synchronously. No network request is needed to render its authored content.
Other blocks genuinely need live data. For example, a product card may use authored promotional copy while price and availability come from a commerce service. Those fields have different owners and freshness requirements. Copying live price into authored content simply to render a card can create two sources of truth.
An asynchronous decorator might begin like this:
export default async function decorate(block) {
block.classList.add('is-loading');
try {
const response = await fetch('/api/example');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
block.classList.toggle('is-empty', !data.items?.length);
// Render or update the appropriate data-driven UI.
} catch (error) {
block.classList.add('has-error');
console.error('Cards data request failed', error);
} finally {
block.classList.remove('is-loading');
}
}
This is an illustrative pattern, not a complete integration. A real implementation must define what the user sees while loading, on success, when empty, and on failure. It must also consider accessibility, caching, SEO, latency, and whether a browser-side fetch is appropriate at all.
Never put API secrets or private service credentials in block JavaScript: code shipped to the browser is public. We will explore integration architecture in Chapter 27 and forms/API behavior in Chapter 28.
A Product Cards block should not casually reach into an unrelated Hero and modify its classes:
// Avoid as an accidental dependency between unrelated blocks.
document.querySelector('.hero')?.classList.add('has-product-cards');
Cross-block coordination is sometimes necessary, but it should be explicit and preferably handled through a suitable shared or page-level boundary.
The same principle applies to utilities. Shared date formatting, DOM helpers, or API clients can be useful when they represent genuinely stable shared behavior. I would not build a large BaseBlock framework just because two blocks share a few lines. A small, understandable decorator is one of the strengths of this development model.
Before implementing a block, I also ask:
These are not checks to postpone until a visual design is complete. The dedicated performance, accessibility, and security chapters will go deeper into them.
For a Cards block, I would test:
| Test | What it reveals |
|---|---|
| One card, then four cards | Fixed-count assumptions |
| CTA missing | Unsafe link selectors |
| Image missing | Whether text-only layout works |
| Long title and rich description | Overflow and DOM assumptions |
| Mobile, tablet, desktop | Responsive layout behavior |
| Featured variant | Variant class and CSS boundaries |
| API returns empty/error, if applicable | Runtime states and failure isolation |
| Keyboard navigation | Interaction semantics |
If the fourth card breaks, I would first ask whether the contract supports four items, then inspect delivered HTML, JavaScript, and CSS in that order. Sometimes a “JavaScript bug” is really an undocumented contract assumption.
| Question | Why I ask it |
|---|---|
| What content does it own? | Defines the content contract |
| What is required or optional? | Defines valid and fallback states |
| What repeats? | Guides row handling and item limits |
| Is rich text allowed? | Changes parsing and DOM assumptions |
| Does it need variants? | Controls approved differences |
| Does it need runtime data? | Adds network and state responsibilities |
| Who owns that runtime data? | Avoids duplicate sources of truth |
| Does it need interaction? | Changes accessibility and JavaScript needs |
| Does it depend on other blocks? | Reveals coupling |
| What can fail? | Guides graceful failure |
| What does it download? | Reveals performance cost |
I do not need a large architecture document for every block. But I do need the answers to be clear enough that someone else can maintain it.
With traditional AEM, I would often start by thinking about the dialog, Sling Model, HTL, and generated markup. With EDS, I still care about authoring constraints, but I also have to reason carefully about the HTML contract that arrives in the browser and what the decorator should—or should not—change.
The goal is not to make every block generic. It is to make each block's purpose obvious. Too few blocks can produce oversized components with dozens of variants; too many can produce duplicate implementations. Clear contracts make that tradeoff easier to manage.
In Chapter 7 — Universal Editor Models, we will turn these design questions into an authoring configuration: component definitions, models, filters, field choices, required and optional values, and placement rules.
title: EDS Blocks — The Fundamental Development Model description: Designing EDS blocks around content contracts, rows and cells, optional fields, repeated content, variants, runtime data, and maintainable boundaries. date: '2026-10-13' chapter: '06' category: eds topics:
In Chapter 5, I used a Hero to understand what an EDS block receives and how decorate(block) can enhance the HTML already delivered to the browser. That was a useful starting point, but a single Hero does not expose many of the design questions that appear once a block is reused.
A Cards block may have two items or twelve. An image or CTA might be optional. A description might contain rich text rather than one paragraph. Authors might need a compact layout, while another block might need data from an external API.
The important question becomes: what content shape does this block promise to support, and what responsibility does the frontend take for that shape?
This chapter uses a hypothetical Cards block and independently written examples to explain the development decisions. It builds on the technical lessons from my EDS exploration without reproducing project-specific code or configurations.
The folder is straightforward:
blocks/
└── cards/
├── cards.js
└── cards.css
The more important part is the agreement between authoring and rendering. The authoring model determines what can be entered. EDS delivers a corresponding HTML structure. The decorator interprets that structure, and CSS controls the presentation.
Before writing a new block, I want answers to seven questions:
Those answers form the content contract. The contract should be checked against the actual delivered HTML rather than assumed from the editor configuration alone.
Imagine a Cards block with an image, title, description, and CTA per item. A content example might be:
| Image | Title | Description | CTA |
|---|---|---|---|
| Image A | Product A | Description A | Learn more |
| Image B | Product B | Description B | Learn more |
| Image C | Product C | Description C | Learn more |
This table is a planning aid, not a guarantee of the HTML that EDS will produce. The exact structure depends on the authoring model and content. Inspect the delivered markup before implementing the parser.
A simplified delivered block could look like this:
<div class="cards block">
<div>
<div><picture>...</picture></div>
<div>
<h3>Product A</h3>
<p>Description A</p>
<p><a href="/products/a">Learn more</a></p>
</div>
</div>
<div>
<div><picture>...</picture></div>
<div>
<h3>Product B</h3>
<p>Description B</p>
<p><a href="/products/b">Learn more</a></p>
</div>
</div>
</div>
Here the direct children of .cards represent rows, and each row contains cells. That gives the decorator a predictable starting point:
export default function decorate(block) {
const rows = [...block.children];
rows.forEach((row) => {
const cells = [...row.children];
console.log('Cards row:', cells);
});
}
The first run is deliberately observational. It verifies the delivered shape before transformation. A Hero, Accordion, Tabs, or comparison block might use a different shape. I would not create a universal row parser that assumes every block contains [image, text] unless that structure is genuinely shared and guaranteed.
Consider:
const [imageCell, contentCell] = row.children;
This is reasonable if the model guarantees two cells in that order. It is fragile if authors can add, remove, or reorder cells. Indexes are not automatically bad; undocumented positional assumptions are.
Rich text introduces another risk. A description can contain several paragraphs, a list, and links:
<div>
<p>Designed for <strong>everyday use</strong>.</p>
<ul>
<li>Feature one</li>
<li>Feature two</li>
</ul>
<p><a href="/details">Read details</a></p>
</div>
Reading cell.textContent flattens that structure into plain text. Sometimes that is the desired output, but not for a rich description. Likewise, reconstructing the content through an HTML string can discard semantics and create avoidable safety problems.
Where possible, preserve existing elements and move or style them. If the content already contains a <picture>, keep its responsive markup. If the CTA is an <a>, keep its navigation and keyboard behavior. A block can change layout without destroying meaningful HTML.
For our Cards example, suppose the contract is:
| Field | Contract | Expected behavior |
|---|---|---|
| Title | Required | The card needs a meaningful title |
| Image | Optional | Text-only layout remains usable |
| Description | Optional | No empty placeholder required |
| CTA | Optional | Card can render without a link |
A decorator should not crash because an optional CTA is absent:
export default function decorate(block) {
[...block.children].forEach((row) => {
row.classList.add('card');
const link = row.querySelector('a');
if (link) link.classList.add('button', 'secondary');
const picture = row.querySelector('picture');
if (picture) picture.closest('div')?.classList.add('card-image');
});
}
This is intentionally modest: it decorates existing HTML rather than reconstructing a complete card. A production implementation would also verify the title and any other requirements of its actual model.
Authoring validation and frontend resilience solve different problems. The authoring model should enforce required content where supported. The frontend should still handle missing or unexpected content without breaking the rest of the page. It should not invent a product title or silently turn invalid business content into valid content.
A repeated Cards block should normally work with the collection of rows rather than hardcoding three items:
const cards = [...block.children];
cards.forEach((card) => {
card.classList.add('card');
});
Avoid writing logic for children[0], children[1], and children[2] unless the contract explicitly requires exactly three entries. If a design has minimum or maximum item counts, define those limits intentionally and test them.
A useful test is to add a fourth card. If it fails, check more than JavaScript: the authoring model, delivered rows, CSS grid assumptions, and selectors may all be involved.
Suppose authors need standard, featured, and compact Cards. If each uses the same content structure and semantic purpose, variants can be a good fit:
<div class="cards featured block">...</div>
.cards.featured .card {
/* approved featured presentation */
}
JavaScript should branch on a variant only when behavior actually changes:
if (block.classList.contains('featured')) {
// Only behavior that genuinely differs for this variant
}
But consider a single cards block that gradually gains carousel, comparison, product-grid, and timeline variants. Those may require different data structures, interactions, accessibility behavior, and lifecycles. At that point, I would question whether they should be separate blocks.
Visual similarity is not sufficient reason to share a block. Reuse should follow a stable responsibility and content contract.
A title, description, image, and CTA are content. A choice such as Theme: Default | Dark or Alignment: Left | Center is presentation configuration.
I prefer exposing a small set of approved options instead of turning the authoring model into a CSS control panel with arbitrary font sizes, spacing, image dimensions, and color values. Otherwise, each page can become a separate design implementation that is difficult to maintain responsively.
Configuration should also live at the right scope. A setting for one Cards block belongs with that block; a site-wide theme should not depend on a single instance of Cards.
The Universal Editor configuration that enforces these choices is the focus of Chapter 7.
A static Hero or Cards block can often be enhanced synchronously. No network request is needed to render its authored content.
Other blocks genuinely need live data. For example, a product card may use authored promotional copy while price and availability come from a commerce service. Those fields have different owners and freshness requirements. Copying live price into authored content simply to render a card can create two sources of truth.
An asynchronous decorator might begin like this:
export default async function decorate(block) {
block.classList.add('is-loading');
try {
const response = await fetch('/api/example');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
block.classList.toggle('is-empty', !data.items?.length);
// Render or update the appropriate data-driven UI.
} catch (error) {
block.classList.add('has-error');
console.error('Cards data request failed', error);
} finally {
block.classList.remove('is-loading');
}
}
This is an illustrative pattern, not a complete integration. A real implementation must define what the user sees while loading, on success, when empty, and on failure. It must also consider accessibility, caching, SEO, latency, and whether a browser-side fetch is appropriate at all.
Never put API secrets or private service credentials in block JavaScript: code shipped to the browser is public. We will explore integration architecture in Chapter 27 and forms/API behavior in Chapter 28.
A Product Cards block should not casually reach into an unrelated Hero and modify its classes:
// Avoid as an accidental dependency between unrelated blocks.
document.querySelector('.hero')?.classList.add('has-product-cards');
Cross-block coordination is sometimes necessary, but it should be explicit and preferably handled through a suitable shared or page-level boundary.
The same principle applies to utilities. Shared date formatting, DOM helpers, or API clients can be useful when they represent genuinely stable shared behavior. I would not build a large BaseBlock framework just because two blocks share a few lines. A small, understandable decorator is one of the strengths of this development model.
Before implementing a block, I also ask:
These are not checks to postpone until a visual design is complete. The dedicated performance, accessibility, and security chapters will go deeper into them.
For a Cards block, I would test:
| Test | What it reveals |
|---|---|
| One card, then four cards | Fixed-count assumptions |
| CTA missing | Unsafe link selectors |
| Image missing | Whether text-only layout works |
| Long title and rich description | Overflow and DOM assumptions |
| Mobile, tablet, desktop | Responsive layout behavior |
| Featured variant | Variant class and CSS boundaries |
| API returns empty/error, if applicable | Runtime states and failure isolation |
| Keyboard navigation | Interaction semantics |
If the fourth card breaks, I would first ask whether the contract supports four items, then inspect delivered HTML, JavaScript, and CSS in that order. Sometimes a “JavaScript bug” is really an undocumented contract assumption.
| Question | Why I ask it |
|---|---|
| What content does it own? | Defines the content contract |
| What is required or optional? | Defines valid and fallback states |
| What repeats? | Guides row handling and item limits |
| Is rich text allowed? | Changes parsing and DOM assumptions |
| Does it need variants? | Controls approved differences |
| Does it need runtime data? | Adds network and state responsibilities |
| Who owns that runtime data? | Avoids duplicate sources of truth |
| Does it need interaction? | Changes accessibility and JavaScript needs |
| Does it depend on other blocks? | Reveals coupling |
| What can fail? | Guides graceful failure |
| What does it download? | Reveals performance cost |
I do not need a large architecture document for every block. But I do need the answers to be clear enough that someone else can maintain it.
With traditional AEM, I would often start by thinking about the dialog, Sling Model, HTL, and generated markup. With EDS, I still care about authoring constraints, but I also have to reason carefully about the HTML contract that arrives in the browser and what the decorator should—or should not—change.
The goal is not to make every block generic. It is to make each block's purpose obvious. Too few blocks can produce oversized components with dozens of variants; too many can produce duplicate implementations. Clear contracts make that tradeoff easier to manage.
In Chapter 7 — Universal Editor Models, we will turn these design questions into an authoring configuration: component definitions, models, filters, field choices, required and optional values, and placement rules.
title: EDS Blocks — The Fundamental Development Model description: Designing EDS blocks around content contracts, rows and cells, optional fields, repeated content, variants, runtime data, and maintainable boundaries. date: '2026-10-13' chapter: '06' category: eds topics:
In Chapter 5, I used a Hero to understand what an EDS block receives and how decorate(block) can enhance the HTML already delivered to the browser. That was a useful starting point, but a single Hero does not expose many of the design questions that appear once a block is reused.
A Cards block may have two items or twelve. An image or CTA might be optional. A description might contain rich text rather than one paragraph. Authors might need a compact layout, while another block might need data from an external API.
The important question becomes: what content shape does this block promise to support, and what responsibility does the frontend take for that shape?
This chapter uses a hypothetical Cards block and independently written examples to explain the development decisions. It builds on the technical lessons from my EDS exploration without reproducing project-specific code or configurations.
The folder is straightforward:
blocks/
└── cards/
├── cards.js
└── cards.css
The more important part is the agreement between authoring and rendering. The authoring model determines what can be entered. EDS delivers a corresponding HTML structure. The decorator interprets that structure, and CSS controls the presentation.
Before writing a new block, I want answers to seven questions:
Those answers form the content contract. The contract should be checked against the actual delivered HTML rather than assumed from the editor configuration alone.
Imagine a Cards block with an image, title, description, and CTA per item. A content example might be:
| Image | Title | Description | CTA |
|---|---|---|---|
| Image A | Product A | Description A | Learn more |
| Image B | Product B | Description B | Learn more |
| Image C | Product C | Description C | Learn more |
This table is a planning aid, not a guarantee of the HTML that EDS will produce. The exact structure depends on the authoring model and content. Inspect the delivered markup before implementing the parser.
A simplified delivered block could look like this:
<div class="cards block">
<div>
<div><picture>...</picture></div>
<div>
<h3>Product A</h3>
<p>Description A</p>
<p><a href="/products/a">Learn more</a></p>
</div>
</div>
<div>
<div><picture>...</picture></div>
<div>
<h3>Product B</h3>
<p>Description B</p>
<p><a href="/products/b">Learn more</a></p>
</div>
</div>
</div>
Here the direct children of .cards represent rows, and each row contains cells. That gives the decorator a predictable starting point:
export default function decorate(block) {
const rows = [...block.children];
rows.forEach((row) => {
const cells = [...row.children];
console.log('Cards row:', cells);
});
}
The first run is deliberately observational. It verifies the delivered shape before transformation. A Hero, Accordion, Tabs, or comparison block might use a different shape. I would not create a universal row parser that assumes every block contains [image, text] unless that structure is genuinely shared and guaranteed.
Consider:
const [imageCell, contentCell] = row.children;
This is reasonable if the model guarantees two cells in that order. It is fragile if authors can add, remove, or reorder cells. Indexes are not automatically bad; undocumented positional assumptions are.
Rich text introduces another risk. A description can contain several paragraphs, a list, and links:
<div>
<p>Designed for <strong>everyday use</strong>.</p>
<ul>
<li>Feature one</li>
<li>Feature two</li>
</ul>
<p><a href="/details">Read details</a></p>
</div>
Reading cell.textContent flattens that structure into plain text. Sometimes that is the desired output, but not for a rich description. Likewise, reconstructing the content through an HTML string can discard semantics and create avoidable safety problems.
Where possible, preserve existing elements and move or style them. If the content already contains a <picture>, keep its responsive markup. If the CTA is an <a>, keep its navigation and keyboard behavior. A block can change layout without destroying meaningful HTML.
For our Cards example, suppose the contract is:
| Field | Contract | Expected behavior |
|---|---|---|
| Title | Required | The card needs a meaningful title |
| Image | Optional | Text-only layout remains usable |
| Description | Optional | No empty placeholder required |
| CTA | Optional | Card can render without a link |
A decorator should not crash because an optional CTA is absent:
export default function decorate(block) {
[...block.children].forEach((row) => {
row.classList.add('card');
const link = row.querySelector('a');
if (link) link.classList.add('button', 'secondary');
const picture = row.querySelector('picture');
if (picture) picture.closest('div')?.classList.add('card-image');
});
}
This is intentionally modest: it decorates existing HTML rather than reconstructing a complete card. A production implementation would also verify the title and any other requirements of its actual model.
Authoring validation and frontend resilience solve different problems. The authoring model should enforce required content where supported. The frontend should still handle missing or unexpected content without breaking the rest of the page. It should not invent a product title or silently turn invalid business content into valid content.
A repeated Cards block should normally work with the collection of rows rather than hardcoding three items:
const cards = [...block.children];
cards.forEach((card) => {
card.classList.add('card');
});
Avoid writing logic for children[0], children[1], and children[2] unless the contract explicitly requires exactly three entries. If a design has minimum or maximum item counts, define those limits intentionally and test them.
A useful test is to add a fourth card. If it fails, check more than JavaScript: the authoring model, delivered rows, CSS grid assumptions, and selectors may all be involved.
Suppose authors need standard, featured, and compact Cards. If each uses the same content structure and semantic purpose, variants can be a good fit:
<div class="cards featured block">...</div>
.cards.featured .card {
/* approved featured presentation */
}
JavaScript should branch on a variant only when behavior actually changes:
if (block.classList.contains('featured')) {
// Only behavior that genuinely differs for this variant
}
But consider a single cards block that gradually gains carousel, comparison, product-grid, and timeline variants. Those may require different data structures, interactions, accessibility behavior, and lifecycles. At that point, I would question whether they should be separate blocks.
Visual similarity is not sufficient reason to share a block. Reuse should follow a stable responsibility and content contract.
A title, description, image, and CTA are content. A choice such as Theme: Default | Dark or Alignment: Left | Center is presentation configuration.
I prefer exposing a small set of approved options instead of turning the authoring model into a CSS control panel with arbitrary font sizes, spacing, image dimensions, and color values. Otherwise, each page can become a separate design implementation that is difficult to maintain responsively.
Configuration should also live at the right scope. A setting for one Cards block belongs with that block; a site-wide theme should not depend on a single instance of Cards.
The Universal Editor configuration that enforces these choices is the focus of Chapter 7.
A static Hero or Cards block can often be enhanced synchronously. No network request is needed to render its authored content.
Other blocks genuinely need live data. For example, a product card may use authored promotional copy while price and availability come from a commerce service. Those fields have different owners and freshness requirements. Copying live price into authored content simply to render a card can create two sources of truth.
An asynchronous decorator might begin like this:
export default async function decorate(block) {
block.classList.add('is-loading');
try {
const response = await fetch('/api/example');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
block.classList.toggle('is-empty', !data.items?.length);
// Render or update the appropriate data-driven UI.
} catch (error) {
block.classList.add('has-error');
console.error('Cards data request failed', error);
} finally {
block.classList.remove('is-loading');
}
}
This is an illustrative pattern, not a complete integration. A real implementation must define what the user sees while loading, on success, when empty, and on failure. It must also consider accessibility, caching, SEO, latency, and whether a browser-side fetch is appropriate at all.
Never put API secrets or private service credentials in block JavaScript: code shipped to the browser is public. We will explore integration architecture in Chapter 27 and forms/API behavior in Chapter 28.
A Product Cards block should not casually reach into an unrelated Hero and modify its classes:
// Avoid as an accidental dependency between unrelated blocks.
document.querySelector('.hero')?.classList.add('has-product-cards');
Cross-block coordination is sometimes necessary, but it should be explicit and preferably handled through a suitable shared or page-level boundary.
The same principle applies to utilities. Shared date formatting, DOM helpers, or API clients can be useful when they represent genuinely stable shared behavior. I would not build a large BaseBlock framework just because two blocks share a few lines. A small, understandable decorator is one of the strengths of this development model.
Before implementing a block, I also ask:
These are not checks to postpone until a visual design is complete. The dedicated performance, accessibility, and security chapters will go deeper into them.
For a Cards block, I would test:
| Test | What it reveals |
|---|---|
| One card, then four cards | Fixed-count assumptions |
| CTA missing | Unsafe link selectors |
| Image missing | Whether text-only layout works |
| Long title and rich description | Overflow and DOM assumptions |
| Mobile, tablet, desktop | Responsive layout behavior |
| Featured variant | Variant class and CSS boundaries |
| API returns empty/error, if applicable | Runtime states and failure isolation |
| Keyboard navigation | Interaction semantics |
If the fourth card breaks, I would first ask whether the contract supports four items, then inspect delivered HTML, JavaScript, and CSS in that order. Sometimes a “JavaScript bug” is really an undocumented contract assumption.
| Question | Why I ask it |
|---|---|
| What content does it own? | Defines the content contract |
| What is required or optional? | Defines valid and fallback states |
| What repeats? | Guides row handling and item limits |
| Is rich text allowed? | Changes parsing and DOM assumptions |
| Does it need variants? | Controls approved differences |
| Does it need runtime data? | Adds network and state responsibilities |
| Who owns that runtime data? | Avoids duplicate sources of truth |
| Does it need interaction? | Changes accessibility and JavaScript needs |
| Does it depend on other blocks? | Reveals coupling |
| What can fail? | Guides graceful failure |
| What does it download? | Reveals performance cost |
I do not need a large architecture document for every block. But I do need the answers to be clear enough that someone else can maintain it.
With traditional AEM, I would often start by thinking about the dialog, Sling Model, HTL, and generated markup. With EDS, I still care about authoring constraints, but I also have to reason carefully about the HTML contract that arrives in the browser and what the decorator should—or should not—change.
The goal is not to make every block generic. It is to make each block's purpose obvious. Too few blocks can produce oversized components with dozens of variants; too many can produce duplicate implementations. Clear contracts make that tradeoff easier to manage.
In Chapter 7 — Universal Editor Models, we will turn these design questions into an authoring configuration: component definitions, models, filters, field choices, required and optional values, and placement rules.
Chapter 6 established the questions a block contract must answer. Chapter 7 explores how the authoring side expresses that contract.
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.