> For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt.

# Resource Resolution and the Sling Resource Merger

## 23 – Resource Resolution and the Sling Resource Merger

> **Target:** 3–4 years experienced AEM Developer **Companies:** Valtech, Publicis Sapient, Deloitte Digital, Cognizant, Accenture, TCS, Capgemini, Infosys, IBM, LTIMindtree **Project domain:** a global energy technology company's marketing site.

***

## Before we start — why this topic separates the developers from the support engineers

File 01 gave you the headline version of resource resolution: URL → Resource → Resource Type → Script, and a one-line mention that `/apps` beats `/libs`. That was enough to survive the architecture round.

This file is what happens when the interviewer doesn't stop there.

Every mid-to-senior AEM interview eventually asks some version of _"how do you add one field to a component's dialog without copying the whole thing?"_ or _"why did our short URLs suddenly start rendering `/content` in every link?"_ Both questions live entirely inside the machinery this file covers: the `ResourceResolver` object itself, the exact difference between `resolve()` and `getResource()`, how overlay, inheritance and override are three genuinely different mechanisms and not three names for the same trick, how the Sling Resource Merger actually stitches two resource trees together, and how `/etc/map` and `resourceResolver.map()` work together — not separately — to give you clean URLs.

Here is why this matters for you specifically. Coming from support, you have almost certainly _used_ the symptoms of this machinery without knowing the mechanism: a ticket where a page 404'd anonymously but worked for a logged-in author, a ticket where "the field doesn't save," a ticket where "links are broken after the URL shortening went live." Every one of those is this file. Learning the mechanism turns four separate memorised incidents into one understood system — and that is exactly the kind of answer that makes an interviewer sit up.

***

## 1. Introduction

### 1.1 What resource resolution actually is, and why it deserves its own file

In file 01 you learned that Sling turns a URL into a resource, and a resource into a script. The object that does all of that work — every single step — is one Java object: the **`ResourceResolver`**.

Almost everything you write in AEM eventually touches this object, directly or through something built on top of it. A Sling Model's `@ValueMapValue` reads from it. A servlet's `request.getResourceResolver()` is it. A scheduled job that needs to read content has to obtain one. HTL's `data-sly-resource` asks it for a resource to include. If you don't understand what this object is and whose permissions it carries, half of AEM's stranger behaviours — the field that "doesn't save," the page that 404s for one user and not another, the servlet that silently stops working — will look like bugs in the platform rather than what they actually are: this object behaving exactly as designed.

### 1.2 The one sentence to memorise before anything else

> **A `ResourceResolver` is a session into the repository, and it always carries the permissions of whichever user it was opened for.**

That is the entire idea, and everything in this file is a consequence of it. File 13 covers users, groups and ACLs in depth — this file is where those permissions actually get _enforced_, at the moment a path is turned into a resource.

If the resolver belongs to an anonymous visitor, every `getResource()` call it makes is filtered by what anonymous can read. If it belongs to a service user with narrow grants, it is filtered by that. If it belongs to an admin, almost nothing is filtered — which is exactly why file 13 tells you never to reach for `getAdministrativeResourceResolver()` out of convenience.

### 1.3 When you reach for this API directly

Most of the time you don't — Sling Models and HTL hide the `ResourceResolver` behind `@ValueMapValue` and `data-sly-resource`. You reach for it directly when:

- You are writing a **servlet** and need to read a resource other than the one the request resolved to.
- You need a **service user** resolver to read content the current visitor cannot see (file 13, and again in section 2.4 here).
- You are generating a **link** in Java and need the short, public-facing form of an internal path — `map()`.
- You are debugging _why_ a path didn't resolve the way you expected.

### 1.4 A real project example to adapt

> "On our energy site, most of our resource resolution is invisible — Sling Models and HTL do it for us. Where we touch the `ResourceResolver` directly is in three places: our Load More servlet, which reads sibling product pages under the current listing; a scheduled job that reads pricing data through a service user because the page tree it walks is partially access-controlled; and a link-building utility that calls `map()` so PDFs generated for download always contain public, shortened URLs rather than internal `/content` paths. Getting the third one wrong — forgetting the outbound half of URL mapping — is exactly the incident I'd bring up if asked for a project story here."

