Edge Delivery Services · 17 of 29
Universal Editor data-aue-* annotations for xwalk projects. Covers data-aue-resource, data-aue-type, data-aue-prop, data-aue-label, data-aue-model, containers, and editor-support.js.
This chapter covers:
data-aue-* attributes and what each one doesdata-aue-resource (content pointer) and data-aue-prop (field pointer)data-aue-type values and when to use eachdecorate() functioneditor-support.js adds the page-level annotations automaticallyWhen you open a page in Universal Editor, you see the fully rendered page. When you click on the hero title, UE should open the properties panel on the right showing the Title field.
But how does UE know that the <h1 class="hero-header"> element corresponds to the title field of the hero component in AEM?
It does not know automatically. Your block's decorate() function renders HTML from content that was originally stored in AEM. The connection between the rendered DOM element and the JCR content node is severed during rendering unless you explicitly re-establish it.
The data-aue-* attributes are the bridge that reconnects rendered HTML to authored content.
WITHOUT ANNOTATIONS:
User clicks <h1 class="hero-header">
→ UE: "I don't know what content node this is. Nothing to show."
→ Properties panel: empty
WITH ANNOTATIONS:
User clicks <h1 data-aue-prop="title" data-aue-resource="urn:aem:...">
→ UE: "This is the 'title' field of the resource at this path."
→ Properties panel: opens with Title field showing current value
data-aue-resourceWhat it is: A URN pointing to the JCR content node that this element comes from.
Format:
urn:aem:/content/{site}/{page}/jcr:content/root/container/{component-path}
Example:
<div class="hero" data-aue-resource="urn:aem:/content/eds-poc/en/home/jcr:content/root/container/hero">
Rules:
urn:aem: scheme for AEM Cloud instanceseditor-support.js adds this automatically to blocks at the page level — you may need to add it to child items in collectionsdata-aue-typeWhat it is: Declares the type of data this element contains.
All valid values:
| Type | Description | When to use |
|---|---|---|
text | Plain text content | Short strings, titles, labels |
richtext | HTML rich text | Paragraphs, formatted content |
media | Image or video | <img>, <video>, <picture> |
reference | Link to another content node | Content Fragments, Experience Fragments |
boolean | True/false toggle | Checkboxes, yes/no fields |
select | Enumerated value | Dropdown choices |
container | Contains child items | Sections, multifields, collections |
component | A component node | Individual component instances |
Example:
<h1 data-aue-prop="title"
data-aue-type="richtext"
data-aue-resource="urn:aem:...">
Welcome to EDS
</h1>
data-aue-propWhat it is: The field name in the component model that this element represents.
Rules:
component-models.json model definition exactlyExample:
If your hero model defines:
{
"id": "hero-model",
"fields": [
{ "component": "richtext", "name": "title", "label": "Title" },
{ "component": "text", "name": "subtitle", "label": "Subtitle" },
{ "component": "reference", "name": "image", "label": "Hero Image" }
]
}
Then your rendered HTML annotations are:
<div class="hero" data-aue-resource="urn:aem:...">
<h1 data-aue-prop="title" data-aue-type="richtext">Welcome</h1>
<p data-aue-prop="subtitle" data-aue-type="text">Discover more</p>
<img data-aue-prop="image" data-aue-type="media" src="..." alt="...">
</div>
data-aue-labelWhat it is: Human-readable label shown in the UE editor when the element is selected or hovered.
Rules:
label in component-models.json<h1 data-aue-prop="title"
data-aue-type="richtext"
data-aue-label="Hero Title">
Welcome
</h1>
data-aue-modelWhat it is: The ID of the component model to use when UE builds the properties panel for this element.
Rules:
id in your component-models.json<div class="hero"
data-aue-resource="urn:aem:..."
data-aue-type="component"
data-aue-model="hero-model">
...
</div>
When you click on an annotated element in UE, this is the resolution chain:
data-aue-resourceIf any step breaks (missing resource, wrong prop name, model not found), the click either does nothing or opens an empty panel.
Here is a complete hero block with full annotations.
{
"id": "hero-model",
"fields": [
{
"component": "richtext",
"name": "title",
"label": "Title",
"required": true
},
{
"component": "text",
"name": "subtitle",
"label": "Subtitle"
},
{
"component": "reference",
"name": "image",
"label": "Hero Image"
},
{
"component": "text",
"name": "cta-primary-label",
"label": "Primary Button Label"
},
{
"component": "aem-content",
"name": "cta-primary-link",
"label": "Primary Button Link"
}
]
}
// blocks/hero/hero.js
export default function decorate(block) {
const row = block.firstElementChild;
const cells = [...row.children];
const title = cells[0]?.querySelector('h1, h2, h3')?.innerHTML || '';
const subtitle = cells[1]?.textContent?.trim() || '';
const image = cells[2]?.querySelector('picture');
const ctaPrimaryLabel = cells[3]?.textContent?.trim() || '';
const ctaPrimaryLink = cells[4]?.querySelector('a')?.href || '';
// Get the resource URN from the block root
// editor-support.js has already set data-aue-resource on the block root
const resourceUrn = block.dataset.aueResource || '';
block.innerHTML = `
<div class="hero-section">
<div class="hero-content">
<h1 class="hero-header"
data-aue-prop="title"
data-aue-type="richtext"
data-aue-label="Title">${title}</h1>
<p class="hero-subtitle"
data-aue-prop="subtitle"
data-aue-type="text"
data-aue-label="Subtitle">${subtitle}</p>
<div class="hero-cta">
<a class="btn btn-primary"
href="${ctaPrimaryLink}"
data-aue-prop="cta-primary-link"
data-aue-type="reference"
data-aue-label="Primary Button Link">${ctaPrimaryLabel}</a>
</div>
</div>
<div class="hero-image-wrapper"
data-aue-prop="image"
data-aue-type="media"
data-aue-label="Hero Image">
</div>
</div>
`;
// Append the picture element into the annotated wrapper
const imageWrapper = block.querySelector('.hero-image-wrapper');
if (image) imageWrapper.append(image);
}
Critical observations:
block.dataset.aueResource — editor-support.js sets data-aue-resource on the block root before decorate() runs. You read it from the existing element, not generate it yourself.innerHTML with trusted content only — the original block HTML comes from AEM content (trusted), but apply proper sanitization if any external data is mixed in.data-aue-resource to child field elements — only the block root has it. Field elements get data-aue-prop only. UE inherits the resource context from the nearest ancestor with data-aue-resource.The boilerplate includes scripts/editor-support.js which runs automatically when the page is loaded inside UE.
What it does:
<head> by the serverdata-aue-resource, data-aue-type="component", and data-aue-model to every block root automatically based on the content treedata-aue-type="container" and data-aue-resource to every sectionThis means you do not need to manually add data-aue-resource to your block root — it is added for you before decorate() runs. You only need to add data-aue-prop (and optionally data-aue-type and data-aue-label) to the individual rendered elements inside the block.
Important: Do not call block.innerHTML = ... and lose the data-aue-* attributes that editor-support.js set on the block root. The typical safe approach:
// SAFE — reads existing annotations, preserves block root element
export default function decorate(block) {
// Read existing annotations set by editor-support.js
const resourceUrn = block.dataset.aueResource;
// Build inner content (does not replace block root, only its children)
block.innerHTML = `<div class="hero-section">...</div>`;
// block root (<div class="hero">) is preserved with its data-aue-* attributes
// Only the innerHTML was replaced
}
When a block contains a list of items (cards, team members, steps), each item needs its own data-aue-resource pointing to its individual JCR node.
In AEM (xwalk), a cards block with 3 cards is stored as:
/content/page/jcr:content/root/container/cards
/item0 (card 1)
/item1 (card 2)
/item2 (card 3)
Each card is a separate JCR node with its own path. The container (cards block) has data-aue-type="container" and each item has data-aue-type="component" with its own resource path.
// blocks/cards/cards.js
export default function decorate(block) {
const blockResource = block.dataset.aueResource || '';
const items = [...block.children];
const ul = document.createElement('ul');
ul.classList.add('cards-items');
items.forEach((row, idx) => {
const image = row.querySelector('picture');
const title = row.querySelector('h2, h3')?.textContent?.trim() || '';
const description = row.querySelector('p')?.innerHTML || '';
// The item resource is derived from the block resource + item node name
// editor-support.js provides the correct resource per item on the row element
const itemResource = row.dataset.aueResource
|| `${blockResource}/item${idx}`;
const li = document.createElement('li');
li.classList.add('cards-item');
li.setAttribute('data-aue-resource', itemResource);
li.setAttribute('data-aue-type', 'component');
li.setAttribute('data-aue-model', 'card-model');
li.setAttribute('data-aue-label', `Card ${idx + 1}`);
li.innerHTML = `
<div class="cards-item-image" data-aue-prop="image" data-aue-type="media" data-aue-label="Image"></div>
<div class="cards-item-body">
<h3 data-aue-prop="title" data-aue-type="text" data-aue-label="Title">${title}</h3>
<p data-aue-prop="description" data-aue-type="richtext" data-aue-label="Description">${description}</p>
</div>
`;
if (image) li.querySelector('.cards-item-image').append(image);
ul.append(li);
});
block.innerHTML = '';
block.append(ul);
}
The block root gets data-aue-type="container" (set by editor-support.js). Each card <li> gets data-aue-type="component" with its own resource path. Now authors can click individual cards in UE, and UE opens that specific card's properties.
When data-aue-type="container" is on the block root, UE shows an "Add" button in the editor toolbar to add new items to the container. For this to work correctly:
component-filters.jsoncomponent-models.json/* component-filters.json */
{
"id": "cards-filter",
"components": ["card"]
}
/* component-definition.json */
{
"groups": [
{
"title": "Blocks",
"id": "blocks",
"components": [
{
"title": "Cards",
"id": "cards",
"plugins": {
"xwalk": {
"page": {
"resourceType": "core/franklin/components/block/v1/block",
"template": {
"name": "Cards",
"filter": "cards-filter"
}
}
}
}
}
]
}
]
}
Two things about containers are not obvious until they break in a real project.
Rebuilding the block DOM breaks "Add child." editor-support.js puts data-aue-* (including data-aue-model) on each child row before decorate() runs. If your container decorate() does block.innerHTML = '' and rebuilds from scratch — as the cards example above does for brevity — those attributes are discarded, and the UE + / Add button silently does nothing. Carry the instrumentation across with moveInstrumentation from scripts.js:
import { moveInstrumentation } from '../../scripts/scripts.js';
[...block.children].forEach((row) => {
const li = document.createElement('li');
moveInstrumentation(row, li); // carries data-aue-* from the source row
// ...build the card markup inside li...
ul.append(li);
});
Re-decoration runs on the same element. After an edit, UE calls decorate() again on the same block element — now holding your already-transformed markup. A block that rebuilds into a new structure (for example wrapping everything in a <form>) will, on the second run, parse its own output and drop children. The symptom is "the field shows up, then disappears on the next edit." Detect child rows by their own data-aue-model attribute rather than by matching cell text, and do not assume the markup is fresh.
The Universal Editor validates component-models.json, and two rules cost us real time:
xwalk/max-cells). To carry more fields, group them under a shared underscore prefix so they collapse into one cell — settings_label and settings_required collapse into a single settings group.Type, Alt, Text, or Title trigger semantic collapsing (xwalk/no-orphan-collapsible-fields). UE expects a partner field and errors if it is missing. For standalone fields, avoid those suffixes — we renamed formType to endpointKey and fieldType to control to get past it.If a field is a richtext type, the author can edit it directly in context (inline editing) by double-clicking. For this to work:
<div data-aue-prop="description"
data-aue-type="richtext"
data-aue-label="Description">
<p>The content that can be edited inline</p>
</div>
The element must:
<p>, <strong>, <em> etc.data-aue-type="richtext" (not text)For text type, the author uses the properties panel field. For richtext, they can edit inline or use the panel.
// BREAKS annotations — replaces block root element's attributes
const newBlock = document.createElement('div');
newBlock.classList.add('hero');
newBlock.innerHTML = template;
block.parentNode.replaceChild(newBlock, block);
editor-support.js already set data-aue-resource on the original block element. Replacing it with a new element loses those attributes. Always manipulate block.innerHTML or append children, never replace the block element itself.
// data-aue-prop is "headline" but model has field named "title"
element.setAttribute('data-aue-prop', 'headline'); // WRONG
element.setAttribute('data-aue-prop', 'title'); // CORRECT
UE will not find the field if the prop name doesn't match. The properties panel appears but the field is not pre-filled.
// UE can't show the media picker without knowing the type
img.setAttribute('data-aue-prop', 'image');
// Missing: img.setAttribute('data-aue-type', 'media');
Without data-aue-type="media", clicking the image opens no editor.
// Hardcoded — breaks when page path changes
block.setAttribute('data-aue-resource',
'urn:aem:/content/eds-poc/en/home/jcr:content/root/container/hero_1234');
Never hardcode resource paths. editor-support.js injects the correct path from the server-side rendered <meta> tags. Read block.dataset.aueResource which is already set correctly.
export default function decorate(block) {
// Annotating BEFORE rendering — element doesn't exist yet
block.querySelector('.hero-header').setAttribute('data-aue-prop', 'title');
// Then rendering — annotation is lost when innerHTML is replaced
block.innerHTML = '<h1 class="hero-header">title</h1>';
}
Always add annotations AFTER rendering the DOM:
export default function decorate(block) {
block.innerHTML = '<h1 class="hero-header">title</h1>';
// Annotate AFTER DOM exists
block.querySelector('.hero-header').setAttribute('data-aue-prop', 'title');
block.querySelector('.hero-header').setAttribute('data-aue-type', 'richtext');
}
Annotations add attributes to every rendered element on every page, including the live published site. This is harmless from a functionality perspective (browsers ignore unknown attributes), but it adds DOM weight. For performance-sensitive pages, you can conditionally add annotations:
export default function decorate(block) {
block.innerHTML = buildTemplate();
// Only annotate when running inside UE
if (block.dataset.aueResource) {
applyAnnotations(block);
}
}
function applyAnnotations(block) {
block.querySelector('.hero-header')?.setAttribute('data-aue-prop', 'title');
block.querySelector('.hero-header')?.setAttribute('data-aue-type', 'richtext');
// ...
}
Check block.dataset.aueResource as a proxy for "are we inside UE" — editor-support.js only runs inside UE, so the attribute is absent on the published site.
// Block renders fine, but "Add" button never appears in UE
// Root cause: filter not configured in component-filters.json
// or block not declared as container in component-definition.json
If an author cannot add items to a container block in UE, check:
component-filters.json — does a filter exist for this block? Does it list the allowed child component IDs?component-definition.json — does the block's template reference the filter?component-models.json — does the child item model exist with the correct ID?In the browser (outside UE):
// Paste in DevTools console on your page
document.querySelectorAll('[data-aue-resource]').forEach(el => {
console.log(el.className, el.dataset.aueResource);
});
document.querySelectorAll('[data-aue-prop]').forEach(el => {
console.log(el.dataset.aueProp, el.dataset.aueType, el.tagName);
});
In UE:
data-aue-prop should be visible and editableIf the panel is empty: check that data-aue-prop matches the field name in the model.
If the panel doesn't open: check that data-aue-resource is on the block root (inspect the DOM).
If the add button is missing: check component-filters.json and component-definition.json.
| Attribute | Required | Scope | Value |
|---|---|---|---|
data-aue-resource | Yes | Block root, container items | urn:aem:/content/... (set by editor-support.js) |
data-aue-type | Yes | Any annotated element | text, richtext, media, reference, boolean, select, container, component |
data-aue-prop | Yes (for fields) | Field elements | Must match name in component model |
data-aue-label | Recommended | Any annotated element | Human-readable display name |
data-aue-model | Yes (for components) | Component roots | Must match id in component-models.json |
data-aue-resource is the JCR path pointer — set automatically by editor-support.js, read it from block.dataset.aueResourcedata-aue-prop is the field pointer — must match the field name in your component model exactlydata-aue-type declares the data type — determines which editor UE opens for the elementblock.innerHTML to preserve editor-support.js attributesinnerHTML is setcomponent-filters.json + component-definition.json setupblock.dataset.aueResource as an UE environment check for zero-overhead on live pagesIn the next chapter, we cover DA vs Universal Editor — a head-to-head comparison from both the author's and developer's perspective. Same page, same block, two authoring systems. We answer the question: for your project, which one do you choose and why?
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.