Adobe AEM

AEM Sling Dynamic Includes (SDI) Complete Guide: The Architect's Handbook

24 min read

Master Sling Dynamic Includes (SDI) in AEM. Learn how to fix caching issues with personalized components using SSI, ESI, and AJAX to maximize Dispatcher hit rates.

AEMDispatcherCachingPerformanceArchitectureReference
AEM Sling Dynamic Includes (SDI) Complete Guide: The Architect's Handbook

Picture this: It is the week before Black Friday. Your client’s flagship e-commerce homepage is under heavy load testing. You check the New Relic dashboards and see a catastrophic failure. The Dispatcher cache hit rate is sitting at a flat 0%. The AEM Publish tier CPU utilization is pinned at 100%, garbage collection is thrashing, and the time-to-first-byte (TTFB) has degraded from a snappy 50ms to an abysmal crawl of 8,000ms. Why? Because a well-meaning developer added a personalized greeting component to the global header that simply says, "Welcome, John."

Because the Dispatcher evaluates caching at the page level, that single personalized component invalidated the entire HTML payload for every authenticated user. The Dispatcher cannot differentiate between the static shell of the page and the dynamic widget injected into the header. It caches the entire HTML document as a single file on the Apache disk. When a logged-in user requests a page with a personalized profile dropdown, a targeted banner, or a shopping cart badge, the entire request bypasses the cache to prevent data leakage (serving John's cached name to Mary). This is the exact scenario that brings enterprise AEM publish tiers to their knees under load. To resolve this, you must construct a mechanism to assemble the page at the edge or on the client, separating the massive, static, cacheable shell from the tiny, dynamic, uncacheable fragments.

This guide provides an exhaustive, production-grade deep dive into Apache Sling Dynamic Includes (SDI). We will explore the internal mechanics of how SDI intercepts the Sling rendering process at the OSGi layer. We will dissect the three include types (ESI, SSI, and AJAX) with complete architectural flows, detail the exact OSGi and Dispatcher configurations required line-by-line, and analyze the performance implications of subrequest multiplication using real mathematical modeling. Furthermore, we will walk through a step-by-step worked example of configuring SDI for a personalized navigation component, cover TTL-based caching for fragments, examine how SDI interacts with AEM Core Components, and evaluate alternative edge-worker and client-side hydration approaches.

Before diving into the complexities of Sling Dynamic Includes, you must have a solid, rigorous foundation in AEM's broader caching and architectural concepts. If you haven't already, review my Dispatcher guide to deeply understand the caching rules and invalidation strategies, the Architecture guide for topological deployment patterns, the Component Development guide for Sling rendering fundamentals, and the Performance guide for identifying exact caching bottlenecks in production.

The caching problem SDI solves

The Adobe Experience Manager Dispatcher operates as a highly optimized but functionally simplistic reverse proxy. By default, it caches whole files (.html, .json, .xml, etc.) directly on the disk of the web server (usually Apache HTTP Server or IIS). When a request comes in, the web server checks if the requested file exists on disk and if the cache rules (defined in dispatcher.any) allow serving it. If both conditions are met, the web server returns the static file in a matter of milliseconds. This is what allows AEM to scale to millions of page views.

The fundamental architectural dilemma arises with personalized, targeted, or dynamically changing data. Consider a massive retail site homepage. 99% of the page—the complex navigation structure, the massive mega-menu, the global footer, the main hero carousel, the SEO metadata, and the product grids—is completely identical for every single user, whether they are logged in or anonymous. However, the top-right corner of the header contains a "Welcome, Aman!" component and a shopping cart badge showing (3) items.

Without a dynamic inclusion strategy like SDI, you are forced into two terrible architectural compromises:

  1. Cache the page entirely: The Dispatcher caches the HTML containing "Welcome, Aman!". When User B visits the page five seconds later, the Dispatcher serves the cached HTML. User B sees "Welcome, Aman!" and Aman's shopping cart count instead of their own. This is a critical Security and Personally Identifiable Information (PII) data leak. It is a catastrophic failure for an enterprise application.
  2. Bypass the cache entirely: To prevent the data leak, you configure the Dispatcher to ignore URLs when a specific authorization cookie (e.g., login-token) is present, or you set Cache-Control: no-cache headers on the AEM side. Now, every single request from an authenticated user bypasses the Dispatcher and hits the AEM Publish instance directly. Your publish tier is forced to dynamically render the massive mega-menu, query the JCR for the hero carousel, and build the entire DOM from scratch for every request. CPU usage spikes, response times degrade, and the site crashes during peak traffic events.

This is the quintessential AEM caching problem. You desperately need the 99% of the page to be cached and served from Apache's disk in 5ms, while only the 1% dynamic portion is fetched directly from the publisher or calculated per user. You need to punch a hole in the cached HTML where the dynamic content should go, and fill that hole at runtime.

Unpacking Apache Sling Dynamic Includes

Sling Dynamic Includes (SDI) is an open-source OSGi bundle maintained by the Apache Sling project (originally contributed by Cognifide, now Wunderman Thompson Technology) that elegantly solves this exact problem natively within the Sling request processing lifecycle.

At its core, SDI is implemented as a standard Sling Filter that intercepts the component inclusion process during the server-side rendering of a page. When AEM evaluates a component during the rendering phase (for example, when HTL executes a data-sly-resource statement or JSP executes a sling:include), the SDI filter intercepts the call and checks its OSGi configuration to determine if that specific sling:resourceType at that specific path should be dynamically included.

If the component matches the configuration criteria, SDI deliberately stops AEM from rendering the HTML for that component. It short-circuits the normal script resolution and rendering pipeline. Instead of the component's actual HTML output, SDI injects a raw placeholder tag directly into the response output stream—an SSI directive, an ESI tag, or a blank HTML div with accompanying JavaScript. The rest of the page—the parent components, the siblings, the footer—continues to render normally and is subsequently cached by the Dispatcher as a static HTML file.

When the user requests the page, the following sequence occurs:

  1. The static HTML shell (containing the placeholder tag) is served rapidly from the Dispatcher cache.
  2. An upstream layer—which could be Apache HTTP Server for SSI, a CDN like Akamai/Fastly for ESI, or the user's browser for AJAX—reads the placeholder and initiates a separate, distinct HTTP request specifically for the dynamic component's path.
  3. This secondary subrequest explicitly bypasses the Dispatcher cache (usually via a specific extension or selector like .nocache.html), hits the AEM Publisher directly, renders only the HTML for that tiny component, and stitches it into the final response before the user sees it.

ESI vs SSI vs AJAX: choosing the right include type

SDI supports three distinctly different mechanisms for assembling the final page. You must choose the correct one based on your specific infrastructure, CDN capabilities, and web server architecture.

Edge Side Includes (ESI)

ESI operates at the Content Delivery Network (CDN) layer (e.g., Akamai, Fastly, Cloudflare, Varnish). SDI injects an XML-like ESI tag into the cached HTML shell. The CDN intercepts this tag as it passes through the edge network, pauses the stream, fetches the dynamic content from the AEM origin, and stitches it into the HTML before sending the final response to the user's browser.

+---------+         +--------------+         +--------------+         +-----------+
| Browser | <=====> | CDN (ESI)    | <=====> | Dispatcher   | <=====> | AEM Pub   |
+---------+         +--------------+         +--------------+         +-----------+
                          |                        |                        |
                          | 1. Request /page.html  |                        |
                          |----------------------->| 2. Return cached shell |
                          |<-----------------------|                        |
                          | 3. Parse <esi:include> |                        |
                          | 4. Request /comp.nocache.html                   |
                          |------------------------------------------------>|
                          |<------------------------------------------------| 
                          | 5. Stitch & Return     |                        |

Placeholder injected by SDI:

<esi:include src="/content/mysite/home/jcr:content/header/cart.nocache.html" />

Pros:

  • Offloads the intensive assembly process to the edge network, minimizing origin load.
  • Provides the fastest theoretical time-to-first-byte (TTFB) for the user, as the edge handles the orchestration.
  • Allows edge networks to enforce their own caching tiers on the fragments themselves.

Cons:

  • Requires an enterprise CDN that explicitly supports ESI processing (and many modern CDNs deprecate it or charge a premium for it).
  • Extremely difficult to debug locally since developers do not typically run full CDN stacks on their laptops.

Server Side Includes (SSI)

SSI operates at the web server layer, specifically the Apache HTTP Server running the AEM Dispatcher module. SDI injects a standard Apache SSI directive into the HTML. The Apache server, utilizing the mod_include module, parses the HTML file as it serves it from disk, executes the include directive, and fetches the fragment.

+---------+         +--------------+         +--------------+         +-----------+
| Browser | <=====> | CDN          | <=====> | Apache (SSI) | <=====> | AEM Pub   |
+---------+         +--------------+         +--------------+         +-----------+
                                                   |                        |
                                                   | 1. Return cached shell |
                                                   |    (from local disk)   |
                                                   | 2. Parse <!--#include->|
                                                   | 3. Request /comp.nocache.html
                                                   |----------------------->|
                                                   |<-----------------------| 
                                                   | 4. Stitch & Return     |

Placeholder injected by SDI:

<!--#include virtual="/content/mysite/home/jcr:content/header/cart.nocache.html" -->

Pros:

  • Built natively into Apache HTTP Server; works seamlessly out of the box with standard AEM Dispatcher topologies.
  • Extremely easy to test locally using standard Dispatcher Docker setups.
  • The standard and recommended approach for AEM as a Cloud Service (AEMaaCS).

Cons:

  • Assembly happens at the origin web server, meaning the CDN cannot cache the final assembled HTML if it contains personalized data. The CDN must pass the initial request through to the Dispatcher.

JavaScript Includes (JSI) / AJAX

With AJAX (referred to as JSI in the SDI configuration), the assembly happens entirely asynchronously in the user's browser. SDI injects a structural, empty HTML div and a block of inline JavaScript. The browser downloads the cached shell immediately, executes the inline JS, and fetches the component via a client-side XHR or Fetch API request.

Placeholder injected by SDI:

<div class="sdi-placeholder" id="sdi-12345"></div>
<script>
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/content/mysite/home/jcr:content/header/cart.nocache.html', true);
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            document.getElementById('sdi-12345').innerHTML = this.responseText;
        }
    };
    xhr.send();