That paragraph does three things: shows you understand where the API is actually used, name-drops a service-user story (file 13's territory), and pre-loads the URL-shortening story from section 7.

***

## 2. Core Concepts

### 2.1 The ResourceResolver API — the methods you're actually asked about

Let's go through the API surface method by method, because interviewers frequently hand you two of these side by side and ask you to explain the difference.

**`getResource(String path)`** — the plain, honest lookup. Give it a path, it gives you back a `Resource` if one exists there, or **`null`** if it doesn't. No mapping is applied. No 404 page is generated. It is the JCR equivalent of asking "is anyone home at this exact address?"

There is an overload, `getResource(Resource base, String relativePath)`, which resolves relative to a resource you already have — useful when walking a tree without rebuilding absolute paths by hand.

**`resolve(...)`** — covered in full in 2.2, because this is the one that trips people up.

**`map(...)`** — the outbound counterpart. Give it an internal repository path (and optionally the current request), and it gives you back the externally usable URL, applying whatever `/etc/map` rules or vanity paths apply. Section 2.10 onward covers this in depth.

**`adaptTo(Class<T> type)`** — not specific to `ResourceResolver`, but used constantly through it. Most commonly you'll see `resolver.adaptTo(Session.class)` to drop down to the raw JCR API when you need something the Sling Resource API doesn't expose, like a `Session` -level move or a workspace-level operation.

**`listChildren(Resource resource)`** — returns an `Iterator<Resource>` of the resource's direct children. This is what a Sling Model uses under the hood when you inject `List<Resource> children` — but knowing you can call it directly matters when you're walking a tree in a servlet or a service.

**`findResources(String query, String language)`** — runs a query (JCR-SQL2 or XPath) through the resolver and returns matching resources as an iterator. This is the lower-level cousin of QueryBuilder from file 01 — QueryBuilder is friendlier for building dynamic predicates, `findResources` is what you reach for when you already have a query string and don't want QueryBuilder's ceremony. The same indexing rules from file 01 apply: an unindexed query traverses and can hang the instance.

**`getSearchPath()`** — returns the configured search path as a `String[]`, by default `{"/apps/", "/libs/"}`. This is the array Sling walks, in order, whenever it needs to turn a _relative_ resource type into an absolute script path. Knowing this method exists is what lets you answer "how would you check the search path on this instance without reading OSGi config by hand?" — you'd call this, or check it from a script console.

**`getUserID()`** — returns the ID of the user this resolver is authenticated as. The fastest way to prove, in a debugging session, that you are looking at the resolver you think you are.

**`close()`** — releases the underlying session. Section 2.4 covers the rule that decides everything about this method: close what you opened, never close what you were handed.

### 2.2 resolve() versus getResource() — the distinction almost everyone gets wrong

This is the single most commonly mis-answered question in this whole file, so slow down here.

Both methods take a path and give you back a `Resource`. That surface similarity is exactly why people assume they're interchangeable. They are not, and the difference matters in production.

**`getResource(path)`** does a direct, literal lookup. No URL mapping. No selector or extension stripping. No search-path fallback for a relative resource type on the node itself. If nothing exists at that exact path, you get **`null`**.

**`resolve(request, path)`** — usually called as `request.getResourceResolver().resolve(request)` inside Sling's own request-handling code, or as `resolver.resolve(path)` in your own code — is the _full_ pipeline. It:

1. Applies `/etc/map` inbound rewriting, so a public-facing path gets translated to its internal repository path first.
2. Strips selectors, extension and suffix from the incoming path before attempting to find the underlying resource.
3. If, after all of that, nothing exists — it does **not** return `null`. It returns a special object called a **`NonExistingResource`**.

**Say that last part out loud, because it's the part that gets tested:**

> `getResource()` on a missing path gives you `null`. `resolve()` on a missing path never gives you `null` — it gives you a `NonExistingResource`, which is a real, non-null `Resource` object whose resource type is set to a fixed marker value.

**Why does Sling bother with this instead of just returning `null` in both cases?**

Because `resolve()` is what runs at the very top of the request-processing pipeline — before Sling even knows whether this is going to be a 404. If `resolve()` returned `null`, every single piece of downstream code that expects a `Resource` object would need a null check just to handle "the page doesn't exist," and Sling's own script-resolution machinery (URL → Resource → Resource Type → Script from file 01) would break, because it needs a resource type to look up a script — even the 404 page is rendered _through_ the same resourceType-to-script pipeline as everything else.

By giving you a `NonExistingResource` with resource type `sling:nonexisting`, Sling lets the _same_ machinery that finds `page.html` for a real page also find a custom 404 script — placed at `/apps/sling/nonexisting` — for a resource that doesn't exist. One pipeline handles both cases.

**The practical consequence for your own code:**

```java
// getResource -- direct, literal, can be null
Resource r1 = resolver.getResource("/content/energy/us/en/products/does-not-exist");
if (r1 == null) {
    // this branch is reachable
}

// resolve -- full pipeline, NEVER null, but can be a NonExistingResource
Resource r2 = resolver.resolve(request, "/content/energy/us/en/products/does-not-exist");
if (r2 == null) {
    // THIS BRANCH IS DEAD CODE. resolve() never returns null.
}
if (ResourceUtil.isNonExistingResource(r2)) {
    // this is the check you actually need
}
```

**The interview answer:**

> " `getResource()` is a direct lookup — you give it a path, and you get either the resource or `null`. It doesn't apply any URL mapping. `resolve()` is the full pipeline Sling uses to turn an incoming request path into a resource: it applies `/etc/map` inbound rewriting first, strips selectors and extension, and — this is the part people miss — it never returns `null`. If nothing matches, it gives you back a `NonExistingResource`, which is a real object with a fixed resource type, `sling:nonexisting`. Sling does that so the same URL-to-script pipeline that renders every other page can also render a custom 404 page, just by placing a script at that resource type. So if I write `if (resolver.resolve(path) == null)`, that branch is dead code — I need to check `ResourceUtil.isNonExistingResource()` instead."

### 2.3 NonExistingResource and SyntheticResource

Two "fake" resource types come up in interviews together, and it's worth being precise about what each is for.

**`NonExistingResource`** — already covered above. It exists specifically to represent "nothing is here" as a real `Resource` object, with resource type `sling:nonexisting`, so that 404 handling can go through the normal script-resolution pipeline instead of needing a special case.

```text
/apps/sling/nonexisting/sling.html    ← your custom 404 page, matched by resource type
```

**`SyntheticResource`** — a `Resource` object that is not backed by an actual repository node at all. You can construct one yourself with `new SyntheticResource(resolver, path, resourceType)` whenever you need to hand something through the Resource API that has no real content behind it — a computed row in an aggregated listing, a fixture in a unit test, or a placeholder Sling itself needs internally when a path exists conceptually (an intermediate segment on the way to a real resource) but has no corresponding node.

**The one-line distinction:** a `NonExistingResource` specifically means _"resolution failed, here's a stand-in so the pipeline keeps working."_ A `SyntheticResource` more broadly means _"here's a Resource with no repository node behind it,"_ and you can create those yourself for your own purposes, whereas you would never construct a `NonExistingResource` by hand — Sling gives you one.

### 2.4 Closing rules — the one habit that prevents a production incident

This continues directly from files 07 and 13, and it is worth restating here because this file is where the rule actually lives.

**The rule, in one sentence:** _close a resolver only if you opened it yourself._

`request.getResourceResolver()` in a servlet, or the resolver backing a Sling Model's injected fields, belongs to Sling. Sling opened it when the request came in, and Sling will close it when the request finishes. If you call `.close()` on it yourself, you don't get an error immediately — you get everything _after_ that call in the same request failing, because the JCR session backing it is now gone.

A resolver you obtain from `resourceResolverFactory.getServiceResourceResolver(...)` (file 13's service-user pattern) is different: **you** opened it, so **you** must close it — always in try-with-resources, never in a `finally` block you might forget to write correctly.

```java
// You opened this one. You close it. Always try-with-resources.
try (ResourceResolver serviceResolver =
        resolverFactory.getServiceResourceResolver(authInfo)) {

    Resource priced = serviceResolver.getResource("/content/energy/pricing/2026");
    // ... use it while the resolver is open ...

}   // closed automatically, even if an exception was thrown above
```

### 2.5 What a leaked resolver actually looks like in production

This is worth walking through slowly, because "session leak" is an abstract phrase until you've seen what it does.

Every `ResourceResolver` you open from a factory holds open a real JCR `Session` underneath it. A `Session` is not free — it holds memory, and in a clustered Oak setup it can hold onto resources that the garbage collector cannot reclaim, because something still technically has a reference to it: your unclosed resolver.

If a piece of code — say, a scheduled job or a servlet that runs on every request — opens a service-user resolver and forgets to close it, here is the timeline you actually see:

1. **Day one:** nothing looks wrong. The instance has plenty of headroom.
2. **Over days or weeks:** heap usage on that instance creeps upward in a way that doesn't correlate with traffic. It goes up and never comes back down after a GC cycle, because live references are pinning those sessions.
3. **Eventually:** the instance starts throwing `OutOfMemoryError`, or session-related warnings appear in the logs about too many open sessions, and a restart "fixes" it — until the leak accumulates again.

**Why this is hard to diagnose the first time you see it:** the symptom (an instance slowly dying) looks nothing like the cause (one missing `.close()` in one servlet). Nobody's first instinct is "check for unclosed resolvers" when heap usage is climbing — they look at traffic, at cache sizes, at the DAM. The diagnostic order that actually gets you there fast is in section 11.

### 2.6 The search path and how a resource type becomes a script — recap and one layer deeper

File 01 gave you the eight-step script resolution order ending in " `/apps` before `/libs`." Here is the one thing worth adding on top of that, because it's what interviewers probe for when they think you've only memorised the list.

**The search path is configuration, not a hardcoded constant.** It comes from the Sling Resource Resolver Factory's OSGi configuration — the same place service-user mappings live (file 13) — and while `/apps` then `/libs` is the standard order almost every project keeps, it is technically a list you _could_ reorder or extend. You will essentially never see a project that changes it, but knowing it's configuration rather than a law of physics is the difference between reciting a fact and understanding a mechanism.

**`sling:resourceType` versus `sling:resourceSuperType` — the one-sentence distinction that matters here:**

- `sling:resourceType` says **what this resource is**. It's the starting point of script resolution.
- `sling:resourceSuperType` says **what to fall back to** if a script isn't found at the resource type's own path. It's how one component can inherit another's rendering and dialog without being a copy of it.

This second one is the mechanism behind proxy components (file 02) and it is also, as you'll see in section 2.9, one of the two ways the Resource Merger stitches content together — not just the `/apps` -over- `/libs` search path, but _also_ the `resourceSuperType` chain.

### 2.7 Overlay, Inheritance, and Override — the comparison interviewers actually ask for

These three words get used loosely in everyday project conversation, and that looseness is exactly what an interviewer is testing when they ask you to tell them apart. Treat this as three genuinely different mechanisms that happen to solve related problems.

**Overlay.** You place a resource at the **same relative path** under `/apps` as one that exists under `/libs`. Because the search path checks `/apps` first, your version is found first and effectively hides Adobe's. Nothing about the original at `/libs` changes — it's still there, just never reached, because `/apps` won the race.

This is a **path-based, global** mechanism. It has no concept of "just this one component" — if two completely unrelated things happen to resolve through the same relative path, overlaying it affects both. That's what makes it powerful and also the riskiest of the three.

**Inheritance.** A resource declares `sling:resourceSuperType`, pointing at a different resource type entirely — usually a Core Component, or a shared base component within your own project. If a script or dialog isn't found at your resource's own path, resolution falls through to the resource type named in `resourceSuperType`, and keeps falling through the chain until something answers or the chain ends.

This is a **scoped, deliberate** relationship — you are explicitly opting one component into another's behaviour. It has nothing to do with `/apps` versus `/libs`; you can inherit from a component that lives entirely under `/apps`, as proxy components do (file 02).

**Override.** Complete replacement, with no fallback and nothing inherited. You aren't extending or shadowing something — you're saying "ignore whatever else exists at any lower-priority location; use exactly and only what I've provided here." Section 2.9 covers the concrete mechanism the Resource Merger gives you for this: the `/mnt/override` mount, as distinct from the merging behaviour of `/mnt/overlay`.

**The comparison, side by side:**

|                                 | Overlay                                                                | Inheritance                                                | Override                                                     |
| ------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------ |
| Mechanism                       | Same relative path, `/apps` before `/libs`                             | `sling:resourceSuperType` chain                            | Complete replacement, no merge                               |
| Scope                           | **Global** — anything resolving to that path                           | **Scoped** — only components that opt in                   | Scoped to the resource declaring it                          |
| What survives from the original | Nothing reachable, but the original still exists at `/libs`            | Whatever you don't explicitly redefine                     | Nothing — that's the point                                   |
| Risk profile                    | **Higher** — you can't tell everything that resolves through that path | Lower — you know exactly what opted in                     | Depends entirely on what you left out                        |
| Typical use                     | Customising the AEM authoring UI itself (a console, a classic dialog)  | Extending a Core Component or a proxy                      | Replacing an entire dialog or script wholesale, deliberately |
| Upgrade safety                  | You silently stop receiving Adobe's fixes to that exact path           | You keep receiving fixes to everything you didn't override | You keep nothing from upstream at all                        |

**Why overlay is riskier, stated plainly:** when you overlay `/libs/foo/bar` with `/apps/foo/bar`, you have frozen your copy in time. The next time Adobe patches or improves `/libs/foo/bar`, you don't get it — your `/apps` copy still wins the search path, and it's now stale relative to the product. This is exactly the same risk file 01 warned about for editing `/libs` directly, except overlay doesn't corrupt the original — it just silently shadows improvements to it. Nobody notices until a security patch to that exact path ships and doesn't take effect on your instance.

**Why inheritance is safer, stated plainly:** because you only redefine the specific pieces you touch — one new dialog field, one changed HTL block — everything else keeps flowing through to the real component. When Adobe ships a Core Component update, your proxy still inherits all of it automatically; you only "own" the small delta you actually wrote.

**The interview answer:**

> "They solve related problems but they're mechanically different. Overlay is path-based — same relative location under `/apps` as under `/libs`, and the search path means yours wins. It's global, which is also its risk: you can't always tell everything that resolves through that path, and you silently stop receiving Adobe's improvements to it. Inheritance is `sling:resourceSuperType` — a deliberate, scoped relationship where a component opts into extending another one, and it's how proxy components and Core Component customisation work; you keep receiving upstream improvements to everything you didn't specifically redefine. Override is the least common of the three — a complete replacement with no fallback at all, which the Resource Merger gives you explicitly through `/mnt/override` as distinct from the merging behaviour of `/mnt/overlay`. I default to inheritance, reach for overlay only to customise the authoring UI itself, and use override rarely and deliberately."

### 2.8 The Sling Resource Merger — the problem it actually solves

Here is the scenario that makes the Resource Merger necessary, told the way it actually happens on a project.

You are three weeks into the energy site build. A stakeholder asks for one extra field on the page properties dialog — a "Regulatory Region" dropdown, because pages need to be tagged for compliance reasons that have nothing to do with anything Adobe anticipated. The out-of-the-box page properties dialog has a dozen tabs and does dozens of things correctly already — SEO metadata, social sharing, page thumbnails, cloud configuration references. You need exactly one more field.

**The wrong way to do this** — and the way almost every AEM developer tries first — is to copy the entire OOTB dialog node from `/libs` down to `/apps`, then add your one field to the copy. It works. It also means:

- You now own a full duplicate of Adobe's dialog structure, frozen at whatever version you copied it from.
- Every future Adobe fix, every new field Adobe adds to page properties in a later service pack or Cloud Service release, never reaches you — your copy doesn't know it exists.
- Six months later, nobody remembers this copy was made for one field, and a code reviewer has no way to tell "this diverged from Adobe's version deliberately" from "this diverged from Adobe's version because someone forgot to update it."

**The Resource Merger exists specifically to avoid this.** Instead of copying the whole dialog, you create a resource under `/apps` at the relevant path that contains **only** the new field, and the Resource Merger combines — merges — your small addition with Adobe's full dialog at read time, every time it's rendered. You never own more than the delta you actually wanted.

**Why this is worth stating as the "why" before the "how":** an interviewer who asks about the Resource Merger is really asking "do you understand the cost of copying product code, and do you know the alternative?" Leading with the cost of copying, the way this section just did, answers the _reason_ the mechanism exists before you even describe the mechanism itself.

### 2.9 The two mount points — /mnt/overlay and /mnt/override

This is the part of the Resource Merger that most candidates who "know the name" cannot actually explain, and it's where you can visibly separate yourself from someone who has only heard the term.

The Resource Merger produces its combined view of the world at two special, virtual mount points. Neither of these is something you write content into directly — you keep writing your actual content under `/apps`, at the same relative path as the thing you're extending, and the Resource Merger computes the merged (or overridden) view on the fly whenever something reads through one of these paths.

**`/mnt/overlay/<path>`** — the **merging** view. For every resource in the search path (by default `/apps` and `/libs`, in priority order), the Resource Merger combines them into one logical resource:

- Properties present in the higher-priority location (`/apps`) win over the same property name from the lower-priority one (`/libs`).
- Properties that exist **only** at the lower-priority location are still present in the merged result — nothing is lost by default.
- Child resources are combined the same way, recursively — your one new child node under `/apps` appears alongside all of Adobe's existing children from `/libs`.

This is the mount point behind almost everything people mean when they casually say "the Resource Merger" — it's how AEM computes the effective page properties dialog, and how it computes the effective dialog for a component when a proxy adds a field on top of a Core Component it inherits from.

**`/mnt/override/<path>`** — the **overriding** view. Instead of combining everything found in the search path, this mount point picks up **only** the single highest-priority resource that exists, and uses it exactly as-is — no merging with anything lower in the search path at all. If `/apps/foo/bar` exists, that is the entire answer; `/libs/foo/bar` is never consulted, not even for properties `/apps/foo/bar` doesn't define.

**The plain-English distinction, worth having ready:**

> " `/mnt/overlay` combines what it finds across the search path — your addition plus everything from `/libs` you didn't touch. `/mnt/override` picks only the highest-priority one it finds and stops there, with nothing merged in from underneath. Overlay is for extending; override is for replacing outright."

**Why you rarely reach for `/mnt/override` deliberately:** almost every real customisation is additive — one more field, one more tab, one hidden property. Genuine full replacement is rare enough that most projects go their entire lifetime without a developer needing to invoke override semantics on purpose. It exists for the cases where merging genuinely isn't what you want — for example, a dialog so different from the original that trying to "hide" your way to the result would be more code than just replacing it.

graph TD subgraph SP\["Search path resources"] LIBS\["/libs/foo/bar\<br/>jcr:title = 'Original'\<br/>field-a, field-b"] APPS\["/apps/foo/bar\<br/>jcr:title = 'Energy Override'\<br/>field-c (new)"] end LIBS --> OVERLAY APPS --> OVERLAY OVERLAY\["/mnt/overlay/foo/bar\<br/>\<b>MERGED:\</b>\<br/>jcr:title = 'Energy Override' (apps wins)\<br/>field-a, field-b (kept from libs)\<br/>field-c (added from apps)"] LIBS -.->|"never consulted"| OVERRIDE APPS --> OVERRIDE OVERRIDE\["/mnt/override/foo/bar\<br/>\<b>OVERRIDE:\</b>\<br/>jcr:title = 'Energy Override'\<br/>field-c only\<br/>(field-a, field-b are GONE)"]

### 2.10 The properties that control the merge

Four properties, placed on the resource you create under `/apps`, give you fine control over the merged result. All four live in the `sling:` namespace, and all four only make sense in the context of the `/mnt/overlay` merging behaviour — they're how you selectively remove or reposition pieces of the lower-priority resource rather than accepting everything from it by default.

**`sling:hideResource`** (Boolean) — placed on a node to say "this entire resource, at this path, should not appear in the merged result, even though something exists here at a lower priority." This is a full hide — the merged view behaves as if nothing exists at this path at all.

**`sling:hideProperties`** — a String or String array naming specific property names to remove from the merged result, even though they exist on the lower-priority resource. Use `"*"` to hide every inherited property at that node, keeping only what you define yourself.

**`sling:hideChildren`** — the same idea, but for child resources rather than properties. Name specific child node names to exclude, or `"*"` to exclude everything inherited and keep only children you've explicitly added.

**`sling:orderBefore`** — a String naming a sibling node. Positions your added or merged child before that named sibling in the merged child ordering. Without this, a newly added field or tab tends to land at the end, which is rarely where you want it — a "Regulatory Region" field added to page properties should sit near the other compliance fields, not trail after everything else.

**A concrete example, put together:**

Say Adobe's dialog under `/libs` has a "SEO" tab containing fields `metaTitle`, `metaDescription`, and `robotsTag`. You want to: keep `metaTitle` and `metaDescription`, remove `robotsTag` because your project handles that a different way, and add a new field `regulatoryRegion`, positioned right after `metaDescription`.

```xml
<!-- /apps/energy/.../seo (nt:unstructured) -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="nt:unstructured"
    sling:hideChildren="robotsTag">
    <!--
      This node itself is NOT hidden -- it merges with the /libs "seo"
      tab. hideChildren removes just robotsTag from the merged children.
    -->
    <regulatoryRegion
        jcr:primaryType="nt:unstructured"
        sling:orderBefore="robotsTag"
        sling:resourceType="granite/ui/components/coral/foundation/form/select"
        fieldLabel="Regulatory Region"
        name="./regulatoryRegion">
        <items jcr:primaryType="nt:unstructured">
            <eu jcr:primaryType="nt:unstructured" value="eu" text="European Union"/>
            <na jcr:primaryType="nt:unstructured" value="na" text="North America"/>
            <apac jcr:primaryType="nt:unstructured" value="apac" text="Asia-Pacific"/>
        </items>
    </regulatoryRegion>
</jcr:root>
```

**Notice `sling:orderBefore="robotsTag"` even though `robotsTag` is about to be hidden.** That's deliberate and worth explaining if asked: ordering is resolved against the full merged child list _before_ hiding is applied, so naming the sibling you're about to hide is still a valid, stable way to say "put me exactly where that field used to be." (If that makes you uneasy about ordering drifting when the hidden sibling itself moves in a future Adobe update, that instinct is correct — it's one of the few genuine downsides of this approach, and it's why some teams anchor `orderBefore` against a field they know Adobe treats as stable, like the tab's first item, rather than one that might be reordered upstream.)

### 2.11 The classic use cases, tied to files 02 and 22

**Extending page properties.** Exactly the scenario in 2.8 — add a field or a whole new tab to Adobe's out-of-the-box page properties dialog. The dialog itself lives under `/libs` (the exact path has shifted slightly across AEM versions — historically the classic Foundation page dialog sat at `/libs/foundation/components/page/dialog`; the mechanism is identical regardless of the exact path in your version). You place a resource at the same relative path under `/apps`, containing only your addition, and the `/mnt/overlay` merge does the rest.

**One nuance worth stating precisely, because it's a genuine gotcha:** on a project built with **editable templates** — which is the standard on AEM as a Cloud Service and almost every modern project, including our energy site — each template defines its _own_ page properties structure under `/conf/<project>/settings/wcm/templates/<template>/`, which is content you already own outright. In that world you often don't need the Resource Merger for page properties at all; you edit your own `/conf` structure directly. The Resource Merger's page-properties use case is most relevant for **static/classic templates**, or for extending shared authoring dialogs and consoles that genuinely still live in `/libs`. Knowing this distinction — and saying so unprompted — is a strong signal that you've actually built on editable templates rather than only read about the classic pattern.

**Extending a Core Component's dialog (file 02's territory).** This is the more common real case on a modern project, and it works through **inheritance**, not the `/apps` -over- `/libs` overlay path. Recall from file 02: your project creates a **proxy component** — `/apps/energy/components/teaser` with `sling:resourceSuperType` pointing at the Core Component, e.g. `core/wcm/components/teaser/v2/teaser`. Core Components install under `/apps` themselves (they ship as a regular content package your project depends on, not as true product code under `/libs`), so there's no same-path overlay opportunity here at all — your proxy is at a completely different absolute path.

What makes the field addition work is that AEM's dialog-computation logic doesn't just merge same-path `/apps` -versus- `/libs` resources — it **also walks the `sling:resourceSuperType` chain** when computing the effective dialog for a resource type, merging your proxy's own (partial) `_cq_dialog` with whatever it inherits from the Core Component it points at, using the exact same `sling:hideResource` / `sling:hideProperties` / `sling:orderBefore` vocabulary. This is why file 02 could already tell you "create a dialog containing only the new field, plus `sling:orderBefore` " without needing to talk about mount points at all — the mount points are the deeper mechanism underneath that same-looking recipe.

**Why this beats copying, restated once more because it's the whole point:** in both cases, you keep receiving Adobe's improvements to the thing you extended. A Core Component security fix, a new field Adobe adds in a later version, a page-properties enhancement in the next AEMaaCS release — all of it reaches you automatically, because you never stopped consuming the original. You only own the small, explainable delta.

### 2.12 The problem /etc/map solves

Back in file 01, section 3.3 gave you the short version: Sling Mapping under `/etc/map` gets rid of `/content/energy/us/en` in your public URLs. Here is the full mechanism, because "how do we get clean URLs" turns into a much longer conversation the moment someone asks _both_ directions.

**The setup.** Your content genuinely lives at `/content/energy/us/en/home`. Nobody wants that in a browser's address bar, in a marketing email, or in a partner's inbound link. You want `https://www.energy-corp.com/us/en/home` instead.

**Inbound.** A visitor requests `/us/en/home.html`. Before Sling can resolve that to a resource, something has to translate it to the real repository path, `/content/energy/us/en/home`. That translation is a `sling:Mapping` node under `/etc/map`, using two properties:

- **`sling:match`** — describes the incoming request to match: typically the scheme, host, port, and path prefix, roughly in the shape `host[.port][/path-prefix]`.
- **`sling:internalRedirect`** — the real repository path to resolve to instead, once matched. This is a transparent, internal rewrite — the visitor's browser never sees it and the address bar doesn't change.

```text
/etc/map
  /http
    map-us-en (sling:Mapping)
      sling:match             = "www.energy-corp.com.80/us/en"
      sling:internalRedirect  = "/content/energy/us/en"
```

**Outbound.** This is the half people forget, and it's the whole subject of section 7's first story. When your own code or your HTL templates generate a link — a "related product" link, a breadcrumb, a canonical URL tag — the value you have in hand is the internal path, `/content/energy/us/en/products/hvdc-converter`. If you write that straight into an `<a href>`, visitors get a working link that exposes the internal repository structure, defeating the entire point of setting up mapping in the first place.

**`resourceResolver.map(request, path)`** is the method that does the reverse translation — it looks at the same set of mapping rules and, for a given internal path, produces the shortened, public form. Call it (or let AEM's built-in **link rewriter transformer**, which post-processes rendered HTML and rewrites `href` / `src` attributes automatically, do it for you) whenever you write a path-derived link.

**There's also a distinct property, `sling:redirect`,** which behaves differently from `sling:internalRedirect`. Where `internalRedirect` is a silent, internal rewrite, `sling:redirect` (Boolean, or sometimes paired with a target) causes Sling to send back an actual HTTP redirect response — the browser's address bar changes and the client makes a second request. You'd reach for this for something like retiring an old domain and sending every request on it to the new one, not for everyday clean-URL rewriting.

**The complete answer, worth having word-for-word ready — this expands directly on file 01's version:**

> "Sling Mapping under `/etc/map` handles it, and the mistake most people make is only implementing one direction. Inbound, a `sling:Mapping` node's `sling:match` and `sling:internalRedirect` properties silently rewrite the short public URL to the real content path before Sling resolves it — the browser never sees this happen. Outbound, when my own code or HTL generates a link from an internal path, I have to call `resourceResolver.map()`, or rely on AEM's link rewriter transformer to do it across the rendered HTML — otherwise every generated link still points at `/content/energy/us/en/...` even though the page you're standing on has a clean URL. Get only the inbound half right and you get exactly the bug you'd expect: a beautiful short URL in the address bar, and every single link on that page pointing at an internal repository path. We also layer Apache rewrite rules in front of the dispatcher for some of this, and on Cloud Service, Adobe gives you an equivalent CDN-level rewrite mechanism through the config pipeline instead of hand-editing Apache config."

### 2.13 map() and the link rewriter — the mechanics of the outbound half

**`resourceResolver.map()`** has two common overloads:

- `map(String resourcePath)` — maps using no request context, useful outside a request (a scheduled job building an email, for instance).
- `map(HttpServletRequest request, String resourcePath)` — maps with the current request's scheme and host taken into account, which matters when the same content is reachable through more than one public hostname (a country-specific domain versus a shared global one, for example).

Both consult the same `/etc/map` configuration, and both will also consider a **vanity path** (2.15) on the target resource if one is set — `map()` doesn't only know about `/etc/map` rules, it's the single entry point for "give me the best public URL for this internal path," and it picks whichever mechanism produces a usable result.

**The link rewriter transformer** is the alternative to calling `map()` by hand everywhere. It's a Sling rewriter pipeline component that post-processes the _entire rendered HTML output_ of a page, finding `href` and `src` attributes that look like internal repository paths and substituting the mapped, external form — so you get outbound mapping applied consistently across a page even where a developer forgot to call `map()` explicitly in one component's HTL. Most projects rely on this as the safety net, and call `map()` directly only in code paths that don't produce ordinary rendered HTML — an email template, a PDF export, a JSON API response.

### 2.14 Interaction with the dispatcher and Apache rewrite rules

URL mapping inside AEM and rewrite rules inside your dispatcher's Apache configuration are two separate layers that have to agree with each other, and this is a genuinely common source of "it works in the CMS but not on the live site" bugs.

Sling's mapping under `/etc/map` decides how AEM itself resolves and generates URLs. Apache's `mod_rewrite` rules, sitting in front of the dispatcher, decide what the _browser_ is allowed to type or receive before the request even reaches AEM. If a marketing team wants `https://www.energy-corp.com/products/` (no country code at all) to transparently serve `/content/energy/us/en/products`, you typically need **both**: an Apache rewrite rule that accepts the friendly incoming path and forwards it appropriately, and a Sling mapping so that AEM's own internal `map()` / `resolve()` calls understand the same relationship for any content generated dynamically. Set up only the Apache side and internal links generated by AEM (breadcrumbs, canonical tags, sitemap entries) will still show the old form; set up only the Sling side and a request from a browser that never goes through the expected Apache rule won't be recognised at all.

### 2.15 On AEM as a Cloud Service — CDN-level rewrite rules

On AEMaaCS, you don't own an Apache VM in front of your publish tier the way you did on 6.5 (file 01, section 3.5) — the dispatcher runs as a container alongside each publish pod, and Adobe manages the CDN in front of everything. For the class of rewrite that used to live in a hand-edited Apache config, Adobe instead gives you a declarative rewrite configuration deployed through the Cloud Manager pipeline, applied at the CDN edge before a request ever reaches your dispatcher or publish tier. The underlying _reason_ you'd want this hasn't changed — friendly incoming paths, redirecting an old domain, stripping or adding path segments — only where the rule lives and how it's deployed has moved, consistent with the general "everything through the pipeline, nothing hand-edited on a box" shift file 01 covers in the 6.5-versus-Cloud-Service comparison.

### 2.16 Vanity URLs — a different mechanism entirely, and how it differs from mapping

It's tempting to lump vanity URLs in with `/etc/map`, because both produce short, friendly public URLs. They are genuinely different mechanisms, solving a different shaped problem.

**`sling:vanityPath`** is a property (String, or a String array for more than one) placed directly on a page's `jcr:content` node. Whatever value you put there becomes an additional, alternate path that resolves straight to that page — set by a content author, per page, without touching `/etc/map`, without a developer, and without a deployment.

```text
/content/energy/us/en/campaigns/summer-grid-modernization/jcr:content
    sling:vanityPath = "/grid2026"
```

Now `https://www.energy-corp.com/grid2026` resolves directly to that campaign page.

**How it differs from `/etc/map` mapping, in the ways that matter:**

|                                | `/etc/map` mapping                             | `sling:vanityPath`                                                        |
| ------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------- |
| Who configures it              | A developer, in `/etc/map` config              | **An author**, as a page property                                         |
| Scope                          | A prefix pattern applying to a whole subtree   | **One specific page**                                                     |
| Deployment needed              | Yes — content package or config change         | **No** — saved like any content edit                                      |
| Typical use                    | Stripping a whole locale/site prefix, globally | A short campaign or landing-page URL                                      |
| How it's discovered at startup | Static config, read once                       | Built by **scanning the repository** for the property, requiring an index |

**A property that pairs with it: `sling:redirect`** (Boolean) on the same node. Left unset, the vanity path resolves internally — the visitor keeps seeing the short URL in the address bar, with no visible redirect. Set to `true`, requesting the vanity path instead causes an actual HTTP redirect to the page's real, canonical URL — useful when you want the vanity path to work as a shareable shortcut, but you want analytics and bookmarks to settle on the canonical URL rather than the shortcut.

**A production detail worth knowing:** because vanity paths are discovered by scanning the whole repository for the `sling:vanityPath` property, this needs a proper Oak index behind it (the same "every query needs an index" rule from file 01) — on a repository with a very large number of pages and no dedicated index for it, vanity path resolution can become measurably slow, and a missing or broken index here is a genuine, if less obvious, production scenario (see section 13).

***

## 3. Internal Working

### 3.1 The full resolution sequence, both directions

This extends file 01's request-flow diagram by zooming into exactly the step where mapping and resolution happen, and by showing the outbound side that the file 01 diagram didn't cover at all.

sequenceDiagram participant B as Browser participant D as Dispatcher participant M as Sling Mapping (/etc/map) participant R as ResourceResolver.resolve() participant V as Vanity Path lookup participant J as JCR / Oak participant S as Script resolution B->>D: GET /us/en/home.html D->>R: forward (cache miss) R->>M: check /etc/map inbound rules M-->>R: sling:internalRedirect -> /content/energy/us/en/home R->>V: also check for a matching vanity path V-->>R: none for this path -- continue R->>J: does a node exist there, and can THIS USER read it? alt resource exists and readable J-->>R: Resource R->>S: read sling:resourceType, find script S-->>B: rendered response else missing, or ACL denies J-->>R: nothing R-->>B: NonExistingResource -> 404 page via sling:nonexisting end Note over B,S: --- OUTBOUND, generating a link on that page --- S->>R: resolver.map(request, "/content/energy/us/en/products/x") R->>M: any matching sling:internalRedirect target? reverse it R->>V: any sling:vanityPath set on that resource? prefer it R-->>S: "/us/en/products/x" (short, public form) S-->>B: \<a href="/us/en/products/x">

**The two things worth narrating out loud from this diagram, because they're the parts a memorised list skips:**

**One** — the permission check happens at the same step as the mapping and the existence check. `resolve()` isn't "find the node, then separately check if the user can see it" — the resource genuinely does not resolve at all for a user without read access. That's why an anonymous visitor and a logged-in author can hit the identical URL and get a 404 versus a real page: same resolution logic, different resolver, different outcome.

**Two** — outbound mapping is not automatic just because inbound mapping exists. It is a distinct call (`map()`), consulted separately, every time a link is generated. Nothing forces a developer to make that call; forgetting it is section 7's first story.

### 3.2 The Resource Merger's actual merge algorithm

graph TD A\["Something asks for /mnt/overlay/apps/energy/.../cq:dialog"] --> B\["Resource Merger walks the search path,\<br/>highest priority first: /apps, then /libs"] B --> C\{"Does a resource exist\<br/>at this relative path in /apps?"} C -->|Yes| D\["Take its properties as the base.\<br/>Apply sling:hideProperties/hideChildren\<br/>if present."] C -->|No| E\["Nothing to contribute at this level --\<br/>fall through to the next search path entry"] D --> F\["Merge in properties/children from /libs\<br/>that were NOT hidden and NOT already\<br/>overridden by /apps"] E --> F F --> G\{"Does the RESOURCE also declare\<br/>sling:resourceSuperType?"} G -->|Yes| H\["Repeat the same merge against\<br/>the super type's resource tree\<br/>(this is how a proxy extends\<br/>a Core Component's dialog)"] G -->|No| I\["Merged result returned"] H --> I I --> J\["Apply sling:orderBefore to\<br/>position merged children"]

**The detail worth calling out unprompted:** the merge doesn't stop after combining `/apps` and `/libs` at the same path — for a resource that also declares `sling:resourceSuperType`, the merger keeps going, walking that inheritance chain too. This is exactly why a proxy component's tiny `_cq_dialog`, containing only one new field, ends up producing a complete, working dialog: two separate merge mechanisms — same-path search order, and resourceSuperType inheritance — are both feeding into the one result AEM actually renders.

### 3.3 Comparing getResource(), resolve(), and map() as one flow

graph LR subgraph Inbound\["Turning a URL into content"] U\["/us/en/home.html"] -->|"resolve()"| RM\["/etc/map\<br/>inbound rewrite"] RM --> RES\["/content/.../home"] RES -->|"ACL check happens here"| RESULT1\["Resource, or\<br/>NonExistingResource"] end subgraph Direct\["A literal path lookup"] P\["/content/.../home"] -->|"getResource()"| RESULT2\["Resource, or null\<br/>-- no mapping applied"] end subgraph Outbound\["Turning content into a link"] C\["/content/.../products/x"] -->|"map()"| MM\["/etc/map reverse +\<br/>vanity path check"] MM --> RESULT3\["/us/en/products/x\<br/>-- short, public form"] end

***

## 8. Coding/Configuration Examples

### 8.1 resolve() vs getResource() side by side, with the null-safety consequence

```java
package com.energy.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;

public class ResolutionExamples {

    /**
     * getResource(): a literal, direct lookup.
     * NO url mapping is applied. Returns null if nothing exists here --
     * this null check is real and reachable.
     */
    public boolean pageExistsAt(ResourceResolver resolver, String path) {
        Resource resource = resolver.getResource(path);
        return resource != null;
    }

    /**
     * resolve(): the FULL pipeline -- applies /etc/map inbound rewriting,
     * strips selectors/extension, and NEVER returns null.
     *
     * A missing resource comes back as a NonExistingResource: a real,
     * non-null object with resource type "sling:nonexisting".
     */
    public boolean pageActuallyResolves(SlingHttpServletRequest request, String path) {
        Resource resource = request.getResourceResolver().resolve(request, path);

        // WRONG -- this branch is dead code, resolve() never returns null:
        // if (resource == null) { return false; }

        // RIGHT -- this is the actual "does it exist" check for resolve():
        return !ResourceUtil.isNonExistingResource(resource);
    }
}
```

### 8.2 A service-user resolver, opened and closed correctly

```java
package com.energy.core.services.impl;

import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.Map;

@Component(service = PricingReaderService.class)
public class PricingReaderServiceImpl implements PricingReaderService {

    private static final Logger LOG = LoggerFactory.getLogger(PricingReaderServiceImpl.class);
    private static final String SUBSERVICE = "pricing-reader";

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    public String readCurrentRegionPricing(String region) {

        Map<String, Object> params =
                Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SUBSERVICE);

        // I opened this resolver, so I close it. Always try-with-resources --
        // an early return or an exception below still triggers close().
        try (ResourceResolver resolver = resolverFactory.getServiceResourceResolver(params)) {

            Resource pricing = resolver.getResource("/content/energy/pricing/" + region);
            if (pricing == null) {
                LOG.warn("No pricing data found for region {}", region);
                return null;
            }
            return pricing.getValueMap().get("currentRate", String.class);

        } catch (LoginException e) {
            // The service user mapping itself failed -- almost always a
            // Repoinit / ServiceUserMapper configuration problem (file 13).
            LOG.error("Could not obtain the pricing-reader service resolver", e);
            return null;
        }
    }
}
```

### 8.3 A link-building utility that always calls map() — the fix from Story 1

```java
package com.energy.core.utils;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;

/**
 * Every path-derived link in the project should go through here, so that
 * outbound URL mapping is never something an individual component has to
 * remember to do. This is the fix for a rollout where inbound mapping
 * was set up correctly but outbound mapping was simply forgotten --
 * clean URL in the address bar, /content paths in every link.
 */
public final class LinkBuilder {

    private LinkBuilder() { }

    public static String toPublicUrl(SlingHttpServletRequest request, String internalPath) {
        if (internalPath == null) {
            return null;
        }
        ResourceResolver resolver = request.getResourceResolver();

        // map() also checks for a vanity path on the target resource and
        // prefers it over a plain /etc/map rewrite when one is set.
        return resolver.map(request, internalPath);
    }
}
```

```html
<!-- In HTL, always go through the model rather than printing the raw path -->
<a data-sly-test="${relatedProduct.publicUrl}" href="${relatedProduct.publicUrl}">
    ${relatedProduct.title}
</a>
```

### 8.4 /etc/map — the inbound mapping node

```xml
<!-- /etc/map/http/map-us-en/.content.xml -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="sling:Mapping"
    sling:match="www.energy-corp.com.80/us/en"
    sling:internalRedirect="/content/energy/us/en"/>
```

```xml
<!-- A domain-level redirect, using sling:redirect instead of internalRedirect --
     this one causes a REAL HTTP redirect, not a silent internal rewrite. -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="sling:Mapping"
    sling:match="www.old-energy-domain.com.80"
    sling:redirect="https://www.energy-corp.com"/>
```

### 8.5 A vanity URL on a campaign page

```xml
<!-- /content/energy/us/en/campaigns/summer-grid-modernization/jcr:content -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
    xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="cq:PageContent"
    jcr:title="Summer Grid Modernization Campaign"
    sling:vanityPath="/grid2026"
    sling:vanityOrder="{Long}100"/>
    <!--
      No /etc/map entry needed for this -- an author sets this property
      directly, and it takes effect without a deployment. Add sling:redirect
      = {Boolean}true here to make requesting the vanity path issue a real
      HTTP redirect to the canonical URL instead of resolving silently.
    -->
```

### 8.6 The Resource Merger — extending page properties (full worked example)

```xml
<!--
  /apps/energy/wcm-overlays/.../seo/.content.xml
  (placed at the SAME RELATIVE PATH under /apps as the "seo" tab node
  under /libs -- this is what makes /mnt/overlay merge the two)

  GOAL: keep metaTitle and metaDescription from Adobe's dialog, remove
  robotsTag, and add regulatoryRegion positioned where robotsTag used
  to sit.
-->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="nt:unstructured"
    sling:hideChildren="robotsTag">

    <items jcr:primaryType="nt:unstructured">
        <regulatoryRegion
            jcr:primaryType="nt:unstructured"
            sling:orderBefore="robotsTag"
            sling:resourceType="granite/ui/components/coral/foundation/form/select"
            fieldLabel="Regulatory Region"
            fieldDescription="Used by compliance reporting to filter content by jurisdiction."
            name="./regulatoryRegion">
            <items jcr:primaryType="nt:unstructured">
                <eu jcr:primaryType="nt:unstructured" value="eu" text="European Union"/>
                <na jcr:primaryType="nt:unstructured" value="na" text="North America"/>
                <apac jcr:primaryType="nt:unstructured" value="apac" text="Asia-Pacific"/>
            </items>
        </regulatoryRegion>
    </items>
</jcr:root>
```

### 8.7 The Resource Merger — extending a Core Component's dialog via a proxy (ties to file 02)

```xml
<!--
  /apps/energy/components/teaser/.content.xml  (the PROXY component)

  This is a DIFFERENT mechanism from 8.6 -- there is no /libs copy of
  this exact path to merge with, because the proxy's path is entirely
  our own. The merge happens across sling:resourceSuperType instead.
-->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="cq:Component"
    jcr:title="Energy Teaser"
    sling:resourceSuperType="core/wcm/components/teaser/v2/teaser"/>
```

```xml
<!--
  /apps/energy/components/teaser/_cq_dialog/.content.xml

  This dialog contains ONLY the new field. AEM's dialog computation
  walks the resourceSuperType chain and merges this with the FULL
  Core Component Teaser dialog -- same hide/order vocabulary as 8.6,
  different trigger (inheritance, not same-path overlay).
-->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Teaser">
    <content jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/tabs">
                <items jcr:primaryType="nt:unstructured">
                    <energySpecific jcr:primaryType="nt:unstructured" jcr:title="Energy Sector"
                        sling:resourceType="granite/ui/components/coral/foundation/container">
                        <items jcr:primaryType="nt:unstructured">
                            <productLine
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/select"
                                fieldLabel="Product Line"
                                name="./productLine">
                                <items jcr:primaryType="nt:unstructured">
                                    <transformers jcr:primaryType="nt:unstructured" value="transformers" text="Transformers"/>
                                    <hvdc jcr:primaryType="nt:unstructured" value="hvdc" text="HVDC Systems"/>
                                    <grid jcr:primaryType="nt:unstructured" value="grid" text="Grid Automation"/>
                                </items>
                            </productLine>
                        </items>
                    </energySpecific>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>
```

**Notice the shape difference between 8.6 and 8.7.** In 8.6, the resource I wrote is a small delta node sitting inside Adobe's existing dialog structure — it merges at a granular level because the path lines up exactly. In 8.7, my dialog adds a whole new **tab**, because the proxy pattern typically presents new, project-specific fields as their own clearly separated section rather than trying to slot individual fields into Adobe's existing tabs — both are valid uses of the same underlying merge machinery, chosen based on how naturally the addition fits the original structure.

### 8.8 Reading the merged view directly, for debugging

```java
// Handy in a script console or a debugging service: read the MERGED view
// directly, rather than /apps or /libs individually, to see exactly what
// AEM itself will actually use.
Resource merged = resolver.getResource(
        "/mnt/overlay/energy/wcm-overlays/apps/wcm/core/content/components/page/dialog/content/items/tabs/items/seo");

if (merged != null) {
    merged.getValueMap().forEach((key, value) ->
            System.out.println(key + " = " + value));
}
```

***

## 9. Common Mistakes

| The mistake                                                                                  | What actually happens                                                                                        | The fix                                                                                                      |
| -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
| Treating `resolve()` and `getResource()` as interchangeable                                  | Null checks written for `resolve()` are dead code; a `NonExistingResource` slips through as if it were real  | Use `ResourceUtil.isNonExistingResource()` after `resolve()`, and reserve the null check for `getResource()` |
| Copying an entire OOTB dialog to add one field                                               | You freeze your copy and stop receiving Adobe's future fixes to it                                           | Sling Resource Merger — add only the delta                                                                   |
| Overlaying a Core Component                                                                  | It doesn't work — Core Components already live under `/apps`, there's no `/libs` copy at that path to shadow | Use a proxy component with `sling:resourceSuperType` (inheritance, not overlay)                              |
| Implementing only inbound `/etc/map`                                                         | Clean URL in the address bar, `/content/...` in every generated link                                         | Also call `resourceResolver.map()`, or enable the link rewriter transformer                                  |
| Confusing `sling:internalRedirect` with `sling:redirect`                                     | Expecting a silent rewrite but getting a visible HTTP redirect, or vice versa                                | Know which one you actually need before writing the mapping node                                             |
| Anchoring `sling:orderBefore` against a field you're also hiding, or one Adobe might reorder | Field position drifts on the next Adobe update, with no error                                                | Anchor against a stable, well-known sibling where possible, and re-verify after upgrades                     |
| Closing `request.getResourceResolver()` in a servlet or model                                | The rest of that request breaks                                                                              | Only close a resolver you opened yourself                                                                    |
| Opening a service-user resolver without try-with-resources                                   | An early return or exception skips `.close()`, leaking a JCR session                                         | Always try-with-resources, no exceptions                                                                     |
| Assuming vanity paths and `/etc/map` are the same mechanism                                  | Confusing an author-editable per-page shortcut with a developer-configured prefix rule                       | Know the table in section 2.16                                                                               |
| Not indexing for `sling:vanityPath` at scale                                                 | Vanity path resolution slows down as content grows                                                           | Ensure a proper Oak index backs the vanity path lookup                                                       |
| Setting up Apache/CDN rewrite rules without a matching Sling mapping (or vice versa)         | Browser-typed URLs work but AEM's own generated links don't, or the reverse                                  | Configure both layers to agree, and test both directions                                                     |
| Assuming the Resource Merger only works via `/apps` -over- `/libs`                           | Missing that dialog inheritance through `sling:resourceSuperType` is merged the same way                     | Understand both trigger paths into the same merge service                                                    |

***

## 10. Best Practices

**On resolution.** Use `getResource()` for a direct, literal check where `null` genuinely means "not there." Use `resolve()` (or code paths built on it, like a servlet's `request.getResource()`) when you're processing something that came in as a URL, and always check `isNonExistingResource()` rather than a null check afterward.

**On extension.** Default to inheritance (`sling:resourceSuperType`) for extending a component's behaviour. Reach for overlay only to customise the AEM authoring UI itself — a console, a shared classic dialog — where there genuinely is no `resourceSuperType` relationship to lean on. Treat override as a last resort, chosen deliberately, not as a shortcut when a merge would have done the job.

**On the Resource Merger.** Write the smallest delta that expresses your intent — one field, one hidden property, one reordering — never a wholesale copy. Comment _why_ a field is hidden or an order anchor was chosen, because six months later nobody remembers which Adobe field the new one was meant to replace.

**On URL mapping.** Treat inbound and outbound as two separate checklist items on every mapping-related story, never one. Centralise link-building through a single utility (or the link rewriter transformer) so outbound mapping isn't something every component author has to remember individually.

**On vanity URLs.** Use them for genuinely per-page, author-driven shortcuts — campaigns, landing pages. Use `/etc/map` for structural, prefix-level rewriting a developer owns. Don't use one to solve the other's problem.

**On resolvers.** Close what you opened, in try-with-resources, every time, with no exceptions for "this code path always returns cleanly." Never close what a framework handed you.

***

## 11. Debugging Tips

A resolution or mapping problem almost always has a fast, specific tool behind it — the trick is picking the right one first rather than guessing at OSGi config.

| Tool / console                                                                                               | Answers                                                                                                                                             |
| ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/system/console/servletresolver` (from file 07)                                                             | What actually resolves for a given URL, including the resource-type chain considered                                                                |
| A script console (Groovy console, or CRXDE Lite's JS console) calling `resolve()` / `getResource()` directly | Confirms exactly what a path resolves to, for a specific user's resolver, without guessing                                                          |
| Reading `/mnt/overlay/<path>` directly                                                                       | Shows the actual merged result the Resource Merger produces — the fastest way to confirm a dialog extension took effect                             |
| `/etc/map` in CRXDE Lite                                                                                     | Confirms the exact `sling:match` / `sling:internalRedirect` values in force                                                                         |
| Heap dump + session count                                                                                    | The way to confirm a suspected resolver/session leak is real before hunting for the code                                                            |
| `dispatcher.log`                                                                                             | Confirms whether a rewritten or mapped request even reached AEM, versus being rejected earlier                                                      |
| Browser network tab, on both a short URL and a generated link                                                | Separates an inbound-mapping problem from an outbound-mapping problem immediately — one shows in the address bar, the other in the page's own links |

**The order that actually finds a resolution bug fast:**

1. **Confirm what resolves.** Use the servlet resolver console or a script console call to `resolve()` / `getResource()` directly — don't guess from reading config.
2. **Check whose resolver you're looking at.** `getUserID()` on the resolver in question tells you immediately whether an ACL-shaped bug is actually a permissions difference between two users.
3. **For a merge problem, read the merged view directly** at `/mnt/overlay/<path>` rather than inspecting `/apps` and `/libs` separately and trying to merge them in your head.
4. **For a mapping problem, test both directions independently** — a short URL typed into the browser tests inbound only; viewing page source for `/content/...` links tests outbound only.
5. **For a suspected leak, get a heap dump before touching any code** — confirming the session count is actually elevated saves you from "fixing" the wrong service.

***

## 12. Performance Notes

**`findResources()` and raw queries need an index, exactly like QueryBuilder (file 01).** An unindexed query through the resolver traverses the repository the same way an unindexed QueryBuilder query does — the API you called it through doesn't change Oak's underlying cost.

**Vanity path resolution is built by scanning the repository for `sling:vanityPath`.** On a large site, make sure this is backed by a proper Oak index; without one, the initial build (and any rebuild) of the internal vanity path table gets measurably slower as content grows.

**The search path is checked on every resource-type-to-script lookup, per request.** This is cheap individually, but it's why an unnecessarily long or oddly-ordered search path (something almost no project should ever configure) would add up across a high-traffic instance.

**Every unclosed service-user resolver is a retained JCR session.** This isn't a "slow" problem in the traditional sense — it's a slow-growing memory problem that eventually becomes an availability problem. Treat resolver lifecycle as a correctness issue, not a nice-to-have.

**Outbound `map()` calls are cheap individually but add up in a loop.** Building a large listing where every single row calls `map()` separately is fine at normal listing sizes; if you're generating links for genuinely large batches (a sitemap generator, for instance), be aware you're doing real work per link and profile it like anything else in a hot loop.

***

## 15. Comparison Tables

**`getResource()` vs `resolve()`**

|                                  | `getResource()`                    | `resolve()`                                      |
| -------------------------------- | ---------------------------------- | ------------------------------------------------ |
| Applies `/etc/map`               | No                                 | **Yes**                                          |
| Strips selectors/extension first | No                                 | **Yes**                                          |
| Result when nothing matches      | **`null`**                         | **`NonExistingResource`** — never null           |
| Applies the caller's ACLs        | Yes (implicitly, via the resolver) | Yes                                              |
| Typical use                      | Direct, known-path lookups         | Turning an incoming request path into a resource |

**Overlay vs Inheritance vs Override**

|                               | Overlay                                    | Inheritance                           | Override                         |
| ----------------------------- | ------------------------------------------ | ------------------------------------- | -------------------------------- |
| Trigger                       | Same relative path, `/apps` before `/libs` | `sling:resourceSuperType`             | Highest-priority resource only   |
| Scope                         | Global                                     | Scoped, opt-in                        | Scoped to the declaring resource |
| Mount point (Resource Merger) | `/mnt/overlay`                             | `/mnt/overlay` (chain-walked)         | `/mnt/override`                  |
| Keeps future upstream fixes   | **No**                                     | **Yes**, for everything not redefined | **No**                           |

**`/mnt/overlay` vs `/mnt/override`**

|                        | `/mnt/overlay`                                 | `/mnt/override`                                 |
| ---------------------- | ---------------------------------------------- | ----------------------------------------------- |
| Behaviour              | Merges everything found across the search path | Picks only the single highest-priority resource |
| Lower-priority content | Preserved unless explicitly hidden             | **Never consulted at all**                      |
| Typical use            | Adding/removing a field, a tab, a property     | A deliberate, full replacement                  |

**`sling:internalRedirect` vs `sling:redirect`**

|                        | `sling:internalRedirect`   | `sling:redirect`                        |
| ---------------------- | -------------------------- | --------------------------------------- |
| Visible to the browser | **No** — silent rewrite    | **Yes** — actual HTTP redirect          |
| Address bar changes    | No                         | Yes                                     |
| Typical use            | Everyday clean-URL mapping | Domain retirement, deliberate redirects |

**`/etc/map` vs `sling:vanityPath`**

|                        | `/etc/map`                     | `sling:vanityPath`                   |
| ---------------------- | ------------------------------ | ------------------------------------ |
| Configured by          | Developer                      | **Author**                           |
| Scope                  | A path prefix, a whole subtree | **One page**                         |
| Needs a deployment     | Yes                            | **No**                               |
| Performance dependency | Static config                  | **Repository scan — needs an index** |

**Resolvers — who owns closing them**

| Resolver source                                   | Who closes it                                       |
| ------------------------------------------------- | --------------------------------------------------- |
| `request.getResourceResolver()`                   | **Sling** — never close it yourself                 |
| `resolverFactory.getServiceResourceResolver(...)` | **You** — always try-with-resources                 |
| `getAdministrativeResourceResolver()`             | Deprecated, blocked on Cloud Service — don't use it |

***

## Next topic

**`24-Headless-AEM-GraphQL-and-SPA-Editor`** — Content Fragment-driven headless delivery through the GraphQL API, persisted queries, and how the SPA Editor keeps in-context authoring working when the front end is a React or Angular application rather than server-rendered HTL.

***

_Topic 23 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain._
