Universal Editor Annotations
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.
Content Objective
This chapter covers:
- What Universal Editor annotations are and why they are required for in-context editing
- The five
data-aue-*attributes and what each one does - The difference between
data-aue-resource(content pointer) anddata-aue-prop(field pointer) - How UE maps a click on rendered HTML back to the authored field in AEM
- The complete list of
data-aue-typevalues and when to use each - How to annotate block JS correctly in the
decorate()function - Containers: making sections and collections of items editable
- How
editor-support.jsadds the page-level annotations automatically - The 7 mistakes that break in-context editing silently
The Problem Annotations Solve
When 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
The Five Annotation Attributes
1. data-aue-resource
What 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:
- Required on the element that represents the block's root content node
- The path is the JCR path to the component node (not the page, not the resource type)
- Use the
urn:aem:scheme for AEM Cloud instances editor-support.jsadds this automatically to blocks at the page level — you may need to add it to child items in collections
2. data-aue-type
What 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>
3. data-aue-prop
What it is: The field name in the component model that this element represents.
Rules:
- Must match the name of a field in your
component-models.jsonmodel definition exactly - Case-sensitive
- Only add to elements that correspond to a specific authored field
Example:
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>
4. data-aue-label
What it is: Human-readable label shown in the UE editor when the element is selected or hovered.
Rules:
- Optional but strongly recommended — helps authors understand what they are clicking
- Should match the
labelincomponent-models.json - Shown in UE's element selection indicator
<h1 data-aue-prop="title"
data-aue-type="richtext"
data-aue-label="Hero Title">
Welcome
</h1>
5. data-aue-model
What it is: The ID of the component model to use when UE builds the properties panel for this element.
Rules:
- Required on the root element of a block (or child component in a container)
- Must match an
idin yourcomponent-models.json - Only needed on elements that are containers or component roots — not on individual field elements
<div class="hero"
data-aue-resource="urn:aem:..."
data-aue-type="component"
data-aue-model="hero-model">
...
</div>
How UE Maps the Click to AEM
When you click on an annotated element in UE, this is the resolution chain:
- Click event → UE traverses up the DOM to find the nearest
data-aue-resource - Resource found → UE knows the JCR path of the content node
- Prop found → UE knows which field on that node to open
- Model found → UE knows which fields to show in the properties panel
- Properties panel opens → Author sees the field with the current value
If any step breaks (missing resource, wrong prop name, model not found), the click either does nothing or opens an empty panel.
Annotating a Block — Step by Step
Here is a complete hero block with full annotations.
The Component Model
{
"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"
}
]
}
The Block's decorate() with Annotations
// 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.jssetsdata-aue-resourceon the block root beforedecorate()runs. You read it from the existing element, not generate it yourself.innerHTMLwith trusted content only — the original block HTML comes from AEM content (trusted), but apply proper sanitization if any external data is mixed in.- You do NOT add
data-aue-resourceto child field elements — only the block root has it. Field elements getdata-aue-proponly. UE inherits the resource context from the nearest ancestor withdata-aue-resource.
How editor-support.js Works
The boilerplate includes scripts/editor-support.js which runs automatically when the page is loaded inside UE.
What it does:
- Detects the UE environment (checks for the UE JS bridge)
- Reads the AEM content metadata embedded in the page's
<head>by the server - Adds
data-aue-resource,data-aue-type="component", anddata-aue-modelto every block root automatically based on the content tree - Adds
data-aue-type="container"anddata-aue-resourceto every section
This 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
}
Containers: Annotating Collections
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.
The Cards Block Example
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.
Adding Items to Containers
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:
- The block must be declared as a container in
component-filters.json - The contained component type must be in the filter for that block
- The contained item model must exist in
component-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"
}
}
}
}
}
]
}
]
}
Annotating Rich Text In-Context
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:
- Be a block-level element that can contain
<p>,<strong>,<em>etc. - Have
data-aue-type="richtext"(nottext) - Contain the raw rendered HTML (not just text content)
For text type, the author uses the properties panel field. For richtext, they can edit inline or use the panel.
The 7 Mistakes That Break In-Context Editing
Mistake 1: Replacing the block root with innerHTML
// 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.
Mistake 2: Prop name doesn't match model field name
// 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.
Mistake 3: Missing data-aue-type on media elements
// 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.
Mistake 4: Adding data-aue-resource manually with a wrong path
// 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.
Mistake 5: Annotating elements that don't exist in the DOM yet
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');
}
Mistake 6: Using annotations outside of UE context
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.
Mistake 7: Container block missing filter configuration
// 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?
Verifying Annotations Work
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:
- Open the page in UE
- Click on the block element you annotated
- The properties panel should open on the right
- The field corresponding to
data-aue-propshould be visible and editable
If 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.
Complete Annotation Reference
| 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 |
Key Takeaways
- Annotations re-establish the content→DOM link broken during rendering — without them, UE clicking does nothing
data-aue-resourceis the JCR path pointer — set automatically byeditor-support.js, read it fromblock.dataset.aueResourcedata-aue-propis the field pointer — must match the fieldnamein your component model exactlydata-aue-typedeclares the data type — determines which editor UE opens for the element- Never replace the block root element — always manipulate
block.innerHTMLto preserveeditor-support.jsattributes - Add annotations after DOM rendering — annotate elements after
innerHTMLis set - Container blocks need filter config — the "Add item" button requires
component-filters.json+component-definition.jsonsetup - Conditional annotations — use
block.dataset.aueResourceas an UE environment check for zero-overhead on live pages
Next Steps
In 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?
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.