Sling Context-Aware Configuration in AEM: Site-Specific Settings Done Right

Every AEM multi-site project eventually needs site-specific settings. The brand team wants a different analytics ID for each country site, the editorial team wants a newsletter link per language, and the developers want all of it readable from a Sling Model without hard-coded paths. The first instinct is an OSGi factory configuration with a site name in it. That works, but it ties a site decision to the deployment pipeline and to run modes, which know nothing about sites.

Sling Context-Aware Configuration (CAC) solves this differently. You define a Java annotation interface, mark a point in the content tree with sling:configRef, store the values under /conf, and read them through ConfigurationBuilder. The lookup follows the resource’s position in the tree, so the same Sling Model returns the right values for every site.

This lesson builds a SiteSettings configuration for a site named mysite, deploys it through ui.content, and proves that two sites get different values from the same code.

Table of contents

Why this matters

Site settings are content, not code. Changing a tag manager ID or a feature flag for one site should not mean a Java change. Context-aware configuration keeps those values in /conf, which is mutable on AEM as a Cloud Service, while the typed Java interface stays in your code. You get type safety in Java, and the values live with the site content.

It also removes the path-matching code that teams usually write by hand. Instead of a Sling Model that checks getPath().startsWith("/content/mysite/fr"), the model asks for its configuration and the framework walks up the tree.

Who should read this

This lesson is for developers who build Sling Models and services for multi-site or multi-language AEM projects, and for architects deciding where site settings should live. If every site uses the same values, an OSGi configuration is still the simpler choice, and the comparison section explains why.

Prerequisites

How context-aware configuration works

CAC has four moving parts. You need to understand each one before you write code.

Infographic: content tree with sling:configRef on /content/mysite and /content/mysite/fr mapping to /conf/mysite and /conf/mysite-fr, and the lookup order from page context to global fallbacks and the annotation default
The content path decides which /conf node wins. DE has no configRef, so it inherits /conf/mysite. If no node is found, the annotation default is used.
  • The annotation interface. A Java @interface annotated with @Configuration. Each element becomes a property, and @Property supplies the label and description that configuration editors, such as the wcm.io Configuration Editor, show.
  • The context root. A node in the content tree that has the sling:configRef property. Its value is a path under /conf, such as /conf/mysite. The subtree below that node is the context.
  • The configuration node. Under /conf/<name>/sling:configs/<fully-qualified-annotation-name>. The properties on this node hold the values.
  • The reader. ConfigurationBuilder, obtained with resource.adaptTo(ConfigurationBuilder.class), resolves the nearest configuration for the resource and maps it to your interface.

When you call the builder for a resource, the framework starts at that resource and walks up the tree until it finds a context root. It then reads the matching configuration under /conf. If a context root is nested, the nearest one wins for each configuration. This is why the same code returns the French settings under /content/mysite/fr and the US settings under /content/mysite/us.

Note:- For a singleton configuration such as SiteSettings, the first configuration node found wins. The lookup checks the nearest context’s /conf folder and its parents, then parent contexts, then /conf/global, /apps/conf and /libs/conf. Properties missing on the node that wins fall back to the Java defaults, not to the parent’s values, unless you set sling:configPropertyInherit on that node.

Hands-on: build a site settings configuration

You build the feature in the mysite project. The Java package is com.mysite.core.config, and the settings have an analytics ID, a page size and a newsletter flag.

Step 1: Define the annotation interface

Create the file core/src/main/java/com/mysite/core/config/SiteSettings.java. Use @interface, not a regular interface. The element names become the property names in /conf.

package com.mysite.core.config;

import org.apache.sling.caconfig.annotation.Configuration;
import org.apache.sling.caconfig.annotation.Property;

@Configuration(label = "MySite Settings", description = "Settings for one MySite website.")
public @interface SiteSettings {

    @Property(label = "Analytics ID", description = "Tag manager container ID for this site.")
    String analyticsId() default "";

    @Property(label = "Items per page", description = "Number of cards in listing components.")
    int itemsPerPage() default 12;

