N
Naveenr.dev
Chapter 07
16 min read•2026-09-26
📖 Edge Delivery Services SeriesChapter 07 · 27 chapters

Understanding the EDS Block DOM

Understanding the DOM structure received by an AEM Edge Delivery Services block, including rows, cells, rich text, images, links, optional content, selectors, transformations, events, and debugging before and after decorate(block).

Content Objective

This chapter covers:

  • What decorate(block) actually receives: an existing DOM element, not an empty container
  • How to read rows, cells, images, links, and rich text before writing any transformation
  • The difference between direct children, descendants, and semantic selectors
  • How optional content and repeated items change the DOM you have to handle
  • When to preserve delivered markup and when restructuring is justified
  • The extra DOM states an API-backed block moves through
  • Real Universal Editor gotchas from our POC: re-decoration, data-aue-* instrumentation, and stale service-worker JavaScript

Introduction

By now we have looked at both sides of an authorable EDS block.

Universal Editor defines the content authors can provide.

The block implementation handles what eventually reaches the browser.

Between those two sides is the DOM.

This is where I see many block implementation problems start.

The developer expects:

html
<div class="hero">
  <img>
  <h1>...</h1>
  <p>...</p>
</div>

but the browser receives rows, cells, paragraphs, <picture> elements, links, and wrappers that do not match that assumption.

The JavaScript is then written against the expected markup rather than the actual markup.

It works for one content example and fails as soon as:

  • a field is empty
  • rich text contains another paragraph
  • a link is missing
  • another item is added
  • an image is optional
  • the model changes

My rule is simple:

Inspect the DOM first. Write the transformation second.

This chapter is about doing that properly.


1. decorate(block) Starts With an Existing Element

A block JavaScript file normally starts with:

javascript
export default function decorate(block) {
  // block is already in the page
}

The important part is that block is not an empty container waiting for us to render a component.

It already represents the delivered block.

For example:

javascript
export default function decorate(block) {
  console.log(block);
}

may log something conceptually similar to:

html
<div class="hero block">
  <div>
    <div>
      <picture>...</picture>
    </div>
    <div>
      <h1>Explore our products</h1>
      <p>Find the right solution for your needs.</p>
      <p>
        <a href="/products">Explore products</a>
      </p>
    </div>
  </div>
</div>

That structure is our input.

Before changing it, I want to understand it.


2. Use DevTools Before the Code Editor

When I start a new block, one of my first tools is the browser Elements panel.

I inspect:

html
<div class="hero block">

and expand everything inside it.

I want to know:

  • how many direct children exist
  • which children represent rows
  • how many cells each row contains
  • what HTML exists inside each cell
  • whether an image arrives as <picture>
  • whether links already exist as <a>
  • whether rich text contains several elements
  • which classes already exist

Then I compare that structure with the authoring model.

This tells me whether my understanding of the content contract matches what the browser actually receives.


3. block.children

A useful starting point is:

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

Now rows is a normal JavaScript array containing the direct child elements.

For example:

javascript
export default function decorate(block) {
  const rows = [...block.children];

  console.log('row count:', rows.length);

  rows.forEach((row, index) => {
    console.log(`row ${index}`, row);
  });
}

This immediately tells me whether the block has:

  • one row
  • several repeated rows
  • an unexpected empty row
  • a structure different from what I assumed

I often do this before writing any transformation logic.


4. Rows and Cells

Each row can contain one or more cells.

For example:

html
<div class="cards block">
  <div>
    <div>
      <picture>...</picture>
    </div>

    <div>
      <h3>Product A</h3>
      <p>Description</p>
      <p><a href="/a">Learn more</a></p>
    </div>
  </div>
</div>

Here:

javascript
const row = block.children[0];

represents the first row.

Then:

javascript
const cells = [...row.children];

gives us the cells.

We can inspect them:

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

    console.log('cells:', cells);
  });
}

This is much safer than assuming the first <div> anywhere inside the block must be the image container.


