N
Naveenr.dev
Chapter rw-44
8 min read2026-08-28
📖 AEM - Adobe Experience Manager SeriesChapter rw-44 · 13 chapters

Real-World Scenarios: A Rebrand Silently Broke Every Experience Fragment Created Before It

A site-wide theme consolidation changed a handful of page properties and left dozens of previously-created Experience Fragments visually mismatched — with no errors anywhere, because a ResourceChangeListener had copied those properties at creation time instead of inheriting them live.

Background reading: Experience Fragment Property Propagation: When a ResourceChangeListener Copies Instead of Inherits covers the mechanics behind this incident.

Problem Statement

A site-wide theme consolidation renamed and merged several clientlib theme categories, updating the themeName property on the top-level pages that owned them. The rollout plan accounted for every page directly using those themes. It didn't account for Experience Fragments, because nobody working on the rebrand knew those fragments had ever received a copy of a page's theme property at creation time — from their perspective, an XF's properties were just properties, indistinguishable from something an author had set directly in the dialog.

Within a week of the rebrand going live, visual QA flagged dozens of pages where an embedded fragment rendered with clientlib styling that didn't match the page around it — old accent colors, outdated spacing rules, a component skin that had been retired in the new theme. No errors, no failed builds, nothing in any log — the fragments were rendering exactly as configured, just with a themeName value that had been correct the day each fragment was created and had never been touched since.

Approach and Why

The first step was confirming the theory before touching anything: pull the themeName property directly off a sample of the affected fragments' jcr:content nodes and compare it against the current value on the page each fragment was supposedly derived from. If the fragment's value matched the page's old (pre-rebrand) value, that would confirm the fragments held a stale copy rather than a live reference — and that's exactly what the sample showed, consistently, across every flagged fragment.

Two things needed fixing, matching the two-part pattern this series keeps returning to for silent-staleness incidents:

  1. Immediate remediation: identify every fragment holding a stale copied value and update it to match its actual source page's current value — a one-time backfill, not a code change.
  2. Prevent recurrence and make the next drift visible sooner: the underlying listener wasn't wrong to copy the value at creation time (a live JCR relationship between a fragment and a page genuinely doesn't exist), but the copy needed to record where it came from, so a future audit could check for drift without anyone having to remember the mechanism exists.

Deliberately avoided: an automatic re-sync job that just overwrites every fragment's theme property to match its source page whenever they differ. Some of those differences might be a deliberate author override on a specific fragment, not staleness — silently overwriting that would be a worse bug than the one being fixed.

POC

java
class ExperienceFragmentThemeAuditTest {

    @Test
    void detectsExperienceFragmentsWithStaleThemeProperty() throws Exception {
        Resource sourcePage = mockPageWithProperty("/content/mysite/product-launches/spring", "themeName", "theme-2026-modern");
        Resource fragment = mockFragmentWithProperties(
            "/content/experience-fragments/mysite/product-launches/spring/promo-banner",
            Map.of("themeName", "theme-2023-legacy", "themePropagationSource", "/content/mysite/product-launches/spring"));

        List<String> stale = auditor.findStaleFragments(List.of(fragment), resolver);

        assertThat(stale)
            .as("a fragment whose stamped theme no longer matches its source page's current theme is stale")
            .containsExactly(fragment.getPath());
    }

    @Test
    void doesNotFlagFragmentsWithNoRecordedPropagationSource() throws Exception {
        // fragments created before the source-tracking field existed, or ones
        // an author has since edited directly, should not be swept up in an
        // automated "fix" - they need a human decision, not a script
        Resource fragment = mockFragmentWithProperties(
            "/content/experience-fragments/mysite/faq-answers/general/callout",
            Map.of("themeName", "theme-2023-legacy"));

        List<String> stale = auditor.findStaleFragments(List.of(fragment), resolver);

        assertThat(stale).isEmpty();
    }

    @Test
    void doesNotFlagFragmentsThatMatchTheirCurrentSource() throws Exception {
        Resource sourcePage = mockPageWithProperty("/content/mysite/faq-answers/general", "themeName", "theme-2026-modern");
        Resource fragment = mockFragmentWithProperties(
            "/content/experience-fragments/mysite/faq-answers/general/callout",
            Map.of("themeName", "theme-2026-modern", "themePropagationSource", "/content/mysite/faq-answers/general"));

        List<String> stale = auditor.findStaleFragments(List.of(fragment), resolver);

        assertThat(stale).isEmpty();
    }
}

The first test is the one that mattered for this specific incident — it directly reproduces the rebrand scenario (source page's theme changed, fragment's stamped copy didn't) and confirms the auditor flags exactly that fragment and nothing else.

Implementation

Auditor that walks every fragment under the registered roots and reports drift, without touching anything automatically:

java
public class ExperienceFragmentThemeAuditor {

    List<String> findStaleFragments(List<Resource> fragments, ResourceResolver resolver) {
        List<String> stale = new ArrayList<>();
        for (Resource fragment : fragments) {
            ValueMap props = fragment.getValueMap();
            String sourcePagePath = props.get("themePropagationSource", String.class);
            if (sourcePagePath == null) {
                continue; // no recorded provenance - not something this audit can safely judge
            }
            Resource sourcePage = resolver.getResource(sourcePagePath);
            if (sourcePage == null) {
                continue;
            }
            String currentSourceTheme = sourcePage.getValueMap().get("themeName", String.class);
            String stampedTheme = props.get("themeName", String.class);
            if (currentSourceTheme != null && !currentSourceTheme.equals(stampedTheme)) {
                stale.add(fragment.getPath());
            }
        }
        return stale;
    }
}

Backfill script, run once against the audit's output, requiring a human-reviewed list rather than running unattended against every fragment:

java
void reconcileApprovedFragments(List<String> approvedFragmentPaths, ResourceResolver resolver) throws PersistenceException {
    for (String path : approvedFragmentPaths) {
        Resource fragment = resolver.getResource(path);
        String sourcePagePath = fragment.getValueMap().get("themePropagationSource", String.class);
        Resource sourcePage = resolver.getResource(sourcePagePath);
        String currentTheme = sourcePage.getValueMap().get("themeName", String.class);

        ModifiableValueMap props = fragment.getChild(JcrConstants.JCR_CONTENT).adaptTo(ModifiableValueMap.class);
        props.put("themeName", currentTheme);
        props.put("themePropagationTimestamp", Calendar.getInstance());
    }
    resolver.commit();
}

Rollout Steps

  1. Ran the auditor read-only against the full fragment tree, producing a list of every fragment whose stamped theme diverged from its source page's current value — 41 fragments, all tracing back to the same rebrand.
  2. Manually reviewed the list with the content team to rule out any fragment that had been intentionally customized after creation (none were, in this case, but the check was made explicit rather than assumed).
  3. Ran the reconciliation script against the approved list, updating all 41 fragments in a single controlled batch, verified visually in staging before running against production.
  4. Added themePropagationSource and themePropagationTimestamp to the original listener going forward, so every fragment created from that point on carries its own provenance automatically.
  5. Scheduled the auditor to run monthly and post its findings (not auto-fix them) to the content team's channel, so the next theme change surfaces drifted fragments within weeks instead of via a visual QA pass months later.

Why This Approach Held Up

The monthly audit has since caught two smaller instances of the same drift pattern — both from unrelated content restructuring, not another rebrand — each resolved within days instead of surfacing as a mystery visual bug report. The deliberate choice not to auto-reconcile turned out to matter in practice: one of those two later instances included a fragment an author had genuinely customized on purpose, and the review step caught it before the reconciliation script would have silently overwritten that intentional change.

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.