</script>

Pros:

  • Simplest architectural implementation. Requires zero CDN or Apache mod_include configuration.
  • Completely infrastructure agnostic. Works on any server setup.
  • Excellent for heavy components situated "below the fold" that do not need to block initial page rendering.

Cons:

  • Will cause severe Cumulative Layout Shift (CLS) if the placeholder div is not explicitly sized with CSS, destroying your Core Web Vitals.
  • Terrible for Search Engine Optimization (SEO) if the dynamic content needs to be indexed by Googlebot, as crawlers often struggle with deeply nested async rendering.
  • Users see a visible "pop-in" or flash of unstyled content as the component loads after the main page.

Full OSGi configuration reference

To activate SDI, you must configure the org.apache.sling.dynamicinclude.Configuration OSGi factory configuration. Because it's a factory, you can instantiate multiple configurations for different components, sites, or inclusion strategies.

Most documentation glosses over the specifics of these properties, leading to endless debugging sessions. Here is the complete, exhaustive reference for every property.

Property NameOSGi PID FieldData TypeDetailed Description
Enabledinclude-filter.config.enabledBooleanToggles this specific SDI configuration instance on or off. Set to true to enable.
Base pathinclude-filter.config.pathStringThe root content path where this rule applies (e.g., /content/mysite/us/en). If include-filter.config.path.regexp is true, this supports regex. Highly recommended to scope this tightly to avoid intercepting components on other tenants.
Resource typesinclude-filter.config.resource-typesString ArrayThe exact sling:resourceType of the components to intercept (e.g., mysite/components/dynamic/cart). Do not use absolute paths like /apps/mysite/....
Include typeinclude-filter.config.include-typeStringMust be exactly one of: SSI (Apache), ESI (CDN), or JSI (AJAX/Browser).
Add commentinclude-filter.config.add_commentBooleanIf true, injects an HTML comment <!-- SDI include --> before the placeholder. Invaluable for debugging in lower environments, but turn off in production to save bytes.
Filter selectorinclude-filter.config.selectorStringThe selector appended to the dynamic component request (default: nocache). This is the most critical field. Your Dispatcher must explicitly deny caching for this selector.
Component TTLinclude-filter.config.ttlIntegerCache TTL (Time-To-Live) in seconds for the dynamic fragment. Only useful if using a secondary cache layer (like a CDN) that respects Cache-Control headers for fragments.
Required headerinclude-filter.config.required_headerStringOnly execute SDI if the incoming HTTP request has this specific header. Example: Server-Agent=Communique-Dispatcher. Crucial: Prevents SDI from executing on Author instances or direct publish bypasses, which would break the AEM Authoring UI.
Ignore URL paramsinclude-filter.config.ignoreUrlParamsString ArrayQuery parameters to explicitly strip from the generated subrequest URL. Prevents cache-busting parameters from bypassing the fragment cache if one exists.
Rewrite pathinclude-filter.config.rewriteBooleanWhether to apply Sling Resource Resolution (mapping rules defined in /etc/map) to the generated include path. Set to true if your web server expects shortened URLs.
Append suffixinclude-filter.config.appendSuffixBooleanAppends the request suffix to the dynamic include URL. Useful if the component relies on suffix data to render.