5. Direct Children and Descendants Are Different

Consider:

javascript
block.children

versus:

javascript
block.querySelectorAll('div')

They answer different questions.

block.children gives me the direct children.

querySelectorAll('div') gives me matching descendants at every level.

For structural parsing, direct children are often more useful.

If I write:

javascript
const divs = block.querySelectorAll('div');

I may get:

  • rows
  • cells
  • nested wrappers
  • content wrappers created during decoration

all mixed together.

Use the DOM relationship that matches the contract you are trying to read.


6. querySelector() Is Useful for Semantic Elements

For elements with clear semantics, selectors are often easier.

For example:

javascript
const picture = block.querySelector('picture');
const heading = block.querySelector('h1, h2');
const link = block.querySelector('a');

This works well when the contract says:

The Hero contains one main picture, one heading, and one CTA.

For repeated structures, I may combine both approaches.

Use direct children to identify the item.

Then query within that item:

javascript
[...block.children].forEach((row) => {
  const picture = row.querySelector('picture');
  const heading = row.querySelector('h3');
  const link = row.querySelector('a');

  // decorate this item
});

That keeps selectors scoped to the item being processed.


7. Avoid Document-Level Queries Inside Blocks

Inside:

javascript
decorate(block)

I avoid this unless there is a specific page-level requirement:

javascript
document.querySelector('.some-element');

Prefer:

javascript
block.querySelector('.some-element');

or:

javascript
row.querySelector('.some-element');

The narrower scope reduces coupling.

If two copies of the same block exist on a page, document-level queries can easily target the wrong one.

A block should normally be able to exist more than once without one instance manipulating another.


8. Text Content and HTML Content Are Not the Same

Suppose the block contains:

html
<div>
  <p>This product supports <strong>multiple workflows</strong>.</p>
  <p>Read the <a href="/guide">implementation guide</a>.</p>
</div>

If I do:

javascript
const text = cell.textContent;

I get the textual value.

That may be useful.

But I lose the structure:

  • paragraphs
  • emphasis
  • link

If I need the authored rich content, I should preserve the existing nodes instead of flattening everything into text.

This is why I don't automatically convert every cell into a string.


9. Be Careful With innerHTML

It is easy to write:

javascript
const markup = cell.innerHTML;

and later:

javascript
wrapper.innerHTML = markup;

For trusted delivered markup in a tightly controlled transformation, there may be cases where developers use existing HTML.

But I don't make innerHTML the default block-building technique.

Moving existing DOM nodes is often clearer.

For example:

javascript
const wrapper = document.createElement('div');
wrapper.classList.add('card-content');

while (cell.firstChild) {
  wrapper.append(cell.firstChild);
}

Now the existing nodes are moved without converting the content to an HTML string and reparsing it.

For external or untrusted runtime data, I avoid inserting raw HTML entirely unless it has been handled through an appropriate trusted sanitization path.


10. Images Usually Arrive as <picture>

A block image may arrive as a <picture> structure rather than a simple image URL.

For example:

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

So I usually start with:

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

not:

javascript
const imageUrl = block.textContent;

If I need the actual <img>:

javascript
const image = picture?.querySelector('img');

Then I can inspect:

javascript
image?.alt
image?.width
image?.height

The delivered image structure already gives the browser useful information.

I preserve it unless the component has a real reason to change it.


11. Moving a <picture> Is Different From Recreating It

Suppose the design requires:

html
<div class="card-image">
  <picture>...</picture>
</div>

I can create the wrapper:

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

if (picture) {
  const imageWrapper = document.createElement('div');
  imageWrapper.classList.add('card-image');

  imageWrapper.append(picture);
  row.prepend(imageWrapper);
}

I am moving the existing <picture>.

I am not reading its URL and creating another <img>.

That preserves the useful delivered image markup.


12. Alternative Text Is Part of the Content Contract

When inspecting an image, I also inspect:

javascript
const img = block.querySelector('img');

console.log(img?.alt);

