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:
<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:
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:
export default function decorate(block) {
console.log(block);
}
may log something conceptually similar to:
<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:
<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:
const rows = [...block.children];
Now rows is a normal JavaScript array containing the direct child elements.
For example:
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:
<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:
const row = block.children[0];
represents the first row.
Then:
const cells = [...row.children];
gives us the cells.
We can inspect them:
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:
block.children
versus:
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:
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:
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:
[...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:
decorate(block)
I avoid this unless there is a specific page-level requirement:
document.querySelector('.some-element');
Prefer:
block.querySelector('.some-element');
or:
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:
<div>
<p>This product supports <strong>multiple workflows</strong>.</p>
<p>Read the <a href="/guide">implementation guide</a>.</p>
</div>
If I do:
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:
const markup = cell.innerHTML;
and later:
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:
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:
<picture>
<source ...>
<img
src="..."
alt="Product"
width="..."
height="..."
>
</picture>
So I usually start with:
const picture = block.querySelector('picture');
not:
const imageUrl = block.textContent;
If I need the actual <img>:
const image = picture?.querySelector('img');
Then I can inspect:
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:
<div class="card-image">
<picture>...</picture>
</div>
I can create the wrapper:
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:
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:
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.
13. Links Already Carry Useful Behavior
A CTA may arrive as:
<a href="/products">Explore products</a>
The first thing I normally need is not to rebuild it.
It may simply need a class:
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.
14. Multiple Links Need a Clear Contract
Suppose Hero originally had one CTA:
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:
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:
<h2>Title</h2>
<p>Description</p>
<p><a href="/products">Explore</a></p>
Without it:
<h2>Title</h2>
<p><a href="/products">Explore</a></p>
If our JavaScript assumes:
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:
| Cell | Meaning |
|---|---|
| 1 | Image |
| 2 | Content |
Then:
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:
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:
<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:
<p>...</p>
<ul>...</ul>
<p>...</p>
but the CSS assumes:
.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:
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:
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:
<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:
block.classList.add('is-ready');
or:
item.classList.toggle('is-open');
But for accessibility-relevant states, CSS classes alone are not enough.
An Accordion may also need appropriate ARIA state:
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:
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:
Accordion A
other content
Accordion B
Clicking Accordion A should not accidentally modify Accordion B.
This is another reason to query relative to:
block
rather than:
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:
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:
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:
{
"status": "Available"
}
I can render it using:
const status = document.createElement('p');
status.textContent = data.status;
For runtime data, I prefer APIs such as:
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:
block.querySelector('.hero-title')
depends on a class.
A selector such as:
block.querySelector('h1, h2')
depends on semantics.
A selector such as:
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:
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:
.cards > div > div:nth-child(2) > p:nth-child(2) {
...
}
More intentional:
.cards .card-description {
...
}
This may require adding:
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:
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:
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:
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:
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:
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:
export default function decorate(block) {
console.log('block', block);
[...block.children].forEach((row, index) => {
console.log(`row ${index}`, row);
});
}
Or:
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.

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:
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:
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:
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:
<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.childrenis 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
innerHTMLthe 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; usemoveInstrumentationto 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.