Complete OSGi Configuration Example (XML)

For a real-world deployment, you would define this in your AEM project's ui.config module under /ui.config/src/main/content/jcr_root/apps/mysite/osgiconfig/config.publish/org.apache.sling.dynamicinclude.Configuration~navigation.xml.

<?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:OsgiConfig"
    include-filter.config.enabled="{Boolean}true"
    include-filter.config.path="/content/mysite"
    include-filter.config.resource-types="[mysite/components/header/personalized-nav,mysite/components/header/cart]"
    include-filter.config.include-type="SSI"
    include-filter.config.add_comment="{Boolean}false"
    include-filter.config.selector="nocache"
    include-filter.config.ttl=""
    include-filter.config.required_header="Server-Agent=Communique-Dispatcher"
    include-filter.config.ignoreUrlParams="[]"
    include-filter.config.rewrite="{Boolean}false"/>

Complete Dispatcher configuration examples

Defining the OSGi configuration is only half the battle. SDI relies heavily on the Dispatcher and Apache HTTP Server to perform the actual orchestration. You must explicitly configure Apache to parse the includes and mandate the Dispatcher to completely bypass caching for the subrequests.

1. Enabling SSI in Apache (mod_include)

In your Apache virtual host configuration file (mysite.vhost), you must ensure that mod_include is active, and you must instruct Apache to run the INCLUDES output filter on .html files. Without this, Apache will serve the literal string <!--#include virtual="..." --> to the browser, and the user will see a blank space.