If the image is meaningful, the alternative text should make sense.

If it is purely decorative, the accessibility treatment may be different.

I would not use JavaScript to generate generic values such as:

javascript
img.alt = 'Hero image';

That usually does not describe the content.

Image semantics should come from the content/design decision, not a fallback string invented by the frontend.


A CTA may arrive as:

html
<a href="/products">Explore products</a>

The first thing I normally need is not to rebuild it.

It may simply need a class:

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

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

The anchor already knows how to:

  • navigate
  • receive keyboard focus
  • expose link semantics
  • support normal browser interactions

Preserving that is usually better than replacing it with a custom clickable element.


Suppose Hero originally had one CTA:

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

Later the model allows two.

Now that selector only returns the first one.

If two CTAs are valid, make that explicit:

javascript
const links = [...block.querySelectorAll('a')];

const [primary, secondary] = links;

if (primary) {
  primary.classList.add('button', 'primary');
}

if (secondary) {
  secondary.classList.add('button', 'secondary');
}

The code should reflect the component contract.

Don't accidentally support one item because the original sample content only had one.


15. Optional Elements Change the DOM

Suppose Description is optional.

With description:

html
<h2>Title</h2>
<p>Description</p>
<p><a href="/products">Explore</a></p>

Without it:

html
<h2>Title</h2>
<p><a href="/products">Explore</a></p>

If our JavaScript assumes:

javascript
const description = cell.children[1];
const cta = cell.children[2];

the second version breaks that assumption.

This is why optional fields matter when choosing selectors.

If the content contract allows optional structures, selectors should tolerate those states.


16. Positional Parsing Can Still Be Correct

There are blocks where the content contract intentionally defines positions.

For example:

CellMeaning
1Image
2Content

Then:

javascript
const [imageCell, contentCell] = [...row.children];

is reasonable.

That is not fragile if the contract guarantees it.

The important part is knowing the difference between:

The model guarantees two cells in this order.

and:

The sample content happened to have two cells.

The first is a contract.

The second is an assumption.


17. Repeated Items

Consider Cards:

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

    const picture = row.querySelector('picture');
    const heading = row.querySelector('h3');
    const link = row.querySelector('a');

    // decorate one card
  });
}

Each row is handled independently.

This has several advantages.

One card can omit an optional image without changing how another card is processed.

The number of cards can change.

The logic stays local to the repeated item.


18. Nested Content

Not every block stays one level deep.

Suppose a cell contains:

html
<div>
  <h3>Plan details</h3>
  <ul>
    <li>Feature A</li>
    <li>Feature B</li>
  </ul>
  <p>
    <a href="/details">View details</a>
  </p>
</div>

I would not flatten that structure just to make the JavaScript easier.

The browser already understands:

  • headings
  • lists
  • links

The block can style and position those elements without recreating them.

Preserving semantics often produces simpler code.


19. Rich Text Requires Flexible CSS Too

Rich text affects more than JavaScript.

Suppose Description can contain:

html
<p>...</p>
<ul>...</ul>
<p>...</p>

but the CSS assumes:

css
.card-description > p:first-child {
  ...
}

The JavaScript may be perfectly fine while the visual result still breaks.

When rich text is allowed, both DOM handling and CSS need to support the permitted content.

This is another reason not to expose rich text everywhere without a requirement.


20. Transform Only What the UI Needs

Suppose the raw Cards DOM already contains:

  • image
  • heading
  • description
  • link

Maybe all we need is:

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

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

    picture?.closest('div')?.classList.add('card-image');
    link?.classList.add('button', 'secondary');
  });
}

That may be enough.

I don't need to rebuild the entire card as:

javascript
const article = document.createElement('article');
const image = document.createElement('div');
const content = document.createElement('div');
const heading = document.createElement('h3');
// ...

unless the required final structure actually justifies it.

Small transformations are easier to maintain.


21. When Restructuring Is Useful

There are cases where the delivered structure does need meaningful transformation.