    @Property(label = "Newsletter enabled", description = "Show the newsletter block in the footer.")
    boolean newsletterEnabled() default false;
}
VS Code with SiteSettings.java open in core/src/main/java/com/mysite/core/config, showing the @Configuration annotation and three @Property methods with defaults
The SiteSettings annotation and its package-info.java in core/…/config. Each method is one property with a default.

Step 2: Export the package and check the bnd plugin

The framework must find your annotation class at runtime. The CAC bnd plugin scans the annotated classes and writes a Sling-ContextAware-Configuration-Classes header into the bundle manifest. Projects generated from the AEM project archetype already have it: the root pom.xml configures the bnd-maven-plugin with this instruction.

<!-- pom.xml (root, excerpt from the archetype's bnd-maven-plugin configuration) -->
<bnd><![CDATA[
-exportcontents: ${removeall;${packages;VERSIONED};${packages;CONDITIONAL}}
-plugin org.apache.sling.caconfig.bndplugin.ConfigurationClassScannerPlugin
-plugin org.apache.sling.bnd.models.ModelsScannerPlugin
]]></bnd>
...
<dependency>
  <groupId>org.apache.sling</groupId>
  <artifactId>org.apache.sling.caconfig.bnd-plugin</artifactId>
  <version>1.0.2</version>
</dependency>

The -exportcontents line exports every package that declares a version. Add a package-info.java next to the annotation so that com.mysite.core.config is exported:

// core/src/main/java/com/mysite/core/config/package-info.java
@Version("1.0.0")
package com.mysite.core.config;

import org.osgi.annotation.versioning.Version;

Older projects that use maven-bundle-plugin add the same class in a <_plugin> instruction and add org.apache.sling.caconfig.bnd-plugin as a plugin dependency. Either way, rebuild and inspect the manifest of the generated bundle. The header must list com.mysite.core.config.SiteSettings. If it does not, the scanner did not run, and the framework cannot map the configuration to your interface.

cd core
mvn clean install
unzip -p target/mysite.core-1.0.0-SNAPSHOT.jar META-INF/MANIFEST.MF | grep -i ContextAware
Terminal building the core bundle with BUILD SUCCESS, then grepping the manifest for Sling-ContextAware-Configuration-Classes: com.mysite.core.config.SiteSettings and the exported package version
The bnd plugin writes the Sling-ContextAware-Configuration-Classes header. If it is missing, the configuration is not registered.

Step 3: Mark the context root and store the values

Mark the root of the site with sling:configRef. The archetype already does this for you. Open ui.content/src/main/content/jcr_root/content/mysite/.content.xml: the jcr:content node of /content/mysite has sling:configRef="/conf/mysite", plus cq:conf, which AEM uses for editable templates and cloud configurations. Every page below it, including /content/mysite/us/en, resolves to /conf/mysite. Use an absolute path as the value.

<!-- ui.content/src/main/content/jcr_root/content/mysite/.content.xml (excerpt) -->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:Page">
    <jcr:content
        jcr:primaryType="cq:PageContent"
        jcr:title="MySite"
        cq:conf="/conf/mysite"
        sling:configRef="/conf/mysite"
        sling:resourceType="mysite/components/page"/>
</jcr:root>

Next, store the values under /conf/mysite. The configuration node name is the fully-qualified name of the annotation. On disk, FileVault writes the sling:configs bucket as _sling_configs.

ui.content/src/main/content/jcr_root/conf/mysite/
└── _sling_configs/
    └── com.mysite.core.config.SiteSettings/
        └── .content.xml
<!-- ui.content/.../conf/mysite/_sling_configs/com.mysite.core.config.SiteSettings/.content.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="nt:unstructured"
          analyticsId="GTM-MYSITEUS"
          itemsPerPage="{Long}12"
          newsletterEnabled="{Boolean}true"/>
CRXDE Lite showing /conf/mysite/sling:configs/com.mysite.core.config.SiteSettings with analyticsId, itemsPerPage and newsletterEnabled properties
After deployment the node is at /conf/mysite/sling:configs. The _sling_configs folder in ui.content becomes sling:configs in the repository.

Keep the defaults in Java and the actual values in /conf. Do not put site values in ui.apps. That package deploys to the immutable /apps tree.

Know how the values change later. The archetype’s ui.content filter uses mode="merge" for /conf/mysite, so a deployment adds new nodes but does not overwrite nodes that already exist. The package seeds the values. After that, change them in the repository: in CRXDE Lite on a local or dev environment, or with an editor such as the open-source wcm.io Context-Aware Configuration Editor. AEM does not ship an editor for custom configurations. On AEMaaCS, publish the changed /conf node so that the publish tier gets the new values.

Step 4: Read the settings in a Sling Model

Adapt the resource to ConfigurationBuilder inside a @PostConstruct method. Keep the mapping in the model, so the HTL template uses plain getters and does not know about CAC.

package com.mysite.core.models;

import com.mysite.core.config.SiteSettings;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.ConfigurationBuilder;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;

import javax.annotation.PostConstruct;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class SiteSettingsModel {

    @Self
    private Resource resource;

    private SiteSettings settings;

    @PostConstruct
    protected void init() {
        ConfigurationBuilder builder = resource.adaptTo(ConfigurationBuilder.class);
        if (builder != null) {
            settings = builder.as(SiteSettings.class);
        }
    }

    public String getAnalyticsId() {
        return settings != null ? settings.analyticsId() : "";
    }

    public int getItemsPerPage() {
        return settings != null ? settings.itemsPerPage() : 12;
    }

    public boolean isNewsletterEnabled() {
        return settings != null && settings.newsletterEnabled();
    }
}

The HTL template uses data-sly-use to load the model. Use the model’s getters, not the raw configuration object, so that the default fallback stays in one place.

<!-- ui.apps/src/main/content/jcr_root/apps/mysite/components/sitefooter/sitefooter.html -->
<footer class="sitefooter"
        data-sly-use.settings="com.mysite.core.models.SiteSettingsModel"
        data-analytics="${settings.analyticsId}">
  <section class="newsletter" data-sly-test="${settings.newsletterEnabled}">
    <h2>Subscribe</h2>
  </section>
</footer>

Some teams prefer the caconfig binding in HTL. The expression ${caconfig['com.mysite.core.config.SiteSettings'].analyticsId} reads the same value without a model. Use it for one-off values. Use a model when the logic grows.

US page on the local author with the newsletter block in the footer and DevTools showing the footer data-analytics attribute GTM-MYSITEUS
The US page reads /conf/mysite: the newsletter block is shown and the footer carries GTM-MYSITEUS.

Step 5: Verify two sites with different values

Create a second context for the French site. Add sling:configRef="/conf/mysite-fr" to the jcr:content node of the French country page, /content/mysite/fr. Store different values under /conf/mysite-fr, and add the new root to the ui.content filter so that the package includes it. Then deploy and compare the rendered output.

<!-- /conf/mysite-fr/_sling_configs/com.mysite.core.config.SiteSettings/.content.xml -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="nt:unstructured"
          analyticsId="GTM-MYSITEFR"
          itemsPerPage="{Long}6"
          newsletterEnabled="{Boolean}false"/>
<!-- ui.content/src/main/content/META-INF/vault/filter.xml (add this line) -->
<filter root="/conf/mysite-fr" mode="merge"/>

Open a page under each site and inspect the rendered attributes. The US page should show GTM-MYSITEUS and the newsletter block. The French page should show GTM-MYSITEFR and no newsletter block. The Java class is the same for both. Only the content changed.

US and French pages side by side with DevTools: US footer has GTM-MYSITEUS and a newsletter block, French footer has GTM-MYSITEFR and no newsletter
Same component, two contexts. FR resolves /conf/mysite-fr: a different analytics ID and no newsletter block.

Context-aware configuration vs OSGi factory configs

Both mechanisms give you typed configuration, but they solve different problems. The table helps you choose.

AspectContext-aware configurationOSGi factory configuration
PurposeSettings that differ by site, branch or content subtreeSettings for a service instance that differs by environment
Where values live/conf/<name>/sling:configsosgiconfig/config.<runmode>/<pid>.cfg.json under /apps in ui.config
Who edits themSeeded by ui.content, then changed in the repository or in a configuration editor, and publishedDevelopers and DevOps, through a deployment
ScopeContext root and its subtreeWhole instance for a run mode
Read throughConfigurationBuilder.as(...)@Designate and @Reference, or ConfigurationAdmin
InheritanceFalls back along the context hierarchy. Property merging is off by default (sling:configPropertyInherit)Not applicable
Change needs deployment?No, after the first deployment seeds the valuesYes

Use OSGi configs for secrets, endpoints and thread pools. Use CAC for anything an author should change per site. A common mistake is to use an OSGi factory config with a site name as a key, which forces a deployment for a content change.

Common mistakes and troubleshooting

SymptomLikely causeFix
Values always return defaultssling:configRef missing from the context root, or its value does not match a /conf folderCheck the property on the context root and the node name under /conf. The Web Console page /system/console/slingcaconfig shows which configuration a content path resolves to
Configuration not found for the annotationbnd plugin not run, or the annotation package not exportedCheck the Sling-ContextAware-Configuration-Classes header in the bundle manifest
ClassNotFoundException for the annotationAnnotation package not exported or not imported by the model bundleExport the package and rebuild the core bundle
Settings stored under /appsValues placed in ui.apps or ui.configMove the values to ui.content under /conf
Local values are right, but publish shows old valuesThe changed /conf node was never publishedPublish the /conf node, or deploy the change as a new node through ui.content
HTL prints nothingModel path wrong in data-sly-use, or the model is not in the bundleUse the fully-qualified model class name and check the bundle is active

Hands-on challenge

Add a newsletterUrl string property to SiteSettings, set it for both sites, and render it as an href in the footer. Then add a third site under /content/mysite/de with sling:configRef="/conf/mysite-de", but do not create a SiteSettings node under /conf/mysite-de. Predict the output before you load the page. The German page shows the US values, because the lookup falls back to the parent context /content/mysite and its /conf/mysite node. Finally, add a SiteSettings node to /conf/mysite-de with only analyticsId and sling:configPropertyInherit="{Boolean}true", and confirm that the German page now shows its own ID with the US values for the other properties.

Key takeaways

  • Site-specific settings belong under /conf, not in /apps.
  • Mark the context root with sling:configRef, and point it at a folder under /conf.
  • Define the settings as a Java @interface, export its package with package-info.java, and check that the CAC bnd plugin wrote the manifest header.
  • Read values with ConfigurationBuilder.as(...) in a Sling Model, and keep HTL free of CAC code.
  • Use OSGi factory configs for deployment-time values such as secrets and endpoints.

Knowledge check

1. What property marks the root of a context-aware configuration?

sling:configRef. Its value is an absolute path under /conf, such as /conf/mysite.

2. Where are the values of an annotation stored?

Under /conf/<name>/sling:configs/<fully-qualified-annotation-name>. FileVault writes the bucket as _sling_configs on disk.

3. Why do you need the CAC bnd plugin?

It scans the annotated classes and writes the Sling-ContextAware-Configuration-Classes header, which the framework uses to find the configuration classes.

4. When should you use an OSGi factory config instead?

For values that change per environment or per service instance and that developers deploy, such as secrets, endpoints and pool sizes.

Frequently asked questions

Where does sling:configRef go on a page?
Put it on the jcr:content node of the site root page, as the archetype does for /content/mysite. Every resource below that page resolves the configuration from there.

Does CAC replace OSGi configuration?
No. They solve different problems. Keep OSGi configuration for services and use CAC for site content settings.

Can an author break the site by changing a value?
Only as far as the value is used. Validate values in the model, and provide defaults so that an empty field does not break rendering.

Do I need a restart after changing a value?
No. The configuration is read from the repository on each lookup, so the next request sees a changed value. On publish, the value changes when the /conf node is published. Pages cached in the Dispatcher or the CDN show the old value until that cache expires or is flushed.

Continue your learning

Spread the love

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.