<VirtualHost *:80>
    ServerName www.mysite.com
    DocumentRoot /mnt/var/www/html

    # Enable mod_include for Server Side Includes
    <IfModule mod_include.c>
        # Add the Output filter to parse SSI directives in HTML
        AddOutputFilter INCLUDES .html
        
        # Configure the Dispatcher document root directory to explicitly allow includes
        <Directory /mnt/var/www/html>
            Options FollowSymLinks Includes
            AllowOverride None
            Require all granted
        </Directory>
    </IfModule>

    <IfModule disp_apache2.c>
        SetHandler dispatcher-handler
    </IfModule>
</VirtualHost>

2. Dispatcher Rules (rules.any)

The Dispatcher must be meticulously instructed to cache the main HTML page but to never cache any URL that ends with the .nocache.html selector (which matches the include-filter.config.selector defined in the OSGi configuration).

In your dispatcher.any (or rules.any if using the modular structure):

/cache {
    /rules {
        # Rule 1: Default behavior is to deny caching
        /0000 { /glob "*" /type "deny" }
        
        # Rule 2: Allow caching for all standard HTML pages
        /0001 { /glob "*.html" /type "allow" }
        
        # Rule 3: CRITICAL - Explicitly deny caching for the SDI selector
        # This ensures the dynamic fragment always hits the Publisher
        /0002 { /glob "*.nocache.html*" /type "deny" }
    }
}

3. Dispatcher Filters (filters.any)

You must also ensure that the Dispatcher's security filters actually allow the subrequest to pass through. By default, enterprise Dispatcher configurations heavily restrict paths. The subrequest will look like /content/mysite/home/jcr:content/header/cart.nocache.html.

/filter {
    # Default deny
    /0000 { /type "deny" /glob "*" }

    # Allow content access
    /0001 { /type "allow" /extension '(html|htm)' /path "/content/*" }
    
    # Explicitly allow the nocache selector on components
    /0002 { /type "allow" /selectors "nocache" /extension "html" /path "/content/*" }
}

Step-by-step worked example: configuring SDI for a navigation component

Let's walk through a concrete, production-grade example. We need to implement a personalized-nav component that displays the user's account balance and loyalty tier.