Examples include:

  • Accordion interaction
  • Tabs
  • Carousel controls
  • comparison interfaces
  • complex forms

An Accordion may need to associate a trigger with a panel and maintain expanded state.

That is a real behavioral requirement.

In those cases, restructuring the DOM can make sense.

The rule is not:

Never transform the DOM.

The rule is:

Transform it because the component needs that structure, not because rewriting everything feels cleaner.


22. Before and After Decoration

For debugging, I sometimes compare the structure conceptually as two states.

Before decoration

The DOM represents delivered content.

After decoration

The DOM represents the enhanced component.

For example, before:

html
<div class="accordion block">
  <div>
    <div>What is EDS?</div>
    <div>...</div>
  </div>
</div>

After decoration, it might become a structure with:

  • an interactive trigger
  • an associated content panel
  • state attributes
  • classes required for interaction

The exact implementation depends on the accessibility and UI requirements.

Thinking in before/after states helps me keep the transformation deliberate.


23. Use Classes to Describe UI State

For interactive components, classes can represent state.

For example:

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

or:

javascript
item.classList.toggle('is-open');

But for accessibility-relevant states, CSS classes alone are not enough.

An Accordion may also need appropriate ARIA state:

javascript
button.setAttribute('aria-expanded', String(isOpen));

The visual state and semantic state need to stay aligned.


24. Event Listeners Should Stay Inside the Block Boundary

Suppose an Accordion has buttons.

A reasonable pattern is:

javascript
const buttons = block.querySelectorAll('button');

buttons.forEach((button) => {
  button.addEventListener('click', () => {
    // update this accordion
  });
});

I would avoid a page-level listener that knows about every Accordion unless there is a specific reason for event delegation at that scope.

Keeping behavior inside the block makes repeated instances easier to reason about.


25. Repeated Blocks Must Be Independent

A page might contain:

text
Accordion A

other content

Accordion B

Clicking Accordion A should not accidentally modify Accordion B.

This is another reason to query relative to:

javascript
block

rather than:

javascript
document

The block parameter gives us a natural component boundary.

Use it.


26. Async Data Adds Another DOM Stage

For an API-backed block, the DOM may go through more than two states.

For example:

Delivered DOM

contains authored heading and description.

Loading state

shows that runtime data is being retrieved.

Success state

adds the returned information.

Error state

keeps the authored content but shows an appropriate fallback.

This means decorate(block) is not always a single synchronous transformation.

But the same principle applies:

Start from the delivered block.

Then enhance it according to the runtime state.


27. Don't Remove Useful Authored Content While Waiting for an API

Suppose the block contains:

text
Check product availability
Enter your location to see availability near you.

That authored content can render immediately.

I would not blank the entire block while waiting for:

javascript
await fetch(...);

Keep stable content visible.

Add dynamic data when it becomes available.

This creates a better failure mode too.

If the API fails, the block can still explain what it is rather than disappearing completely.


28. Runtime Data Should Use Safe DOM APIs

Suppose an API returns:

json
{
  "status": "Available"
}

I can render it using:

javascript
const status = document.createElement('p');
status.textContent = data.status;

For runtime data, I prefer APIs such as:

javascript
textContent

and explicit element creation.

I do not assume external data is safe HTML.

This becomes especially important when API values can contain user-controlled or external content.


29. Selectors Are Part of the Contract

A selector such as:

javascript
block.querySelector('.hero-title')

depends on a class.

A selector such as:

javascript
block.querySelector('h1, h2')

depends on semantics.

A selector such as:

javascript
block.children[0]

depends on position.

None is automatically correct.

Choose the selector based on what is stable in the block contract.

Ask:

What is guaranteed to remain true?

That should drive the selector.


30. Avoid Selectors Based on Incidental Wrappers

A selector like:

javascript
block.querySelector('div > div > div > p:nth-child(2)')

is a warning sign.

It may describe today's DOM precisely.

It also couples the implementation to every wrapper in that structure.

