N
Naveenr.dev
Chapter 11
26 min read2026-06-30

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) and data-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-type values 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.js adds 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.js adds 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:

TypeDescriptionWhen to use
textPlain text contentShort strings, titles, labels
richtextHTML rich textParagraphs, formatted content
mediaImage or video<img>, <video>, <picture>
referenceLink to another content nodeContent Fragments, Experience Fragments
booleanTrue/false toggleCheckboxes, yes/no fields
selectEnumerated valueDropdown choices
containerContains child itemsSections, multifields, collections
componentA component nodeIndividual 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.json model 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 label in component-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 id in your component-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:

  1. Click event → UE traverses up the DOM to find the nearest data-aue-resource
  2. Resource found → UE knows the JCR path of the content node
  3. Prop found → UE knows which field on that node to open
  4. Model found → UE knows which fields to show in the properties panel
  5. 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:

  1. block.dataset.aueResourceeditor-support.js sets data-aue-resource on the block root before decorate() runs. You read it from the existing element, not generate it yourself.
  2. 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.
  3. You do NOT add 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.

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:

  1. Detects the UE environment (checks for the UE JS bridge)
  2. Reads the AEM content metadata embedded in the page's <head> by the server
  3. Adds data-aue-resource, data-aue-type="component", and data-aue-model to every block root automatically based on the content tree
  4. Adds data-aue-type="container" and data-aue-resource to 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:

  1. The block must be declared as a container in component-filters.json
  2. The contained component type must be in the filter for that block
  3. 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:

  1. Be a block-level element that can contain <p>, <strong>, <em> etc.
  2. Have data-aue-type="richtext" (not text)
  3. 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:

  1. component-filters.json — does a filter exist for this block? Does it list the allowed child component IDs?
  2. component-definition.json — does the block's template reference the filter?
  3. 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:

  1. Open the page in UE
  2. Click on the block element you annotated
  3. The properties panel should open on the right
  4. The field corresponding to data-aue-prop should 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

AttributeRequiredScopeValue
data-aue-resourceYesBlock root, container itemsurn:aem:/content/... (set by editor-support.js)
data-aue-typeYesAny annotated elementtext, richtext, media, reference, boolean, select, container, component
data-aue-propYes (for fields)Field elementsMust match name in component model
data-aue-labelRecommendedAny annotated elementHuman-readable display name
data-aue-modelYes (for components)Component rootsMust 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-resource is the JCR path pointer — set automatically by editor-support.js, read it from block.dataset.aueResource
  • data-aue-prop is the field pointer — must match the field name in your component model exactly
  • data-aue-type declares the data type — determines which editor UE opens for the element
  • Never replace the block root element — always manipulate block.innerHTML to preserve editor-support.js attributes
  • Add annotations after DOM rendering — annotate elements after innerHTML is set
  • Container blocks need filter config — the "Add item" button requires component-filters.json + component-definition.json setup
  • Conditional annotations — use block.dataset.aueResource as 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.