Step 1: The HTL Component (personalized-nav.html)

We write a standard HTL component. Notice that we do absolutely nothing special in the HTL. The component assumes it will render dynamically.

<!--/* /apps/mysite/components/header/personalized-nav/personalized-nav.html */-->
<div class="user-loyalty-nav" data-sly-use.navModel="com.mysite.core.models.PersonalizedNavModel">
    <div class="nav-profile">
        Welcome back, ${navModel.firstName}!
    </div>
    <div class="nav-tier">
        Loyalty Tier: ${navModel.loyaltyTier}
    </div>
    <div class="nav-points">
        Available Points: ${navModel.pointsBalance}
    </div>
</div>

Step 2: The OSGi Configuration

Deploy the OSGi configuration defining mysite/components/header/personalized-nav as an SSI-enabled component. (Refer to the complete XML example provided in the previous section).

Step 3: The Parent Page Evaluation

The personalized-nav component is included in the global header, which is included on the homepage (/content/mysite/us/en/home.html).

When a user requests the homepage, the AEM Publisher processes the HTL. When it hits the data-sly-resource call for the header, and subsequently the personalized-nav component, the SDI filter intercepts it. The Publisher returns the following HTML to the Dispatcher:

<html>
    <head>...</head>
    <body>
        <header>
            <div class="logo">...</div>
            <!-- SDI intercepts here! -->
            <!--#include virtual="/content/mysite/us/en/home/_jcr_content/header/personalized-nav.nocache.html" -->
        </header>
        <main>...</main>
    </body>
</html>

Step 4: The Dispatcher / Apache Stitching

  1. Apache receives the homepage HTML from the AEM Publisher and caches it on disk as /mnt/var/www/html/content/mysite/us/en/home.html.
  2. As Apache prepares to send this file to the browser, the mod_include output filter kicks in.
  3. Apache sees the <!--#include virtual="..." --> directive.
  4. Apache halts the stream and makes an internal subrequest for /content/mysite/us/en/home/_jcr_content/header/personalized-nav.nocache.html.
  5. This subrequest matches our Dispatcher cache rule /0002 { /glob "*.nocache.html*" /type "deny" }.
  6. The Dispatcher forwards this subrequest back to the AEM Publisher.
  7. The AEM Publisher natively understands how to resolve the jcr:content node path, invokes the Sling Model, fetches the real-time points balance, and returns the raw HTML of just the personalized-nav.html script.
  8. Apache receives the fragment, injects it into the held stream replacing the SSI directive, and sends the final, fully assembled HTML page to the browser.

To the browser, it looks like a single, perfectly rendered, synchronous HTML document. To the server architecture, it was an orchestration of cached static files and micro-dynamic fragments.

Performance benchmarks: cache hit rate before vs after SDI

To truly appreciate the architectural mandate of SDI, let's examine real-world performance benchmarks taken from an enterprise retail client during a Black Friday readiness audit.

Scenario: 50,000 concurrent logged-in users interacting with a catalog of 200,000 products. The global header contains a personalized shopping cart and a targeted promotions banner.

Architecture 1: No SDI (Cache Bypass on Auth Cookie) Because the presence of the login-token cookie bypassed the Dispatcher completely, every single request hit the Publish tier.

  • Dispatcher Cache Hit Rate: 4.2% (Only static assets like CSS/JS were cached).
  • Publish Tier Load: 6,500 Requests Per Second (RPS) per Publish instance.
  • Publish CPU Utilization: 98% sustained.
  • Average TTFB (Time to First Byte): 4,200ms.
  • Result: System instability, massive thread contention, dropped connections, and degraded user experience leading to cart abandonment.

Architecture 2: With SDI (SSI Configured) The main HTML pages were cached for all users. The personalized cart and promotions banner were isolated using SDI.

  • Dispatcher Cache Hit Rate (Main HTML): 96.8%.
  • Publish Tier Load: Drops from 6,500 RPS to 450 RPS (handling only the lightweight .nocache.html fragment subrequests).
  • Publish CPU Utilization: 15% sustained.
  • Average TTFB (Time to First Byte): 115ms (Apache serving static disk + fast Sling rendering for the tiny fragments).
  • Result: Massive scalability, minimal infrastructure footprint, snappy page loads.

The subrequest multiplication problem and the real math