If one wrapper changes, the selector breaks.

Prefer selectors tied to:

  • direct structural contracts
  • semantic elements
  • explicit classes added by the block

The goal is not the shortest selector.

It is the most stable selector for the contract.


31. CSS Selectors Need the Same Discipline

The same issue can happen in CSS.

Fragile:

css
.cards > div > div:nth-child(2) > p:nth-child(2) {
  ...
}

More intentional:

css
.cards .card-description {
  ...
}

This may require adding:

javascript
description.classList.add('card-description');

during decoration.

That is often worth it.

A few meaningful classes can make the CSS contract much clearer.


32. Don't Add Classes Without a Reason

The opposite problem is adding a class to every element:

text
hero-row
hero-cell
hero-cell-inner
hero-title-wrapper
hero-title
hero-description-wrapper
hero-description
hero-cta-wrapper
hero-cta

That can recreate component markup complexity we did not need.

Add classes when they help:

  • styling
  • behavior
  • state
  • debugging

Not because every element needs a custom name.


33. closest() Is Useful, but Understand What It Finds

Consider:

javascript
const picture = block.querySelector('picture');
const cell = picture?.closest('div');

This returns the nearest ancestor <div>.

That may be the image cell.

But I verify it.

If the image markup gains another wrapper, the nearest <div> may change.

When using:

javascript
closest()
parentElement
children

I still check the actual DOM structure.

DOM traversal is only reliable when we understand the relationships being traversed.


34. parentElement Can Encode Hidden Assumptions

This:

javascript
const wrapper = link.parentElement.parentElement;

works only while the link remains exactly two levels below the desired wrapper.

If that hierarchy is guaranteed, fine.

If not, it is fragile.

I prefer code that communicates the actual relationship being used.

Sometimes that means:

javascript
const cell = link.closest('.some-known-class');

Other times it means parsing direct row/cell structure before looking for the link.

Again, the contract decides.


35. Use DevTools to Inspect the Transformation

While developing a block, I use the Elements panel before and after decoration.

I check:

  • which classes were added
  • whether nodes moved
  • whether links remain links
  • whether headings remain meaningful
  • whether event controls have the expected attributes
  • whether optional content changed the structure
  • whether unexpected wrappers appeared

The final screenshot is not enough.

A component can look correct while its DOM is difficult to maintain or inaccessible.


36. Use the Console to Inspect Specific Nodes

Temporary logs are useful during block development.

For example:

javascript
export default function decorate(block) {
  console.log('block', block);

  [...block.children].forEach((row, index) => {
    console.log(`row ${index}`, row);
  });
}

Or:

javascript
console.log({
  picture: block.querySelector('picture'),
  heading: block.querySelector('h1, h2'),
  links: [...block.querySelectorAll('a')],
});

Once the block is understood, remove unnecessary debugging logs.

The goal is to inspect the contract, not leave console noise in production.


37. A DOM Debugging Sequence

When a block behaves incorrectly, this is the sequence I use.

1. Find the block

Does the expected block element exist?

2. Inspect direct children

Does the row structure match the contract?

3. Inspect each cell

Is the expected content in the expected structural area?

4. Inspect semantic elements

Are images, headings, links, and rich text present?

5. Confirm decoration

Did decorate(block) run?

6. Inspect the transformed DOM

What changed?

7. Check CSS

Do selectors match the transformed structure?

This avoids changing JavaScript before we understand the input.

EDS block DOM inspection flow moving from the block element through direct children, rows, cells, and semantic elements into decorate(block) and the final decorated DOM
EDS block DOM inspection flow moving from the block element through direct children, rows, cells, and semantic elements into decorate(block) and the final decorated DOM

38. Common DOM Mistakes

Most block DOM problems I see fall into a small set of patterns.

Assuming sample content is the contract

One example has an image, title, description, and CTA.

The code assumes all four always exist.

Flattening rich text

Using textContent when the component needs paragraphs, links, or lists.

Recreating existing semantic elements

