Skip to main content

Context-Aware Configuration (Sling CA Config)

Sling Context-Aware Configuration (CA Config) lets you store configuration values in the content tree so they resolve differently depending on the content context. Unlike OSGi configurations (which are global or per-run-mode), CA Config values are site-specific, tenant-specific, or even page-specific - without any code branching.

Use cases:

  • Different analytics tracking IDs per site
  • Public integration IDs that vary between tenants (not secrets)
  • Feature flags scoped to a specific brand
  • Social media URLs per regional site

How It Works​

The lookup chain​

When code requests a CA Config value for a given resource, Sling walks up the content tree looking for a sling:configRef property. This property points to a /conf/... path where the configuration is stored.

  1. Start at the resource (e.g., /content/brand-a/en/products/shoe)
  2. Walk up: /content/brand-a/en/products → /content/brand-a/en → /content/brand-a
  3. Find sling:configRef = /conf/brand-a on /content/brand-a
  4. Look for the configuration under /conf/brand-a/sling:configs/
  5. If not found, check /conf/global/sling:configs/ (if configured)
  6. If still not found, use the default values from the annotation

JCR structure​

/conf/
├── brand-a/
│ └── sling:configs/
│ └── com.myproject.core.config.SiteConfig/
│ ├── analyticsId = "UA-12345-A"
│ ├── socialFacebookUrl = "https://facebook.com/brand-a"
│ └── enableNewHeader = true
├── brand-b/
│ └── sling:configs/
│ └── com.myproject.core.config.SiteConfig/
│ ├── analyticsId = "UA-12345-B"
│ └── enableNewHeader = false
└── global/
└── sling:configs/
└── com.myproject.core.config.SiteConfig/
├── analyticsId = "UA-00000"
├── socialFacebookUrl = ""
└── enableNewHeader = false

/content/
├── brand-a/
│ ├── sling:configRef = "/conf/brand-a" ← links to /conf/brand-a
│ └── en/
│ └── home/
├── brand-b/
│ ├── sling:configRef = "/conf/brand-b"
│ └── en/
Do not store secrets in CA Config

CA Config is content and is replicated like content. Store only author-manageable or public values there. Use OSGi configuration with $[env:...] or $[secret:...] placeholders for credentials.


Defining a Configuration Class​

Use Sling CA Config annotations to define a typed configuration:

core/.../config/SiteConfig.java
package com.myproject.core.config;

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

@Configuration(
label = "Site Configuration",
description = "Site-specific settings like analytics, social, and feature flags"
)
public @interface SiteConfig {

@Property(label = "Analytics Tracking ID",
description = "Google Analytics or Adobe Analytics tracking ID")
String analyticsId() default "";

@Property(label = "Facebook URL")
String socialFacebookUrl() default "";

@Property(label = "Twitter URL")
String socialTwitterUrl() default "";

@Property(label = "Enable new header",
description = "Feature flag for the redesigned header")
boolean enableNewHeader() default false;

@Property(label = "Max items per page")
int maxItemsPerPage() default 10;
}
Register CA Config classes in the bundle

The CA Config annotations must be discoverable by Sling. Either add the Sling-ContextAware-Configuration-Classes bundle header for your config package/classes, or use the Sling CA Config bnd scanner plugin (org.apache.sling.caconfig.bndplugin.ConfigurationClassScannerPlugin).

Supported property types​

TypeExampleNotes
String"UA-12345"Default: ""
booleantrue / falseDefault: false
int10Default: 0
long1000LDefault: 0L
double3.14Default: 0.0
String[]{"a", "b"}Arrays of any primitive type

Reading CA Config in Sling Models​

Using ConfigurationResolver​

import org.apache.sling.caconfig.ConfigurationResolver;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.Self;

import com.myproject.core.config.SiteConfig;