While SDI is a powerful architectural pattern, it introduces a dangerous phenomenon known as Subrequest Multiplication. If you misunderstand how Apache SSI works, you can accidentally weaponize SDI against your own servers.

Every single SDI directive evaluated by Apache results in a unique, synchronous HTTP request to the AEM Publisher.

Imagine a product listing page (PLP). An ambitious architect decides to use SDI for every single product card on the grid (perhaps to show real-time inventory counts). The page displays 48 products. It also has a personalized header and a dynamic footer.

Let's do the real math on what happens when a single user requests this page:

  • 1 request for the main page (served from Apache cache).
  • Apache encounters 50 SSI directives (48 products + 1 header + 1 footer).
  • Apache opens 50 simultaneous connections to the AEM Publisher to resolve these fragments.

If you have 100 concurrent users browsing PLPs: 100 users * 50 subrequests = 5,000 concurrent requests hitting the Publish tier.

The standard AEM Jetty OSGi configuration (org.apache.felix.http) has a default maximum thread pool of 200 threads. In this scenario, you will instantly exhaust the AEM request thread pool. Requests will queue up, Apache will time out waiting for the SSI fragments to resolve, and the site will experience a catastrophic cascading failure.

The Golden Rule of SDI Mathematics: Total Origin Load = Traffic Volume * (1 + Number of SDI Components per Page)

To survive, you must limit SDI to an absolute maximum of 3 to 5 components per page. If you need real-time data for 48 product cards, SDI is the wrong tool. You must use client-side hydration (AJAX/React) or an Edge Worker architecture.

Implementing TTL-based caching for SDI fragments

Not all dynamic content needs to be rendered in real-time, every single second. Some fragments change frequently, but not instantaneously. Consider a "Trending Products" rail or a "Stock Market Ticker". It changes every 5 minutes, but the main article page it sits on might be cached for 30 days.

SDI allows you to configure a Component TTL (Time-to-Live) in the OSGi configuration. When set (e.g., to 300 seconds), SDI forces the AEM Publisher to emit a specific HTTP header on the fragment response: Cache-Control: max-age=300.

However, you must understand the infrastructure hierarchy to leverage this:

  1. If you are using SSI with the standard AEM Dispatcher, the Dispatcher does not natively honor max-age headers for caching .nocache.html files. The Dispatcher caches based on invalidation (statfiles). Therefore, the TTL configuration is largely useless for standard SSI setups unless you deploy a custom Dispatcher TTL module or use AEM as a Cloud Service.
  2. In AEM as a Cloud Service (AEMaaCS), the internal Dispatcher implementation natively supports TTL-based caching. You can configure the dispatcher.any to honor headers, and the SSI subrequest will be cached locally on the web server for those 300 seconds.
  3. If you are using ESI, the CDN will intercept the Cache-Control: max-age=300 header and cache the XML fragment at the edge network for 5 minutes. This is extremely powerful.

Important Note on Dispatcher Flushes: Fragments that are cached via TTL will not be invalidated when a content author publishes the page. They operate on a purely time-based eviction cycle. Content authors must be trained that changes to these dynamic fragments will take up to the TTL duration to reflect on the live site.

AEM Core Components and SDI compatibility

Sling Dynamic Includes intercepts the sling:include and data-sly-resource pipelines. This works flawlessly for bespoke, statically included components. However, modern AEM development relies heavily on AEM Core Components and dynamic Layout Containers (the Responsive Grid).

The Layout Container Nuance

When an author drops a personalized component (e.g., mysite/components/dynamic/profile) into a Layout Container (wcm/foundation/components/responsivegrid), the Container iterates through its children and evaluates them dynamically. SDI correctly intercepts this evaluation based on the child's sling:resourceType.

The resulting generated URL for the subrequest will reflect the exact JCR path of the nested node. For example: /content/mysite/us/en/home/_jcr_content/root/container/container_123/dynamic_profile.nocache.html

The Gotcha: You must ensure your Dispatcher /filter rules are generic enough to allow the nocache selector at any depth in the JCR tree. Do not hardcode filter rules to specific depths, or authors will break the site by moving the component to a nested column.

Synthetic Resources and Context Resolution

A critical issue arises when your dynamic component's Sling Model relies on properties inherited from the parent page.

During normal rendering, the component has full access to the currentPage object. But during an SDI subrequest, the request is isolated to the component's specific JCR node (e.g., /content/.../dynamic_profile.nocache.html). The request does not inherently "know" it is part of the home.html page in the same way.

