Edge Delivery Services · 12 of 29
Feature comparison between EDS and traditional AEM. Covers Sling Models, editable templates, parsys, multifield, workflow, component inheritance, ClientLibs, OSGi, JCR, and the matching EDS patterns or gaps.
This chapter covers:
This is the chapter I wish existed when I first encountered EDS after years of AEM development.
The most dangerous mistake an AEM developer makes in EDS is trying to replicate AEM patterns. EDS is not a simplified AEM. It is a different paradigm. Some AEM concepts map cleanly. Others have no mapping. A few things EDS does better.
Understanding the difference at an architectural level — not just syntactically — is what this chapter provides.
Before the feature comparison, the mindset:
AEM is server-centric. The CMS assembles the page on the server. Sling resolution, component rendering, template policies, workflow — all happen on the server. The browser receives a finished HTML page.
EDS is edge-client-centric. The edge delivers raw content HTML. The browser executes JavaScript that assembles the final UI. The CMS provides content structure; the browser provides rendering.
AEM: Browser → Server (Sling renders component, applies policy, returns HTML) → Browser renders
EDS: Browser → Edge (returns pre-built HTML) → Browser JS (decorates blocks) → Browser renders
This is not a simplification. It is a fundamentally different architecture. Every "missing feature" in EDS comes from this architectural choice.
A component consists of:
.html) for markup.java) for business logic.xml) for author UIcq:Component node definition for metadataThe component is a server-side unit. The Sling Model runs on the server, queries JCR, applies business logic, and exposes data to HTL.
@Model(adaptables = SlingHttpServletRequest.class)
public class HeroModel {
@ValueMapValue
private String size;
@ValueMapValue
private String title;
@ChildResource
private Resource desktopImage;
public String getSize() {
return StringUtils.defaultIfBlank(size, "tall");
}
}
<!-- hero.html (HTL) -->
<div class="hero hero-${model.size}">
<div class="hero-section">
<h1 class="hero-header">${model.title}</h1>
</div>
</div>
A block (component) consists of:
block.js — JavaScript that transforms DOM (replaces Sling Model + HTL combined)block.css — Scoped styles_block.json — Field definitions for Universal Editor (replaces dialog XML)There is no server-side rendering. The decorate(block) function receives the raw content HTML and transforms it into the final UI entirely in the browser.
// hero.js (replaces both Sling Model and HTL)
export default function decorate(block) {
const props = parseKeyValue(block);
const size = props.size?.textContent.trim().toLowerCase() || 'tall';
block.classList.add(`hero-${size}`);
block.innerHTML = '';
const section = document.createElement('section');
section.className = 'hero-section';
const header = document.createElement('h1');
header.className = 'hero-header';
header.innerHTML = props.title?.innerHTML || '';
section.append(header);
block.append(section);
}
| Aspect | AEM | EDS |
|---|---|---|
| Where logic runs | Server (Java) | Browser (JavaScript) |
| Language | Java | JavaScript |
| Markup | HTL template | DOM API in JS |
| Business logic | Sling Model | Block JS function |
| Data access | JCR via ResourceResolver | DOM parsing (authored content) |
| External API calls | Java HTTP client in Sling Model | fetch() in decorate() |
| Error handling | Server-side exception | try/catch in decorate() |
| Testing | JUnit + AEM Mocks | Browser testing + unit tests |
Architect takeaway: In EDS, the "component" concept splits into two concerns — content structure (authored in UE/DA) and rendering (block JS). There is no server-side processing step between content and rendering.
Sling Models provide a typed, testable layer between JCR content and the presentation layer. They handle:
@Model(adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class CardListModel {
@OSGiService
private QueryBuilder queryBuilder;
@ValueMapValue
private String path;
public List<CardItem> getCards() {
// Query JCR, return typed list
Map<String, String> queryMap = new HashMap<>();
queryMap.put("path", path);
queryMap.put("type", "nt:unstructured");
// ... complex query logic
}
}
There is no equivalent to Sling Models. All data logic lives in the block's decorate() function.
For simple content rendering, this is fine — the authored content IS the data.
For complex data aggregation (query multiple paths, apply business rules), you have three options:
Option 1: Query API (for page metadata lists)
// Instead of JCR query, fetch pre-built JSON index
const response = await fetch('/content/query-index.json');
const { data } = await response.json();
const filtered = data.filter(item => item.category === 'news');
Option 2: External API (for non-content data)
// In AEM: Sling Model calls internal service
// In EDS: decorate() calls external REST API
const response = await fetch('https://api.company.com/products');
const products = await response.json();
Option 3: JSON spreadsheet (for configuration data)
EDS supports Google Sheets or SharePoint spreadsheets as JSON data sources. A spreadsheet at /data/config.xlsx becomes available at /data/config.json. This replaces some use cases for Sling Models that read configuration from JCR.
| Aspect | AEM | EDS |
|---|---|---|
| Business logic location | Java Sling Model | Block JS function |
| Type safety | Java strong typing | JavaScript (use JSDoc for hints) |
| JCR access | ResourceResolver API | Not available |
| OSGi service injection | @OSGiService annotation | Not applicable |
| External API calls | Via injected service | fetch() in block JS |
| Testability | JUnit with AEM Mocks | Browser/unit tests |
| Reusability | Shared Sling Models across components | Import utility functions |
Architect takeaway: If your component has complex server-side logic (JCR queries, OSGi service calls, data transformation), that logic must move to an external API that EDS blocks can call via fetch(). EDS cannot execute Java code.
Editable templates provide:
/conf/mysite/settings/wcm/templates/
├── article-template/
│ ├── structure/
│ │ ├── jcr:content/
│ │ │ ├── root (locked)
│ │ │ │ ├── header (locked, structure)
│ │ │ │ ├── responsivegrid (editable)
│ │ │ │ └── footer (locked, structure)
│ ├── policies/
│ │ ├── headline-styles (h1,h2 only)
│ │ └── text-colors (brand palette only)
There are no editable templates in the AEM sense. The "template" concept in EDS is much simpler.
EDS page structure is fixed by convention:
<header></header>
<main>
<div><!-- section 1 --></div>
<div><!-- section 2 --></div>
</main>
<footer></footer>
The header and footer are always present and always in the same position. This is structurally locked — but not configurable per page type.
Per-page-type behavior is achieved via the template metadata:
| Metadata |
|-------------------|
| Template | article |
In scripts.js, different templates can load different CSS:
const template = getMetadata('template');
if (template) {
document.body.classList.add(`page-${template}`);
loadCSS(`/styles/templates/${template}.css`);
}
Template-level restrictions (what an author can add): In UE, component-filters.json controls which blocks can be added to sections — but this is global, not per template type.
Policies (restricting component options): There is no equivalent to AEM policies in EDS. A block's options (size, color, layout) are all always available to any author on any page. You cannot lock down "only certain themes are allowed on article pages" the way you can with AEM template policies.
| Aspect | AEM | EDS |
|---|---|---|
| Template structure locking | Yes (structure components) | No |
| Per-template allowed components | Yes (policies) | No — global only |
| Design policies (color/font restrictions) | Yes | No |
| Template inheritance | Yes | No |
| Initial content (pre-populated) | Yes | No |
| Page type differentiation | Templates | Metadata template field |
| Author freedom control | High (per template/policy) | Low (global filters only) |
Architect takeaway: EDS gives authors more freedom and developers less control over author behavior. If your project requires strict brand governance with enforcement (not just guidelines), EDS requires supplementary governance tooling. If your authors are trusted and brand-mature, EDS works well.
AEM's Sling Resource Merger provides a powerful inheritance system:
sling:resourceSuperTypewcm/foundation/components/text (parent)
└─ mysite/components/text (child - overrides only the dialog)
└─ mysite/components/article-text (grandchild - overrides only the style)
Overlaying is done via /apps/ — placing a file at the same path in /apps/ as an existing /libs/ file overrides that file without modifying the original.
There is no inheritance system. There is no overlay mechanism.
Each block is a standalone unit. If two blocks share 80% of their code, you have two choices:
Option 1: Shared utility module
// blocks/shared/block-utils.js
export function parseKeyValue(block, knownFields) {
const props = {};
[...block.querySelectorAll(':scope > div')].forEach((row) => {
const [keyCell, valCell] = [...row.querySelectorAll(':scope > div')];
if (keyCell && valCell) {
const key = keyCell.textContent.trim().toLowerCase().replace(/[\s-]/g, '');
if (knownFields.has(key)) props[key] = valCell;
}
});
return props;
}
// blocks/hero/hero.js
import { parseKeyValue } from '../shared/block-utils.js';
export default function decorate(block) {
const props = parseKeyValue(block, new Set(['title', 'size', 'image']));
// ...
}
Option 2: Block variants instead of separate blocks
Instead of creating hero-article as a separate block extending hero, add an article variant to the existing hero block:
// hero.js handles all variants
const layout = props.layout?.textContent.trim().toLowerCase() || 'default';
block.classList.add(`hero-${layout}`);
/* hero.css handles all variants */
.hero.hero-article { /* article variant styles */ }
.hero.hero-landing { /* landing page variant styles */ }
| Aspect | AEM | EDS |
|---|---|---|
| Component extension | sling:resourceSuperType | Copy/paste + utility functions |
| Overlay | /apps/ overlay of /libs/ | No system — fork or shared module |
| Core Components extension | Standard pattern | No equivalent |
| Inheritance depth | Unlimited (A → B → C) | No inheritance |
| Override granularity | Field level, HTL section level | Full rewrite or utility extraction |
| Maintenance of shared logic | Single parent, N children updated automatically | N copies to update |
Architect takeaway: This is a genuine EDS limitation for large component libraries. Without inheritance, shared logic must be maintained in utility modules and consciously kept in sync across blocks. For a small block library (10-15 blocks), this is manageable. For a large enterprise library (50+ blocks), it requires discipline.
The Layout Container (wcm/foundation/components/responsivegrid) is the core of AEM's WYSIWYG authoring model:
This is the defining feature of AEM's page authoring model.
EDS with Universal Editor does support adding blocks to a page and reordering them. Authors can:
What is different:
| AEM Layout Container | EDS UE Section |
|---|---|
| Nested containers (any depth) | Two levels: section > blocks |
| Per-container allowed components | Global allowed components (component-filters.json) |
| Responsive grid with column spans | No built-in grid — handled by block CSS |
| Multiple containers with independent configs | Multiple sections, same global config |
| Inline editing (click text to edit) | Properties panel (sidebar) editing |
| Real-time collaborative editing | Single-user editing |
The section concept in EDS:
The <main> element contains sections. Each section is a <div>. Blocks are placed within sections. You cannot place a block outside a section. You cannot nest sections.
<main>
<div> <!-- Section 1 -->
<div class="hero block">...</div>
</div>
<div> <!-- Section 2 -->
<div class="cards block">...</div>
<div class="cta block">...</div>
</div>
</main>
DA does not have drag-and-drop at all. Authors order content by position in the document (earlier in the document = earlier on the page).
Architect takeaway: If visual drag-and-drop authoring with per-container component restrictions is a hard requirement, EDS UE comes close but with less granularity than AEM. For DA-authored sites, the drag-and-drop authoring model does not exist.
The Granite UI Multifield allows authors to add N instances of a field or field group:
<listItems
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="List Items">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./listItems" />
</listItems>
Authors can add as many items as they need. Each item is stored as a separate child node in JCR.
EDS UE implements the multifield concept via child items — the "+" button that allows adding multiple instances of a sub-block:
{
"title": "Cards",
"id": "cards",
"plugins": {
"xwalk": {
"page": {
"resourceType": "core/franklin/components/block/v1/block",
"template": {
"name": "cards",
"model": "cards",
"filter": "cards-item"
}
}
}
}
}
The filter reference tells UE: the "+" button inside this block adds a cards-item child block.
{
"id": "cards-item",
"components": ["cards-item"]
}
Authors click "+" to add new cards. Each card has its own fields defined in the cards-item model.
The constraint: filter (child items) cannot be combined with key-value: true. See Chapter 5 for the detailed explanation.
In DA, "multifield" is just multiple rows in the block table:
| Cards |
|-------------------------------------|
| Image 1 | Title 1 | Description 1 |
| Image 2 | Title 2 | Description 2 |
| Image 3 | Title 3 | Description 3 |
Each row is one item. Authors add as many rows as needed.
| Aspect | AEM | EDS UE | EDS DA |
|---|---|---|---|
| Dynamic add/remove | Multifield UI | "+" button (filter) | Add/remove table rows |
| Nested fields | NestedMultifield | Child block model | Nested table structure |
| Reordering | Drag handle in multifield | Drag in UE | Move rows in document |
| Max items enforcement | Via validation | Not supported | Not supported |
| Conditional fields per item | Via Granite showHide | Via JSONLogic conditions | Not supported |
AEM Workflow is a full workflow engine:
/var/workflow/models/)Workflows are triggered automatically (on content creation, activation) or manually.
There is no workflow engine in EDS. The content lifecycle has two states: Draft (in AEM author/DA) and Published (on .aem.live).
The Sidekick provides:
.aem.page (staging).aem.live (live)There is no multi-step approval workflow. If the business requires "Author submits → Manager approves → Legal reviews → Publishes", EDS cannot enforce this natively.
Workarounds:
AEM Workflow (for xwalk content): Since xwalk content lives in AEM JCR, standard AEM workflows can be used. The content goes through AEM workflow before being made available for preview. This is not a pure EDS workaround — it uses the AEM layer that xwalk shares with traditional AEM.
External review tools: Use a third-party review tool (e.g., Frame.io, external Jira-based review workflow) that integrates with a custom Sidekick plugin to trigger publish only after approval.
Sidekick custom plugins: Build a custom Sidekick plugin that intercepts the Preview action and requires approval through an external system before allowing preview/publish.
Branch-based workflow: Use GitHub branch strategy (feature branch → PR → review → merge to main triggers publish). This is a developer-centric workaround.
Architect takeaway: For organizations that require enforced multi-step content approval (legal, regulated industries), the absence of a native workflow engine in EDS is a significant gap. The xwalk path (via AEM JCR) partially addresses this by allowing AEM's workflow engine to be used.
AEM's ClientLibrary system provides:
dependencies and embed properties automatically load required libraries/apps/ with the same path as /libs/ to override it<!-- .content.xml for a ClientLib -->
<jcr:root
jcr:primaryType="cq:ClientLibraryFolder"
categories="[mysite.base, mysite.hero]"
dependencies="[core.wcm.components.commons.react.base]"
embed="[mysite.vendor.jquery]" />
The embed property inlines another library's code. The dependencies property ensures another library loads first but remains separate.
There is no ClientLib system, no overlay system, and no build-time CSS/JS concatenation.
Direct equivalents:
| AEM ClientLib Concept | EDS Equivalent |
|---|---|
categories (what the lib provides) | Block folder name / CSS class |
dependencies (requires this first) | ES Module import statement |
embed (inline this code) | Copy source, or NPM import and bundle |
Overlay /apps/ over /libs/ | No equivalent — fork or contribute upstream |
| Maven dependency for ClientLib | NPM package (if build step added) |
| LESS/SCSS compilation | CSS custom properties (no preprocessing) |
| Category-based inclusion per template | Template metadata + conditional CSS load |
Cross-repo sharing in EDS:
There is no built-in mechanism for one EDS project to include components from another EDS project the way AEM's ClientLib dependencies (across Maven artifacts) enables it.
Practical options:
Multi-theme in single repo:
AEM can handle multiple brands in a single repository via overlays (brand A overrides /apps/mysite/brand-a/ over /libs/base/).
EDS handles this via:
/themes/
├── brand-a/
│ ├── brand-a.css ← Brand-specific CSS variables
│ └── brand-a.js ← Brand-specific logic (rarely needed)
└── brand-b/
└── brand-b.css
In scripts.js, load the appropriate theme CSS based on metadata or URL path:
const brand = new URL(window.location).hostname.split('.')[0];
if (brand !== 'www') {
loadCSS(`/themes/${brand}/${brand}.css`);
}
Theme CSS files override the global CSS custom properties defined in styles.css:
/* themes/brand-b/brand-b.css */
:root {
--color-primary: #0052cc; /* Override brand-a's yellow */
--font-family-sans: 'Inter', sans-serif;
}
This is less powerful than AEM's overlay system (you cannot override JS logic per brand without copying block files), but sufficient for visual theming.
The AEM Dispatcher is a reverse proxy and cache layer:
Dispatcher configuration is complex (Apache httpd.conf, dispatcher.any files) and requires detailed knowledge of rewrite rules and cache invalidation strategies.
There is no Dispatcher. The EDS Edge Network (Adobe's CDN infrastructure) handles:
Cache invalidation in EDS: When an author clicks Publish, the EDS CDN automatically purges the cache for that page. No manual flush agent required.
No custom Apache rules. You cannot write arbitrary rewrite rules or add server-side logic at the Dispatcher layer. If you need URL rewriting or complex routing, that must happen at a higher-level CDN configuration (via Adobe Managed CDN rules, or a Fastly/Cloudflare layer in front of EDS).
Architect takeaway: Dispatcher is significantly simplified in EDS — mostly automatic. But this means you lose the ability to write custom cache-busting logic, complex rewrite rules, and fine-grained authentication filtering at the edge. These must be handled elsewhere in your architecture.
OSGi provides a modular service container:
@Component and discovered via @Reference@Scheduler, @Component + Runnable).cfg.json files)@EventHandler, ResourceChangeListener)This enables background processing, integration services, and complex application logic running continuously on the server.
There is no OSGi, no server-side services, no schedulers.
Workarounds:
| AEM OSGi Use Case | EDS Approach |
|---|---|
| Scheduled content sync | GitHub Actions workflow on schedule |
| External API polling | AWS Lambda / Azure Function (external microservice) |
| Event-driven cache invalidation | Webhook → GitHub Action → EDS publish API |
| Background image processing | AEM Assets pipeline (if using UE/xwalk) |
| Email sending on form submission | Form action to external service (Formspree, Adobe Forms) |
Architect takeaway: Any server-side background processing must be handled by external systems (cloud functions, GitHub Actions, third-party services). EDS is purely a delivery mechanism.
| AEM Feature | EDS Equivalent | Gap Level |
|---|---|---|
| HTL + Sling Model | Block JS + DOM API | Medium — different paradigm, same capability |
| Editable Templates | Template metadata + conditional CSS | High — no structural locking or per-template policies |
| Component Inheritance | Utility modules + copy | High — no inheritance system |
| Parsys / Layout Container | UE sections + block drag-drop | Medium — less granular |
| Multifield | Child items (UE) / table rows (DA) | Low — equivalent capability |
| Workflow | AEM Workflow (xwalk only) / Sidekick plugins | High — no native multi-step workflow in pure EDS |
| ClientLib categories | Block CSS files (auto-loaded per block) | Low — simpler but less flexible |
| ClientLib dependencies | ES Module imports | Low — equivalent |
| ClientLib embed | Copy source / NPM build step | Medium — no runtime embed |
| Overlay (/apps/ over /libs/) | Fork / shared module | High — no overlay system |
| Cross-repo dependencies | NPM / submodules | Medium — possible but no native support |
| Multi-theme single repo | CSS custom property themes | Low — works well for visual theming |
| Dispatcher caching | EDS CDN (automatic) | Low — simpler |
| Dispatcher rules | No equivalent | High — cannot write Apache rules |
| OSGi services | External microservices | High — no server-side services |
| Schedulers | GitHub Actions / cloud functions | Medium — requires external system |
| JCR queries | Query API (JSON index) | High — limited to indexed metadata |
| AEM Forms | Not available | High — no native forms engine |
| Translation workflows | Not available | High — no native translation management |
| MSM / Live Copy | Folder structure only | High — no inheritance or live sync |
| Content Fragments | JSON sheets / Query API | High — no structured content model |
| Experience Fragments | Fragment block | Low — equivalent |
| DAM metadata schemas | Not available | High — no DAM schema enforcement |
In the next chapter, we examine the architecture question that most AEM developers ask: Where does content actually go in EDS?
We will cover the content storage architecture: JCR in AEM, da.live content bus in EDS, how the preview/publish pipeline works, where assets are stored, and the lifecycle from authoring to delivery.
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.