@Model(adaptables = Resource.class)
public class HeaderModel {

@Self
private Resource resource;

@OSGiService
private ConfigurationResolver configResolver;

public boolean isNewHeaderEnabled() {
SiteConfig config = configResolver.get(resource).as(SiteConfig.class);
// as() may return null when no configuration resource is bound to this context.
return config != null && config.enableNewHeader();
}

public String getAnalyticsId() {
SiteConfig config = configResolver.get(resource).as(SiteConfig.class);
return config != null ? config.analyticsId() : null;
}
}

Adapting from a Resource​

ConfigurationBuilder can also be obtained by adapting from the current Resource, avoiding an explicit @OSGiService injection:

import javax.annotation.PostConstruct;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.caconfig.ConfigurationBuilder;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.Self;

import com.myproject.core.config.SiteConfig;

@Model(adaptables = Resource.class)
public class HeaderModel {

@Self
private Resource resource;

private SiteConfig siteConfig;

@PostConstruct
protected void init() {
ConfigurationBuilder builder = resource.adaptTo(ConfigurationBuilder.class);
siteConfig = builder != null ? builder.as(SiteConfig.class) : null;
}

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

public boolean isNewHeaderEnabled() {
return siteConfig != null && siteConfig.enableNewHeader();
}
}

Accessing from HTL​

Expose the config values through your Sling Model and use them in HTL:

<sly data-sly-use.header="com.myproject.core.models.HeaderModel"/>

<div data-sly-test="${header.newHeaderEnabled}" class="header-v2">
<!-- New header markup -->
</div>
<div data-sly-test="${!header.newHeaderEnabled}" class="header-v1">
<!-- Legacy header markup -->
</div>

<!-- Analytics snippet -->
<script data-sly-test="${header.analyticsId}">
gtag('config', '${header.analyticsId @ context="scriptString"}');
</script>
tip
Always set @ context inside <script>

HTL's auto-context detection usually picks scriptString inside <script> tags, but set it explicitly to be safe - a value that slips through unescaped can break out of the string literal and execute arbitrary JavaScript. For JSON payloads use @ context="scriptToken" or render the value via a <meta> tag instead.


Nested Configurations​

CA Config supports nested configurations for complex structures:

@Configuration(label = "Navigation Config")
public @interface NavigationConfig {

@Property(label = "Max depth")
int maxDepth() default 3;

@Property(label = "Navigation items")
NavigationItem[] items() default {};
}

@Configuration(label = "Navigation Item")
public @interface NavigationItem {

@Property(label = "Label")
String label() default "";

@Property(label = "Path")
String path() default "";

@Property(label = "Open in new window")
boolean newWindow() default false;
}

JCR structure for nested configs:

/conf/brand-a/sling:configs/
└── com.myproject.core.config.NavigationConfig/
├── maxDepth = 3
└── items/
├── item0/
│ ├── label = "Products"
│ ├── path = "/content/brand-a/en/products"
│ └── newWindow = false
└── item1/
├── label = "Support"
├── path = "https://support.brand-a.com"
└── newWindow = true

Configuration Collections​

For lists of configuration items where each item has the same structure, use configuration collections:

@Model(adaptables = Resource.class)
public class SocialLinksModel {

@Self
private Resource resource;

@OSGiService
private ConfigurationResolver configResolver;

public Collection<SocialLink> getSocialLinks() {
return configResolver.get(resource)
.asCollection(SocialLink.class);
}
}

@Configuration(label = "Social Link", collection = true)
public @interface SocialLink {

@Property(label = "Platform")
String platform() default "";

@Property(label = "URL")
String url() default "";

@Property(label = "Icon")
String icon() default "";
}

OSGi Config vs CA Config​

AspectOSGi ConfigurationCA Config
ScopeGlobal or per-run-modePer-site, per-tenant, per-page
Storage/apps/*/osgiconfig/ (code)/conf/*/sling:configs/ (content)
Managed byDevelopers (code deployment)Authors or admins (content authoring)
Typical useService endpoints, feature toggles, system settingsSite-specific branding, analytics IDs, feature flags
LookupDirect PID lookupContext-aware tree walk
AEMaaCSDeployed via Cloud ManagerStored in content, replicated