Replacing anchors, headings, or pictures without a requirement.

Querying the whole document

One block accidentally modifies another.

Using deeply positional selectors

The implementation breaks after a harmless structural change.

Rebuilding too much DOM

The block becomes harder to understand than the delivered structure.

These are usually design problems rather than complicated JavaScript problems.


39. Universal Editor Edits the DOM Live: Real Gotchas From the POC

Everything above assumes decorate(block) runs once against delivered content. Inside Universal Editor that assumption breaks, and it cost us real debugging time on the POC.

Universal Editor re-invokes decorate() on the same element

After an author edits a field, Universal Editor calls decorate(block) again on the same block element. It does not reload the page.

If the block rebuilds its DOM into a new structure — for example, replacing the delivered rows with a <form> or an <article> wrapper — the second run parses the already-transformed markup instead of the original rows.

The symptom we hit: a newly added field appeared only after a full page refresh, then disappeared again on the next edit. The block was re-decorating its own output.

Two habits prevent this:

  • rebuild from a stable read of the delivered structure, not from your own output, or
  • detect whether the block has already been decorated and skip the destructive rebuild.

Rebuilding the DOM discards the authoring instrumentation

Universal Editor attaches data-aue-* attributes (data-aue-resource, data-aue-model, and related) to the delivered rows. Those attributes are how the editor maps each authored field back to content.

If decorate() does block.textContent = '' or otherwise rebuilds children from scratch, those attributes are thrown away. The symptom is subtle but serious: the block still renders, but the author's "+" / Add control silently does nothing, because the editor can no longer map a new child to a resource.

The boilerplate fix is moveInstrumentation from scripts.js. When you create a new element to replace a delivered row, move the instrumentation across:

javascript
import { moveInstrumentation } from '../../scripts/scripts.js';

[...block.children].forEach((row) => {
  const card = document.createElement('div');
  // build card from row...
  moveInstrumentation(row, card);
  block.append(card);
});

Keep the rebuilt element as a descendant of the block so the editor can still find it.

Detect child rows by data-aue-model, not by cell text

For container blocks with repeatable child items, it is tempting to identify a child row by matching the text or type of its first cell. That is fragile: select values can render as display names, and empty fields can shift positions.

Each child row carries its own data-aue-model set to the child component id. Detect children by that attribute:

javascript
const isChild = (row) => row.getAttribute('data-aue-model') === 'card';

On the POC, a form block was dropping fields because it matched a control's text against a fixed set of field-type strings. Switching to data-aue-model detection fixed it.

An editor-only diagnostic

When a block behaves differently inside Universal Editor, dump the raw rows only in the editor context:

javascript
if (block.hasAttribute('data-aue-resource')) {
  console.debug('[block] raw rows', [...block.children].map((row) => ({
    cells: row.children.length,
    model: row.getAttribute('data-aue-model'),
    text: row.firstElementChild?.textContent?.trim().slice(0, 40),
  })));
}

This shows the real editor DOM — cell counts, model ids, and first-cell text — which is exactly what you need when authoring and delivery disagree.

Stale JavaScript from the service worker

The boilerplate registers a service worker. After deploying new block JavaScript, the browser can still run the old file from the service-worker cache. If a block change "isn't taking effect," hard-reload (Cmd+Shift+R) or unregister the service worker before assuming the code is wrong.


40. Developer Perspective

The DOM is one of the most important contracts in EDS block development.

I don't treat it as generated markup that I can ignore.

I inspect it because it tells me:

  • what the authoring model actually produced
  • what EDS delivered
  • what my JavaScript needs to handle
  • what CSS can target
  • what semantics already exist

The less unnecessary DOM work the block performs, the easier it is to debug.


41. AEM Developer Perspective

For an AEM developer, this is one of the bigger implementation shifts.

With HTL, we often control the final markup directly.

We write:

html
<div class="cmp-example">
  ...
</div>

and the server renders that structure.

With an EDS block, I first inspect the content-derived DOM and then enhance it.