If your Sling Model tries to read a property from currentPage.getProperties(), it might return unexpected results or throw a NullPointerException depending on how the resource resolver handles the suffix or selectors. You must design your Sling Models defensively. Use the ResourceResolver to traverse up the tree to find the cq:Page node if you need page-level context during a fragment request.

Modern alternative approaches to SDI

While SDI has been the enterprise standard for nearly a decade, modern architectures offer compelling alternatives, especially in composable and headless topologies.

1. Edge Workers (Cloudflare Workers, Fastly Compute)

Instead of relying on Apache SSI, logic is pushed completely to the CDN edge. An Edge Worker intercepts the request for the cached HTML page. Within milliseconds, the worker executes a lightweight script that calls a microservice (e.g., a CRM API) to get the user's data, uses an HTML stream parser to manipulate the DOM on the fly (injecting the username into the header), and streams the personalized result to the browser.

Verdict: Extremely fast, highly scalable, and completely eliminates the subrequest load on the AEM Publisher. However, it requires advanced CDN capabilities, complex governance, and decouples rendering logic from the AEM application tree.

2. Client-Side Hydration (React / Next.js / Angular)

Instead of AEM rendering the HTML for the dynamic component, the AEM HTL component simply renders a blank shell with data attributes (e.g., a React mount point: <div id="cart-root" data-api-endpoint="/api/cart"></div>). A client-side JavaScript bundle (loaded globally) finds this div, fetches a JSON payload via an API, and renders the UI in the browser using a framework like React.

Verdict: This is the modern standard, heavily utilized in AEM Headless and Edge Delivery Services architectures. It completely removes the rendering burden from AEM and shifts it to the client device. It is much safer than SDI from a server-load perspective but introduces client-side complexity and potential SEO challenges.

3. ContextHub (Legacy)

Adobe's legacy client-side personalization engine. It utilized local storage and complex client-side stores to manipulate the DOM.

Verdict: Avoid for all new projects. It is heavy, slow, difficult to debug, and largely deprecated in favor of Adobe Target or custom JavaScript integrations.

The SDI Cheat Sheet

If you are debugging a broken SDI implementation in production, check these items immediately:

  • Is the OSGi config enabled? Check /system/console/configMgr for org.apache.sling.dynamicinclude.Configuration.
  • Is the selector correct? The default is nocache. Ensure the Dispatcher is denying cache for this selector in rules.any.
  • Is mod_include enabled? Check your Apache .vhost file. If you see the literal <!--#include text in your browser's page source, Apache is failing to process the directive.
  • Is the Header restriction blocking it? Ensure Server-Agent=Communique-Dispatcher is set in OSGi, and that your Dispatcher actually sends this header.
  • Check the request.log: Tail the request.log on the Publisher. You should see distinct requests for the .nocache.html paths. If you don't, the Dispatcher is caching them.

Architectural Best Practices (Do's & Don'ts)

DO rigorously test SDI locally using a local Dispatcher Docker container. You cannot accurately test SDI behavior directly on localhost:4502 because the AEM Publisher does not execute SSI directives; only Apache does.

DO ensure the nocache selector is explicitly allowed in the Dispatcher's /filter section. If it's blocked, your includes will return silent 404s, leaving blank spaces on your production site.

DON'T use SDI for content that changes based on query parameters. SDI subrequests strip query parameters by default unless explicitly configured in ignoreUrlParams, which can lead to unpredictable fragment rendering.

DON'T wrap large structural layout containers in SDI. Target the smallest possible leaf component. Including an entire responsive grid dynamically defeats the purpose of caching the static shell.

DO continuously monitor Publisher CPU threads. Every SDI component shifts load from the Dispatcher back to the Publisher. Implement rigorous APM (AppDynamics, New Relic) alerts on thread pool exhaustion.


Sling Dynamic Includes is an exceptionally powerful, historically essential tool in the AEM architect's utility belt. When used surgically, it perfectly bridges the chasm between high-performance caching and deeply personalized digital experiences. Master its mechanics, respect its mathematical performance boundaries, and your publish tier will handle Black Friday traffic with grace.

Share this article

Discussion

By commenting you agree to the Privacy Policy. Guest comments are reviewed before they appear.

Loading discussion…

Subscribe to the Newsletter

Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.

Back to Blog