Rule of thumb: If the value is the same across all sites, use OSGi. If it varies per site or tenant, use CA Config.


wcm.io Context-Aware Configuration Extensions​

Most enterprise AEM projects use the wcm.io CA Config extensions, which add:

  • Context path strategies - more flexible lookup (absolute parent level, root template)
  • Persistence strategies - storing configs in /conf with AEM-compatible structure
  • Editor UI - a page-level configuration editor for authors
  • Override support - system-wide overrides via OSGi

Installation​

Add the wcm.io CA Config dependencies to your project:

pom.xml (core module)
<dependency>
<groupId>io.wcm</groupId>
<artifactId>io.wcm.caconfig.extensions</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.wcm</groupId>
<artifactId>io.wcm.caconfig.editor</artifactId>
<scope>provided</scope>
</dependency>

Context path strategy​

Configure how CA Config finds the "site root" for a given resource:

ui.config/.../config/io.wcm.caconfig.extensions.contextpath.impl.AbsoluteParentContextPathStrategy~site.cfg.json
{
"levels": [3],
"contextPathRegex": "^/content/([^/]+)/[^/]+$",
"configPathPatterns": ["/conf/$1"],
"templatePaths": ["/conf/myproject/settings/wcm/templates/.*"]
}

This tells CA Config: "The site root is always at depth 3 (e.g., /content/brand/language), and its config lives under /conf/brand/."


Practical Examples​

Analytics tracking per site​

@Configuration(label = "Analytics Configuration")
public @interface AnalyticsConfig {

@Property(label = "Adobe Analytics Report Suite ID")
String reportSuiteId() default "";

@Property(label = "Google Tag Manager Container ID")
String gtmContainerId() default "";

@Property(label = "Enable tracking")
boolean enabled() default true;
}

Feature flags per tenant​

@Configuration(label = "Feature Flags")
public @interface FeatureFlags {

@Property(label = "Enable dark mode")
boolean darkMode() default false;

@Property(label = "Enable chat widget")
boolean chatWidget() default false;

@Property(label = "Enable A/B testing")
boolean abTesting() default false;

@Property(label = "Max search results")
int maxSearchResults() default 20;
}

API keys per environment and site​

@Configuration(label = "Integration Settings")
public @interface IntegrationConfig {

@Property(label = "Maps API Key")
String mapsApiKey() default "";

@Property(label = "Search API endpoint")
String searchEndpoint() default "";

@Property(label = "Recommendation engine URL")
String recommendationUrl() default "";
}

Best Practices​

Use meaningful configuration class names​

The fully qualified class name becomes the JCR node name under sling:configs. Keep packages short and names descriptive.

Provide sensible defaults​

Always set default values in the annotation. Code should work without any configuration being authored.

Keep configurations flat​

Avoid deeply nested configurations. Flat structures are easier for authors to manage and for developers to read.

Version-control initial configurations​

Include default /conf content in your ui.content package so environments start with reasonable defaults.

Use CA Config for author-manageable values only​

Don't put developer-only settings (thread pool sizes, cache TTLs) in CA Config. Those belong in OSGi configurations.


Common Pitfalls​

PitfallSolution
Config values are always defaultsCheck that sling:configRef is set on the site root node
ConfigurationResolver returns nullEnsure the CA Config bundles are installed and active; check OSGi console
Config works on author but not publishReplicate the /conf/ content to publish instances
Nested config items are emptyVerify the JCR structure matches the annotation hierarchy (child nodes under items/)
wcm.io editor not showingCheck that the context path strategy is configured correctly for your site structure
Wrong site config resolvedVerify the sling:configRef points to the correct /conf/ path; check for overlapping context path strategies
Config changes not picked upCA Config caching may delay updates; clear the Sling CA Config cache

See also​