That means browser DevTools becomes part of the implementation workflow much earlier.

Instead of asking:

What HTML should my template render?

I often start with:

What HTML did the content already produce, and what does the UI still need?

That is a different development habit.


42. Architect Perspective

At architecture level, fragile DOM assumptions become maintenance problems.

If every block:

  • parses content differently
  • uses deep positional selectors
  • rebuilds semantic HTML
  • depends on global selectors
  • manipulates unrelated blocks

then the project becomes difficult to evolve.

I would establish a few simple block rules:

  • inspect before transforming
  • keep selectors local
  • preserve semantics
  • document intentional positional contracts
  • handle optional content deliberately
  • avoid unnecessary reconstruction
  • keep repeated block instances independent
  • treat runtime data as untrusted unless its trust boundary says otherwise

These rules are small, but they prevent a large amount of frontend complexity.


43. What I Learned From Working With the DOM

The DOM stopped feeling like an implementation detail once I started debugging EDS blocks.

It is the point where the authoring contract becomes something the frontend can actually inspect.

If the content model says one thing and the DOM shows another, I can see the mismatch.

If the DOM is correct before decoration and wrong afterward, I know where the problem started.

If the final DOM is correct but the page looks wrong, I move toward CSS.

That gives me clear boundaries.

And those boundaries make block development much easier than trying to reason about authoring, delivery, JavaScript, and styling at the same time.


Key Takeaways

  • decorate(block) receives an existing DOM element containing delivered content.
  • Inspect the actual block DOM before implementing transformation logic.
  • block.children is useful for direct structural parsing.
  • Rows and cells should be understood before querying nested elements.
  • Direct children and descendant selectors solve different problems.
  • Scope selectors to the block or repeated item whenever possible.
  • Rich text should not be flattened unless plain text is genuinely required.
  • Avoid making innerHTML the default way to rebuild content.
  • Preserve delivered <picture> structures where possible.
  • Move existing DOM nodes instead of recreating them without a reason.
  • Image alternative text is part of the content/accessibility contract.
  • Preserve anchor semantics for navigation.
  • Optional fields must be considered when choosing positional selectors.
  • Positional parsing is reasonable when the model guarantees the structure.
  • Process repeated items independently.
  • Preserve useful nested and semantic HTML.
  • Transform only what the UI requires.
  • Interactive blocks may need deliberate DOM restructuring.
  • Keep visual and accessibility state synchronized.
  • Keep event handling within the block boundary where possible.
  • Multiple instances of the same block should remain independent.
  • Async blocks add loading, success, empty, and error DOM states.
  • Keep stable authored content visible while dynamic data loads where appropriate.
  • Use safe DOM APIs for runtime/external data.
  • Choose selectors based on what the component contract guarantees.
  • Avoid selectors tied to incidental wrapper depth.
  • CSS selectors need the same stability as JavaScript selectors.
  • Add meaningful classes where they improve styling or behavior, not everywhere.
  • Use DevTools to inspect both the input DOM and the transformed DOM.
  • In Universal Editor, decorate(block) runs again on the same element after every edit; do not re-decorate your own output.
  • Rebuilding block DOM discards data-aue-* instrumentation; use moveInstrumentation to preserve authoring controls.
  • Detect repeatable child rows by data-aue-model, not by matching cell text.
  • A service worker can serve stale block JavaScript; hard-reload after deploys when testing.
  • Most DOM bugs come from undocumented assumptions about the content structure.

Next Steps

We now understand what the block receives in the browser, and how that DOM behaves both at delivery time and while an author edits it in Universal Editor.

The next step is to connect everything into a complete authorable component — component definition, model, and filter on the authoring side, and the block implementation on the frontend side — following one component through its full lifecycle.

The next chapter, Building a Universal Editor Component, takes the Hero through that complete path: component definition, model, filter, authoring, delivered structure, block implementation, Preview, local development, and the debugging boundaries between authoring and frontend.

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.