I once witnessed a seemingly innocuous change to an /etc/map node bring down an entire production Publish farm. A junior developer mapped a root regex too greedily, causing an infinite internal redirect loop that bypassed the Dispatcher cache, exhausted the Sling thread pools, and spiked CPU utilization to 100% across the cluster. URL mapping is arguably the most misunderstood layer in Adobe Experience Manager (AEM). It sits precariously at the intersection of frontend rendering, SEO compliance, backend resource resolution, and infrastructure-level caching. When misconfigured, it results in broken outbound links, malformed canonical tags, and severe performance degradation. When configured correctly, it seamlessly translates raw JCR node structures into elegant, SEO-friendly URLs across multi-tenant architectures.
In this exhaustive guide, we are going to dissect the entire lifecycle of a URL in AEM. We will cover the exact sequence of Apache Sling's resource resolution, breaking down how a browser's request for a web page translates into a specific JCR node and a corresponding HTL script. We will dive deep into the ResourceResolver API, contrasting inbound resolution with outbound mapping, and providing real Java code examples. We will explore complex multi-domain /etc/map structures, analyzing how to host multiple brands on a single AEM instance. We will also tackle the notorious performance issues surrounding vanity URLs, including how to tune the Bloom filter and configure the vanity path blacklist. Finally, we will cover the architectural boundary between Dispatcher rewrites and AEM mappings, how to generate extensionless URLs safely, and how AEM as a Cloud Service fundamentally alters /etc/map provisioning via RepoInit.
Before we descend into the JCR structures and OSGi configurations that power AEM routing, you must understand the broader context of request processing. I strongly recommend cross-referencing my Dispatcher guide to understand the caching layer, the Apache Sling framework guide for the foundational request lifecycle, the AEM SEO complete guide for canonical and rewrite strategies, and the comprehensive AEM Architecture guide to visualize how these discrete components orchestrate the delivery of an enterprise platform.
How Sling Resource Resolution Actually Works
Every HTTP request that reaches an AEM instance is intercepted and processed by the Apache Sling framework. Sling operates on a fundamental paradigm: everything in the system is a Resource. A web page is a resource, a JSON endpoint is a resource, a binary asset is a resource. The intricate process of translating an incoming HTTP request URI—such as https://www.brand1.com/about-us.html—into an absolute JCR Node path—like /content/brand1/us/en/about-us—is called Resource Resolution.
This translation mechanism is not a simplistic string matching utility or a basic switch statement. The SlingRequestProcessor delegates the routing logic to the JcrResourceResolver, which evaluates a highly specific, multi-layered chain of configurations, mappings, and JCR properties to pinpoint the target node.
The Resolution Pipeline Flowchart
When an incoming request breaches the Dispatcher and hits the AEM Publish instance, the exact sequence of evaluation looks like this:
HTTP Request: GET /about-us.html
1. Extract Request Details
├── Scheme: https
├── Host: www.brand1.com
├── Port: 443
└── Path: /about-us.html
2. Evaluate /etc/map (Inbound Mapping)
├── Look for a node matching the exact scheme/host/port (e.g., /etc/map/https/www.brand1.com_443)
├── Evaluate the sling:match regular expression against the request path
├── Apply sling:internalRedirect logic (e.g., /about-us -> /content/brand1/us/en/about-us)
└── If a valid mapped JCR path is found, update the internal request path in memory.
3. Vanity Path Checking (If /etc/map didn't yield a definitive content node)
├── Check the in-memory Bloom Filter to see if the requested path might be a vanity path
├── If the Bloom Filter indicates a possible match, execute a JCR query for the sling:vanityPath property matching "/about-us"
└── If a node is found with that vanity property, resolve to that node.
4. Default JCR Direct Resolution (Fallback)
├── Check if the path requested directly exists in the JCR (e.g., if the user requested /content/brand1/us/en/about-us directly)
└── If the node exists, return the Resource object representing it.
5. Resource Provider & Script Resolution
├── Extract the selectors (e.g., .printable) and extensions (e.g., .html) from the URL
├── Read the sling:resourceType property of the resolved Resource
├── Traverse the /apps and /libs directory structure to find the matching component script
└── Execute the corresponding HTL or JSP script to render the HTML.
6. Non-Existing Resource (404 Fallback)
└── If all the above steps fail to yield an accessible JCR node, return a NonExistingResource. AEM will then render the configured 404 error page.Notice a critical detail in this architecture: the /etc/map evaluation happens before vanity path checks and before direct JCR path evaluation. This sequence makes /etc/map an incredibly powerful mechanism. It intercepts requests at the earliest possible stage in Sling's lifecycle. However, this power makes it equally dangerous; a flawed regex in an /etc/map node will unconditionally hijack requests, breaking authoring capabilities or causing infinite loops.
The Dual Nature of ResourceResolver: Inbound vs. Outbound
To truly master AEM routing, you must fundamentally comprehend the difference between inbound resolution and outbound mapping. The ResourceResolver API provides two distinct, opposing methods that handle these responsibilities: resolve() and map(). Most engineers confuse the two, leading to broken outbound links or incorrect 404 routing.
Inbound Resolution: ResourceResolver.resolve()
The resolve(HttpServletRequest request, String absPath) method handles inbound resolution. It consumes a user-facing URL that a client requested (or simulated) and translates it into a concrete JCR Resource object.
When the AEM Sling engine processes a live incoming HTTP request, it implicitly executes this method behind the scenes. If a user's browser requests https://www.brand1.com/products.html, Sling utilizes resolve() to deduce that it needs to serve the node located at /content/brand1/us/en/products.
- Execution Flow: It reads configurations from the
/etc/maptree, applies the correspondingsling:internalRedirectproperties, queries for vanity paths if necessary, and ultimately resolves the target node. - Result: It returns a
Resourceobject. If the path cannot be mapped to an existing node, it returns aNonExistingResourceobject (which subsequently triggers a 404 response).
Java Code Example: Using resolve() in a Backend Service
If you are writing a custom OSGi service or a Sling Model and need to programmatically determine which JCR node corresponds to a given user-facing URL, you would use resolve().
package com.brand1.core.services.impl;
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;
@Component(service = UrlAnalysisService.class)
public class UrlAnalysisServiceImpl implements UrlAnalysisService {
private static final Logger LOG = LoggerFactory.getLogger(UrlAnalysisServiceImpl.class);
@Reference
private ResourceResolverFactory resolverFactory;
@Override
public void analyzeUrl(ResourceResolver resolver, String userFacingUrl) {
// userFacingUrl example: "/products.html" (assuming host information is contextual)
// Execute inbound resolution
Resource resolvedResource = resolver.resolve(userFacingUrl);
if (resolvedResource != null && !ResourceUtil.isNonExistingResource(resolvedResource)) {
LOG.info("The URL '{}' resolves to JCR path: {}", userFacingUrl, resolvedResource.getPath());
// Expected Output: The URL '/products.html' resolves to JCR path: /content/brand1/us/en/products
} else {
LOG.warn("The URL '{}' could not be resolved to an existing JCR node.", userFacingUrl);
}
}
}Outbound Mapping: ResourceResolver.map()
The map(HttpServletRequest request, String resourcePath) method is the antithesis of resolve(). It handles outbound mapping. It takes an absolute JCR path (e.g., /content/brand1/us/en/products) and shortens, rewrites, or transforms it into a user-facing URL string (e.g., /products.html or the fully qualified https://www.brand1.com/products.html).
This method is heavily utilized when AEM generates outgoing HTML markup. Whenever an HTL component uses a link—for instance, when you write <a href="${request.resourceResolver.map(properties.link)}">—Sling traverses the /etc/map configurations to reverse-engineer the path. It looks for rules that define how the absolute JCR path should be formatted for public consumption.
- Execution Flow: It scans the
/etc/maphierarchy searching for asling:internalRedirectproperty that matches the provided JCR path. When it finds a match, it applies the associatedsling:matchregular expression and domain information to output the shortened URL. - Result: It returns a
Stringrepresenting the mapped, public-facing URL.
Java Code Example: Using map() in a Sling Model
If you are building a custom navigation component and need to expose shortened, SEO-friendly URLs to the frontend, you must explicitly call map(). Note that AEM's Core Components automatically handle mapping for you, but if you are writing a custom JSON exporter or a bespoke component, you must do this manually.
package com.brand1.core.models;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import javax.annotation.PostConstruct;
@Model(
adaptables = SlingHttpServletRequest.class,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class CustomButtonModel {
@SlingObject
private SlingHttpServletRequest request;
@SlingObject
private ResourceResolver resourceResolver;
@ValueMapValue
private String linkTarget; // e.g., "/content/brand1/us/en/contact-us"
private String mappedUrl;
@PostConstruct
protected void init() {
if (linkTarget != null) {
// Apply outbound mapping to generate the short URL
// We pass the request object to ensure context (host, scheme) is considered
mappedUrl = resourceResolver.map(request, linkTarget) + ".html";
// Expected mappedUrl: "/contact-us.html" (or "https://www.brand1.com/contact-us.html" depending on config)
}
}
public String getMappedUrl() {
return mappedUrl;
}
}The Golden Rule of Map vs. Resolve
If you memorize nothing else from this article, memorize this table. The confusion between these two methods is the root cause of 90% of routing bugs in AEM development.
| Aspect | ResourceResolver.resolve() | ResourceResolver.map() |
|---|---|---|
| Direction of Translation | Inbound (Browser -> AEM) | Outbound (AEM -> Browser) |
| Input Parameter | User-facing URL String (/products.html) | Absolute JCR Path String (/content/brand1/us/en/products) |
| Return Type | Resource object | String (Formatted URL) |
| Primary Use Case | Routing the incoming HTTP request to a specific node | Generating href or src attributes in HTML markup |
| How it uses /etc/map | Matches the URL against sling:match, yielding the sling:internalRedirect path | Matches the JCR path against sling:internalRedirect, yielding the sling:match pattern |
The /etc/map Node Structure Unveiled
The actual mechanics of the mapping process are governed by the node structure beneath /etc/map. This JCR tree is systematically organized by protocol (e.g., http, https) and then by domain or port.
A mapping rule node must be defined with a specific jcr:primaryType, which is virtually always sling:Mapping.
The Core Properties of sling:Mapping
Understanding the specific JCR properties applied to a sling:Mapping node is non-negotiable.
sling:match(String)- Inbound Context: This defines the regular expression that Sling attempts to match against the incoming request URL path. If the regex matches, Sling executes the rule.
- Outbound Context: This acts as the string template or replacement pattern used to format the final URL when generating links via
map().
sling:internalRedirect(String or String[])- Inbound Context: This defines the target JCR path (or array of paths) that AEM should route to if the
sling:matchcondition is satisfied. Sling evaluates the array sequentially; the first path that corresponds to an existing node wins. This happens entirely server-side; the browser's URL does not change. - Outbound Context: This defines the JCR path pattern that triggers this specific mapping rule when
map()is invoked. If the absolute path passed tomap()matches this string, AEM applies the rule to shorten the path.
- Inbound Context: This defines the target JCR path (or array of paths) that AEM should route to if the
sling:redirect(String)- Do not confuse this with
internalRedirect. WhileinternalRedirectacts as a transparent proxy inside the Sling engine,sling:redirectforcefully issues an HTTP 301 or 302 response to the client browser, instructing it to navigate to a new URL. This incurs an additional network round-trip.
- Do not confuse this with
sling:status(Long)- This property works in tandem with
sling:redirect. It defines the specific HTTP status code for the redirection. By default, Sling issues a302 Found. You typically want to configure this as301 Moved Permanentlyfor SEO preservation.
- This property works in tandem with
Single Domain Example: The Anatomy of a Mapping Node
Let us analyze a concrete JCR XML representation of a standard mapping rule for a single domain, https://www.brand1.com.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:Folder">
<https jcr:primaryType="sling:Folder">
<www.brand1.com_443
jcr:primaryType="sling:Mapping"
sling:internalRedirect="[/content/brand1/us/en/$1, /content/brand1/us/en/]"
sling:match="(.+)$"/>
</https>
</jcr:root>If this configuration is installed at /etc/map/https/www.brand1.com_443:
- Inbound Execution: A user makes a request to
https://www.brand1.com/about.html. Sling locates thewww.brand1.com_443node. It evaluates the regex(.+)$against/about.html. The match is successful, and the regex capturing group$1holds the valueabout.html. Sling then evaluates thesling:internalRedirectarray. It substitutes$1into the first string, attempting to resolve/content/brand1/us/en/about.html. If the node exists, routing is complete. - Outbound Execution: An HTL script calls
map("/content/brand1/us/en/about.html"). Sling searches/etc/mapand identifies that the path matches the prefix defined insling:internalRedirect(/content/brand1/us/en/). It strips that prefix from the absolute path. It then applies thesling:matchpattern to the remainder of the string, yielding the final, shortened URL:/about.html.
Multi-Domain Mapping for Multi-Tenant Architectures
Most enterprise AEM architectures do not host a single website. They are multi-tenant environments, hosting dozens or hundreds of disparate sites on a single monolithic AEM cluster (e.g., brand1.com, brand2.com, corporate.com). Multi-domain mapping is where /etc/map demonstrates its sheer necessity.
To facilitate multiple tenants, you construct separate, isolated hierarchy trees under the /etc/map structure based on the hostnames.
Complete Multi-Domain JCR Structure
Consider an AEM instance hosting two distinct brands: Brand 1 and Brand 2. Here is the architectural layout of the /etc/map structure required to route traffic correctly for both domains.
/etc/map
├── http
│ └── localhost_4502 (sling:Mapping) -> Default fallback for Author
│ ├── sling:match = (.+)$
│ └── sling:internalRedirect = [/$1]
├── https
│ ├── brand1.com_443 (sling:Mapping)
│ │ ├── sling:match = (.+)$
│ │ └── sling:internalRedirect = [/content/brand1/us/en/$1, /content/brand1/us/en/]
│ └── brand2.com_443 (sling:Mapping)
│ ├── sling:match = (.+)$
│ └── sling:internalRedirect = [/content/brand2/global/en/$1, /content/brand2/global/en/]When a request arrives at the AEM server, Sling inspects the Host HTTP header.
- If the header is
brand1.com, Sling exclusively utilizes the rules underbrand1.com_443, seamlessly translating/products.htmlto/content/brand1/us/en/products.html. - If the header is
brand2.com, Sling utilizes the rules underbrand2.com_443, translating/products.htmlto/content/brand2/global/en/products.html.
This mechanism completely isolates the routing logic, ensuring that Brand 1 cannot accidentally render content from Brand 2, even if both brands have identically named pages at the root level.
The "localhost.any" Fallback Strategy
What happens if a user accesses AEM via a direct IP address, an internal load balancer health check, or a domain that is not explicitly mapped in the /etc/map tree?
Sling relies entirely on the requested host to find a match. If it cannot find a corresponding node (e.g., it cannot find a node named 10.0.0.55_4503), it bypasses domain-specific mapping entirely. In a strict configuration, this will result in AEM failing to resolve paths and throwing a 404 error for valid URLs.
Best Practice: You must always provision a default, catch-all mapping fallback. This is typically placed under /etc/map/http/localhost.any or /etc/map/http/any.
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="sling:Mapping"
sling:internalRedirect="[/$1]"
sling:match="(.+)$"/>This fallback ensures that if a specific domain mapping is not found, AEM falls back to default JCR path resolution. This is absolutely critical for the Author instance. On Author servers, domain-level outbound mapping is heavily discouraged and fundamentally destructive. Content authors must see the full absolute paths (e.g., /content/brand1/us/en/products.html) to utilize the Sites Console, Page Properties, and side-by-side authoring interfaces effectively. If you aggressively map paths on Author, you will break the foundational authoring UI. Mappings should generally be isolated to the Publish runmode.
Vanity URLs, The Bloom Filter, and Production Performance
Marketing teams frequently demand Vanity URLs. A vanity URL is a brief, memorable alias for a deeper content page (e.g., brand1.com/summer-promo resolving to /content/brand1/campaigns/2026/summer-promo-page). AEM handles these requests natively via the sling:vanityPath property, which can be applied to cq:PageContent nodes.
While vanity URLs appear simple on the surface, their underlying implementation in AEM is fraught with massive performance implications that every Staff engineer must understand.
The Mechanics of JCR Vanity Path Queries
When a request enters AEM and fails to map to a physical node via the /etc/map rules, Sling executes its fallback logic: it checks if the requested URL is a vanity path.
To do this, Sling historically issued a direct JCR SQL2 or XPath query against the entire repository:
SELECT * FROM [nt:base] WHERE [sling:vanityPath] = '/summer-promo'
If the query returned a node, Sling successfully routed the request to that node. If it returned nothing, Sling finally yielded a 404.
The Bloom Filter Bottleneck
In an enterprise environment, evaluating a repository-wide JCR query for every unresolved request is catastrophic. Imagine a Publish instance bombarded with thousands of malicious bot requests for random URLs. Each 404 would trigger an expensive database query, rapidly exhausting resources and bringing down the server.
To mitigate this architectural flaw, Adobe introduced the Bloom Filter for vanity paths.
- Initialization: During startup (and periodically on updates), AEM scans the repository and constructs an in-memory Bloom Filter containing all active
sling:vanityPathstrings. - Execution: When a request arrives, Sling checks the requested path against the Bloom Filter before querying the JCR.
- Probabilistic Nature: Bloom Filters are probabilistic data structures. They can authoritatively state "this item is definitely not present," but they can only state "this item is possibly present."
- The Happy Path: If the filter asserts "definitely not present," AEM bypasses the expensive JCR query and immediately returns a 404, saving immense CPU cycles.
- The Expensive Path: If the filter asserts "possibly present," AEM is forced to execute the slow JCR query to verify.
The Critical Warning: The effectiveness of a Bloom Filter degrades as it fills up. If an organization misuses AEM and creates tens of thousands of vanity paths (e.g., migrating 50,000 legacy URLs using sling:vanityPath), the Bloom filter becomes oversaturated. The false positive rate skyrockets. The filter begins telling AEM that every 404 request is "possibly present" in the repository. AEM then executes the slow JCR query for every single 404 request, completely neutralizing the performance optimization and destroying Publish cluster throughput.
The vanityUrlBlacklist and Mitigation Strategies
You can tune the behavior of vanity paths via the OSGi configuration for the org.apache.sling.jcr.resource.internal.JcrResourceResolverFactoryImpl bundle.
vanityUrlBlacklist: This property allows you to define regex patterns (e.g.,^/apps/.*,^/libs/.*) that explicitly bypass vanity path checking entirely. Properly configuring this blacklist prevents AEM from wasting cycles checking if/apps/granite/core/clientlibs.cssis a vanity URL.vanityUrlBloomFilterMaxBytes: You can increase the memory allocation for the Bloom filter to reduce false positives if you legitimately have many vanity paths, though this is a band-aid solution.
Staff-level Advice: Never, under any circumstances, utilize AEM sling:vanityPath properties for massive-scale legacy redirect management. If you need to redirect 10,000 URLs from a previous CMS platform, you must handle those bulk redirects at the Dispatcher/Apache level utilizing a RewriteMap or at the Edge/CDN level (e.g., Akamai EdgeRedirector or Cloudflare Bulk Redirects). Reserve AEM sling:vanityPath properties exclusively for active, highly visible marketing campaigns that require rapid curation by content authors without deployment pipelines.
Outbound Links and the LinkChecker
Understanding how AEM generates outbound links via ResourceResolver.map() is only half the battle. You must also understand how AEM validates those links before serving them to the client.
The AEM LinkChecker Transformer
AEM employs an aggressive HTML rewriting pipeline. Before AEM flushes HTML to the response stream, the output passes through the LinkChecker transformer. This OSGi service intercepts all href and src attributes in the markup.
The LinkChecker performs two critical functions:
- Validation: It verifies that the target JCR node actually exists and is valid.
- Externalization/Mapping: It implicitly invokes
ResourceResolver.map()on the absolute paths to generate the shortened, SEO-friendly URLs.
Common Broken-Link Scenarios
If the LinkChecker detects that an internal path does not resolve to a valid node, it assumes the link is dead. By default on Publish instances, the LinkChecker will actively remove the href attribute entirely from the <a> tag, or it will append a specific CSS class (like broken-link) depending on the configuration.
This often leads to baffling scenarios for developers:
- Scenario A: You author a link to
/content/brand1/us/en/page. In the AEM Author UI, the link works perfectly. However, on the live Publish site, thehrefis mysteriously blank.- Root Cause: The
pagenode was never activated (published) to the Publish tier. The LinkChecker on the Publish server cannot find the node, flags it as broken, and strips the link to prevent users from hitting a 404.
- Root Cause: The
- Scenario B: Your HTL outputs
/content/brand1/us/en/page.htmlinstead of the expected/page.htmlshort URL.- Root Cause: Your
/etc/mapconfiguration is missing, malformed, or thesling:internalRedirectregex does not precisely match the string passed to the LinkChecker. AEM falls back to outputting the raw, absolute JCR path.
- Root Cause: Your
Debugging Tip: You can disable the LinkChecker for specific links by adding the x-cq-linkchecker="valid" attribute to your HTML tag (e.g., <a href="/my/custom/path" x-cq-linkchecker="valid">). This forces AEM to bypass validation and output the link exactly as authored. This is frequently necessary for dynamically generated paths or proxy endpoints that do not exist as physical JCR nodes.
Extension and Selector Handling in the Resolution Pipeline
Sling dissects incoming URLs into highly granular components to orchestrate script execution. A standard AEM URL is not a monolith; it is parsed into discrete segments.
Consider the URL: https://www.brand1.com/products.printable.a4.html/suffix/path
Sling parses this string as follows:
- Resource Path:
/products(which maps via/etc/mapto/content/brand1/us/en/products) - Selectors:
printable,a4(used to locate specialized HTL scripts likepage.printable.a4.html) - Extension:
html(dictates the response MIME type and the primary script format) - Suffix:
/suffix/path(passed to the underlying component logic)
How /etc/map Treats Extensions
By default, /etc/map string matching includes the file extension. If you map /content/brand1/us/en/(.*) to /$1, the outbound map() execution will translate /content/brand1/us/en/about.html directly into /about.html.
Developers frequently attempt to implement extensionless URLs by stripping the .html extension directly inside the /etc/map configuration. They configure the sling:match to capture only the path without the extension.
If you strip extensions at the Sling Mapping layer, you fundamentally disrupt Sling's script resolution logic, and far more destructively, you corrupt the Dispatcher cache.
Mapping for Extensionless (SEO-friendly) URLs
SEO marketing teams almost universally demand extensionless URLs (e.g., brand1.com/about instead of brand1.com/about.html). They are perceived as cleaner and more authoritative.
Implementing extensionless URLs purely within AEM's /etc/map is architecturally flawed. If you output extensionless links via ResourceResolver.map(), AEM Publish will serve the requests (assuming Sling script resolution defaults to interpreting the lack of an extension as a request for HTML). However, the Dispatcher will cache the rendered files without an extension.
Apache web servers rely on file extensions to determine the MIME type of a file. If the Dispatcher caches a file simply named about instead of about.html, Apache will not serve it with a Content-Type: text/html header. It will likely serve it with a Content-Type: application/octet-stream header. The user's browser will not render the webpage; instead, it will prompt the user to download the file. This is a catastrophic production failure.
The Correct Extensionless Architecture
Instead of fighting AEM's core design, the industry standard is to combine Sling Mapping with Dispatcher Rewrites.
- AEM (/etc/map): Configure your outbound mapping to include the explicit
.htmlextension. AEM must generate and internally link to explicit HTML files.sling:internalRedirect="[/content/brand1/us/en/$1]" sling:match="(.+)$" - Dispatcher Outbound (Apache mod_substitute or AEM Rewriter): When generating the final HTML payload, use Apache
mod_substitute(or a custom AEM Link Rewriter transformer) to strip the.htmlextension from allhrefattributes immediately before the payload is sent to the client's browser. The user sees<a href="/about">. - Dispatcher Inbound (Apache mod_rewrite): When the browser requests the extensionless URL
/about, use Apachemod_rewriteto silently append.htmlbefore checking the Dispatcher cache or proxying the request back to the AEM Publish server.# Dispatcher mod_rewrite configuration RewriteEngine On # Do not append .html if the path targets a known system directory RewriteCond %{REQUEST_URI} !^/apps RewriteCond %{REQUEST_URI} !^/content RewriteCond %{REQUEST_URI} !^/etc RewriteCond %{REQUEST_URI} !^/bin # Do not append .html if the path already has a file extension RewriteCond %{REQUEST_URI} !\.[a-zA-Z0-9]+$ # Silently append .html to the incoming request RewriteRule ^/(.*)$ /$1.html [PT,L]
With this architecture, the AEM Publish server and the Dispatcher caching mechanisms always operate on explicit file extensions, ensuring correct MIME types and script resolution. Simultaneously, the end-user and search engine crawlers only ever interact with clean, extensionless URLs.
Real-World Debugging and Error Logs
Debugging /etc/map configuration issues by randomly modifying JCR nodes and refreshing the browser is a fool's errand. AEM provides a dedicated, purpose-built tool within the OSGi Web Console specifically for this task.
The JCR Resource Resolver UI
Navigate to the Felix Console on your local instance: http://localhost:4502/system/console/jcrresolver
This utility allows you to simulate exactly how Sling evaluates the routing chain, bypassing the browser and the Dispatcher entirely.
1. The Configuration Test (Inbound Resolution)
Use this to debug why a specific URL is throwing a 404 or routing to the wrong page.
- Locate the "Resolve" input field.
- Enter a full URL as a user would type it (e.g.,
https://www.brand1.com/about.html). - Click the Resolve button.
- The UI will output the precise absolute JCR path that AEM resolved the URL to. If the configuration is broken, it will explicitly output
NonExistingResource.
2. The Map Test (Outbound Mapping)
Use this to debug why your HTL links are rendering as long /content/brand1/... paths instead of short SEO URLs.
- Locate the "Map" input field.
- Enter an absolute JCR path (e.g.,
/content/brand1/us/en/about.html). - Enter the domain in the optional "Host" field.
- Click the Map button.
- The UI will output the exact string that AEM will generate.
Pro-tip: Scroll to the bottom of the jcrresolver page. The UI dumps the entire active, compiled resolution mapping table currently stored in memory. When you make a modification to an /etc/map node in CRXDE, and it doesn't seem to take effect, inspect this table. If your new rule isn't visible, AEM has not refreshed the mapping cache. You can force a refresh by restarting the org.apache.sling.jcr.resource OSGi bundle.
Analyzing Real Error Logs
When routing fails in production, the AEM error.log provides clues, provided you know how to interpret them.
Error 1: The Infinite Loop
*WARN* [10.0.0.1 [1695420000] GET /about.html HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl maxCalls (50) exceeded for request path /about.html- Interpretation: AEM intercepted a request and attempted to route it, but an
/etc/maprule or a dispatcher rewrite caused the request to forward back upon itself infinitely. - Resolution: Check your
sling:internalRedirectrules. Ensure you are not mapping a short URL back to the identical short URL.
Error 2: The NonExistingResource Warning
*INFO* [10.0.0.1 [1695420100] GET /broken-page.html HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl service: Resource /content/brand1/us/en/broken-page.html not found- Interpretation: The inbound mapping successfully executed. Sling translated the short URL to a long JCR path. However, when it checked the JCR database, the node did not physically exist.
- Resolution: Verify that the page is actually published. If it is published, verify that the
sling:internalRedirectregex is dynamically appending the.htmlcorrectly.
AEM as a Cloud Service (AEMaaCS) Differences
Migrating from AEM 6.5 (on-premise or Managed Services) to AEM as a Cloud Service (AEMaaCS) fundamentally alters how /etc/map architectures are managed and deployed.
The End of Mutable /etc/map
In legacy AEM 6.5 environments, the /etc/map structure resided in the mutable repository. System administrators could log into CRXDE Lite on a live production Publish instance, edit a regex, and fix a routing issue in real-time.
In AEM as a Cloud Service, runtime modifications to /etc/map are strictly prohibited on the Publish tier. The environment is containerized and ephemeral. Any manual changes would be wiped out upon the next pod restart or deployment. The /etc/map configuration must be deployed via the CI/CD pipeline as code.
Furthermore, mapping configurations inherently contain environment-specific domain names (e.g., dev.brand1.com, stage.brand1.com, www.brand1.com). Deploying these configurations via standard AEM content packages (ui.content) is highly problematic, as you would need different content packages for each environment.
Immutable Provisioning via OSGi RepoInit
The architecturally sound solution in AEMaaCS is to construct the /etc/map structures utilizing OSGi RepoInit scripts.
RepoInit allows you to define JCR structures via plain text scripts bundled directly within your environment-specific OSGi configuration files (your ui.config project). When the AEMaaCS container starts up, the OSGi framework executes the RepoInit script, forcefully creating the nodes and properties before Sling begins accepting traffic.
Here is an example of a RepoInit script designed for the production runmode, defining the /etc/map structure for Brand 1.
File: ui.config/src/main/content/jcr_root/apps/brand1/osgiconfig/config.publish.prod/org.apache.sling.jcr.repoinit.RepositoryInitializer~mapping.cfg.json
{
"scripts": [
"create path (sling:Folder) /etc/map",
"create path (sling:Folder) /etc/map/https",
"create path (sling:Mapping) /etc/map/https/www.brand1.com_443",
"set properties on /etc/map/https/www.brand1.com_443\n set sling:match to (.+)$\n set sling:internalRedirect to [/content/brand1/us/en/$1, /content/brand1/us/en/]\nend"
]
}By leveraging RepoInit, your routing configurations become strictly environment-aware, version-controlled in Git, and seamlessly immutable during the automated Cloud Manager deployment pipeline.
Cheat Sheet
ResourceResolver.resolve(): Translates a public short URL into a backend JCR node path. (Inbound)ResourceResolver.map(): Translates a backend JCR node path into a public short URL. (Outbound)sling:match: The regex pattern used to evaluate the request path.sling:internalRedirect: The hidden proxy target path.sling:redirect: Issues a physical HTTP 301/302 redirect to the browser.- JCR Resolver UI:
http://localhost:4502/system/console/jcrresolver
Best Practices
- Strict Regex Anchors: Always utilize explicit regex anchors
^and$in yoursling:matchproperties to prevent disastrous partial string matches. For example, use^/content/brand1/(.*)$rather than/content/brand1/(.*). - Order of Evaluation: Sling evaluates mapping rules at the same hierarchical level alphabetically by node name. Name your mapping nodes strategically utilizing numeric prefixes (e.g.,
10_specific_brand_match,99_global_catch_all) to ensure deterministic evaluation order. - Environment Specificity: Leverage OSGi RepoInit in AEMaaCS, or meticulously structured Runmode-specific config packages in AEM 6.5, to manage development versus staging versus production domain mappings. Never hardcode a production domain into a global mapping node.
- Monitor the LinkChecker: The internal AEM Link Checker transformer utilizes
ResourceResolver.map(). If your links are suddenly vanishing or showing up as "broken" in the authoring interface, your outbound mappings on the Author instance are misconfigured.
Do's & Don'ts
- DO use
/etc/mapexplicitly to strip deep content root hierarchies (like/content/brand1/us/en) for generating outbound links. - DO rigorously validate all mapping configurations in the Felix Console
jcrresolverUI before committing them to source control. - DO implement massive, bulk SEO 301 legacy redirects in the Dispatcher Apache configuration via
RewriteMap, not via AEM JCR structures. - DON'T configure aggressive, domain-specific
/etc/mapoutbound routing rules on the Author instance; authors fundamentally require absolute JCR paths to navigate the Sites interface and utilize side-by-side editing. - DON'T arbitrarily strip file extensions like
.htmlinside/etc/mapdefinitions. Rely on the Dispatcher and Apachemod_rewriteto handle extensionless URLs to preserve crucial Dispatcher caching mechanics. - DON'T abuse the
sling:vanityPathproperty for bulk URL migrations. Creating thousands of vanity paths will irreversibly saturate the Bloom Filter, forcing JCR queries for every 404, and severely degrading Publish cluster performance.
By rigorously mastering the /etc/map architecture and the ResourceResolver API, you construct a resilient bridge between AEM's backend JCR repository and the frontend user experience. Treat your routing mappings as mission-critical infrastructure, test regex patterns defensively, and always respect the architectural boundaries between AEM's application logic and the Dispatcher's caching tier.
Happy Coding.
Discussion
Loading discussion…
Try a related tool
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.