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
- Who should read this
- Prerequisites
- How context-aware configuration works
- Hands-on: build a site settings configuration
- Step 1: Define the annotation interface
- Step 2: Export the package and check the bnd plugin
- Step 3: Mark the context root and store the values
- Step 4: Read the settings in a Sling Model
- Step 5: Verify two sites with different values
- Context-aware configuration vs OSGi factory configs
- Common mistakes and troubleshooting
- Hands-on challenge
- Key takeaways
- Knowledge check
- Frequently asked questions
- Continue your learning
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
- A project generated from the AEM project archetype. The structure of
core,ui.contentandui.configis explained in the archetype and repository structure lesson. - Working knowledge of Sling Models. See Sling Model and Sightly in AEM.
- Basic OSGi concepts, such as bundles and configurations. See OSGi configs and environment variables on AEMaaCS.
How context-aware configuration works
CAC has four moving parts. You need to understand each one before you write code.

- The annotation interface. A Java
@interfaceannotated with@Configuration. Each element becomes a property, and@Propertysupplies 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:configRefproperty. 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 withresource.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;
}

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

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"/>

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.

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.

Context-aware configuration vs OSGi factory configs
Both mechanisms give you typed configuration, but they solve different problems. The table helps you choose.
| Aspect | Context-aware configuration | OSGi factory configuration |
|---|---|---|
| Purpose | Settings that differ by site, branch or content subtree | Settings for a service instance that differs by environment |
| Where values live | /conf/<name>/sling:configs | osgiconfig/config.<runmode>/<pid>.cfg.json under /apps in ui.config |
| Who edits them | Seeded by ui.content, then changed in the repository or in a configuration editor, and published | Developers and DevOps, through a deployment |
| Scope | Context root and its subtree | Whole instance for a run mode |
| Read through | ConfigurationBuilder.as(...) | @Designate and @Reference, or ConfigurationAdmin |
| Inheritance | Falls 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 values | Yes |
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
| Symptom | Likely cause | Fix |
|---|---|---|
| Values always return defaults | sling:configRef missing from the context root, or its value does not match a /conf folder | Check 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 annotation | bnd plugin not run, or the annotation package not exported | Check the Sling-ContextAware-Configuration-Classes header in the bundle manifest |
| ClassNotFoundException for the annotation | Annotation package not exported or not imported by the model bundle | Export the package and rebuild the core bundle |
| Settings stored under /apps | Values placed in ui.apps or ui.config | Move the values to ui.content under /conf |
| Local values are right, but publish shows old values | The changed /conf node was never published | Publish the /conf node, or deploy the change as a new node through ui.content |
| HTL prints nothing | Model path wrong in data-sly-use, or the model is not in the bundle | Use 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 withpackage-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
- Previous lesson: Lesson 08, Scheduled Sling Jobs in AEMaaCS. For the project structure this lesson uses, see Lesson 05, AEM Project Archetype and Repository Structure.
- Next lesson: Lesson 10, Unit Testing AEM with JUnit 5 and AEM Mocks (unpublished draft; see the AEM Cloud roadmap).
- Practice quiz: OSGi services and components.
- Roadmap: AEM Cloud learning roadmap.
Leave a Reply