---
title: "Adobe CQ5 vs Sanity: Architecture, TCO & Migration Guide"
slug: adobe-cq5-vs-sanity-architecture-tco-migration-guide
date: 2026-08-28
author: Abdul Wahab
categories: [Migration Guide, General]
excerpt: "A technical comparison of Adobe CQ5/AEM and Sanity covering architecture, real TCO numbers, and a step-by-step migration framework for teams evaluating a platform switch."
tldr: "AEM is a monolithic Java/JCR platform with high TCO; Sanity is a headless, API-first CMS with lower cost and broader developer talent. Migration requires a full content model transformation, not a lift-and-shift."
canonical: https://clonepartner.com/blog/adobe-cq5-vs-sanity-architecture-tco-migration-guide
---

# Adobe CQ5 vs Sanity: Architecture, TCO & Migration Guide


# Adobe CQ5 vs Sanity: Architecture, TCO & Migration Guide

If you're evaluating Adobe CQ5 (now Adobe Experience Manager) against Sanity, the short answer is: **these are fundamentally different architectures built for different eras of content management.** CQ5/AEM is a monolithic, Java-based digital experience platform designed for page-centric publishing. Sanity is an API-first, headless content platform built around structured, reusable data.

The decision to move off CQ5 is rarely about a single missing feature. It is driven by the friction of maintaining an aging monolith — engineering teams spending more time managing OSGi bundles, configuring Dispatcher caching, and upgrading Apache Jackrabbit repositories than building digital experiences. If you are truly on CQ5, budget this as a content and integration re-platform, not a routine version upgrade.

This guide breaks down the real architectural differences, honest cost comparisons, a content-type mapping reference, and a practical migration framework for teams evaluating a platform switch.

## What Is Adobe CQ5 (AEM)?

**Adobe CQ5** — short for Adobe Communiqué 5 — is the legacy name for what is now **Adobe Experience Manager (AEM)**. CQ5 originated as Day CQ, developed by Swiss-based Day Software starting in 2008. Adobe acquired Day Software in 2010 and renamed the product Adobe CQ5. After releasing version 5.5, Adobe rebranded it as Adobe Experience Manager in 2013.

AEM is an enterprise Digital Experience Platform (DXP) that combines content management, digital asset management, personalization, workflows, and analytics integrations. The platform runs on a Java stack — specifically OSGi bundles, Apache Sling for request handling, and the **Java Content Repository (JCR)** for storage.

The term "CQ5" survives because Adobe kept that original UI present in newer AEM releases so customers could migrate without retraining authors all at once. Many teams say "CQ5" even when the real question is whether to stay in the Adobe world or redesign the content layer entirely.

**The product line as it currently stands:**

| Version | Status |
|---|---|
| CQ5 (5.x) | End of Life — unsupported, no longer sold |
| AEM 6.5 | Core support ends February 2027 (on-prem); extended support until February 2028 |
| AEM 6.5 LTS | Long-term support release for organizations continuing on-prem or AMS hosting |
| AEMaaCS | Adobe's cloud-native version; continuous updates, evergreen delivery model |

> [!WARNING]
> **CQ5 is end-of-life.** Adobe CQ5 and earlier CRX versions are End of Life and no longer officially sold by Adobe. If you're still on CQ5 proper (versions 5.x), you're running unsupported software with unpatched security vulnerabilities. AEM 6.5 on-premises follows: Adobe announced end of general support for February 2027, with extended support available until February 2028. Both deadlines are driven by Java 8 and 11 reaching end-of-life upstream.

## What Is Sanity?

Sanity is a headless CMS that allows developers and content teams to manage structured content separately from the frontend application.

Sanity's architecture is built around three core components:

- **Sanity Content Lake:** A cloud-hosted, real-time datastore. It stores all structured content and assets in a format that can be instantly queried and delivered via APIs. Content is stored as JSON documents, not HTML, making it reusable across any frontend.
- **Sanity Studio:** An editing environment written in React and fully customizable. Instead of rigid templates, Sanity Studio lets you define your content model using JavaScript or TypeScript. The Studio is open-source and can be hosted anywhere, but it connects to Sanity's hosted APIs and Content Lake.
- **GROQ (Graph-Relational Object Queries):** Sanity's native query language for filtering, joining references, and shaping JSON responses at query time. Sanity also exposes REST and GraphQL APIs.

**Hard constraints to know before evaluating Sanity:**

- The Content Lake backend is Sanity-hosted. There is no self-hosted option for the content repository.
- Studio requires HTTP/2 or later. TLS-inspecting proxies can break the Studio interface.
- Growth plans cap datasets at 50,000 documents and 10,000 unique attributes. Mutation rate limits are 25 requests per second per IP with a 4 MB mutation body cap.
- Cross-dataset references are only available on Enterprise plans and only between datasets in the same project.

## Architecture Comparison: JCR Monolith vs Content Lake

This is the most important technical distinction between the two platforms, and the one that shapes every downstream decision — from content modeling to migration complexity.

### AEM's Java Content Repository (JCR)

Adobe CQ5/AEM is built on a highly specific Java stack, often referred to as the **Sling/JCR/OSGi** stack:

- **Apache Jackrabbit (JCR):** A JCR is a type of object database tailored to storing, searching, and retrieving hierarchical data. The repository built into AEM is called CRX — Adobe's implementation of the Content Repository Specification for Java Technology 2.0 (JSR-238). Everything in CQ5 — content, configuration, and code — is stored as a node in this tree.
- **Apache Sling:** A web framework that maps HTTP request URLs directly to nodes in the JCR. Sling resolves the URL, finds the content node, and then finds the script (historically JSP, now HTL/Sightly) required to render it.
- **OSGi (Apache Felix):** The module system for Java. It allows developers to load and unload Java bundles dynamically without restarting the server.
- **JCR-SQL2:** A domain-specific language used to query JCR nodes. The syntax resembles SQL but operates on tree structures, with content organized in a clear parent-child relationship that mirrors the URL structure of a website.

> [!WARNING]
> **The Monolithic Bottleneck:** Because Sling maps URLs directly to the content tree, CQ5 is inherently page-centric. Reusing a piece of content across a mobile app, a smart watch, and a website requires complex workarounds or heavy reliance on AEM Content Services. Adobe's GraphQL API is designed specifically for Content Fragment delivery — it does not expose the full JCR repository as a generic headless API. This distinction is critical: if your content is in standard CQ5 page components rather than Content Fragments, the "headless AEM" path requires significant re-authoring before migration.

### Sanity's Content Lake

Sanity abandons the page-tree and the rendering layer entirely. It provides a hosted database and a customizable editing interface, leaving the frontend implementation up to you.

Key characteristics:

- **Document-based storage.** Content lives as typed JSON documents, not tree nodes. Each document has an `_id`, a `_type`, and a schema-defined set of fields.
- **Schema-as-code.** Sanity's schemas live in your repo, so developers can ship new content types through pull requests — just like any other code change.
- **Real-time by default.** Content Lake supports real-time updates — changes made in the Studio are instantly reflected in apps and websites.
- **Frontend-agnostic.** No rendering layer is bundled. Compatible with Next.js, React, Vue, Svelte, Gatsby, Astro, or any framework.

Under the hood, transactions land in a document store and GROQ queries read from a search store. The API CDN serves cached reads from a separate CDN hostname and can continue serving the last cached content for up to two hours if the Content Lake is unavailable.

### Side-by-Side Comparison

| Dimension | AEM (CQ5 lineage) | Sanity |
|---|---|---|
| **Content storage** | JCR (hierarchical tree) | Content Lake (document store) |
| **Content model** | Node types + dialog XML/Coral UI | JavaScript/TypeScript schema definitions |
| **Query language** | JCR-SQL2, QueryBuilder, GraphQL (Content Fragments only) | GROQ, GraphQL |
| **Rendering** | Server-side (HTL/Sightly, JSP) or headless via Content Fragments | None — bring your own frontend |
| **Deployment** | On-prem, Adobe Managed Services, or AEMaaCS | Fully managed SaaS (no self-hosted backend) |
| **Primary language** | Java (OSGi, Sling) | JavaScript/TypeScript |
| **Content–code coupling** | High (content paths map to rendering scripts) | Low (content is pure structured data) |
| **Real-time collaboration** | Author-publish replication model (async) | Real-time by default |
| **Localization** | Language Copy, MSM blueprint/live copy | Locale fields or multi-dataset patterns |
| **Workflow/approvals** | Mature workflow engine with role-based gates | Basic publish states; custom workflows require integration |
| **Self-hosted option** | Yes (on-prem or AMS) | Studio only; Content Lake is SaaS-only |

### Querying: JCR-SQL2 vs GROQ

Extracting data from CQ5 programmatically requires JCR-SQL2 or XPath queries, which are notoriously difficult to index and optimize at scale.

**CQ5 JCR-SQL2 query:**
```sql
SELECT * FROM [cq:PageContent] AS s 
WHERE ISDESCENDANTNODE(s, '/content/mysite/en/articles') 
AND s.[cq:tags] = 'technology'
```

**Sanity GROQ query:**
```groq
*[_type == "article" && "technology" in tags[]->slug.current] {
  title,
  "authorName": author->name,
  publishedAt
}
```

GROQ's ability to traverse references (`->`) and project exactly the data the frontend needs reduces payload sizes and eliminates the need for complex backend aggregation layers. Sanity supports GraphQL as well, but GROQ is preferred when a single query must both filter and reshape content into a non-standard shape — GraphQL requires schema-defined types for every projection, while GROQ projections are defined inline at query time.

## AEMaaCS vs. Sanity: The Current Forcing Function

The most common migration decision in 2025 is not "CQ5 vs. Sanity" — it is "should we upgrade to AEMaaCS or exit the Adobe ecosystem entirely?" This comparison deserves its own analysis.

**AEMaaCS (Adobe Experience Manager as a Cloud Service)** is Adobe's cloud-native delivery model. It is architecturally distinct from AEM 6.x in ways that make upgrading from on-prem a re-architecture project, not a version bump:

- **`/apps` and `/libs` become read-only.** Any custom code that expects to write to these paths at runtime fails. This breaks a large class of CQ5/6.x customizations.
- **No more static deployment of bundles.** AEMaaCS uses continuous delivery via Cloud Manager pipelines. The deployment model is fundamentally different from AEM 6.x OSGi bundle management.
- **Immutable content repository for code.** Application code and configurations are deployed as Docker containers, not as content packages.
- **Author and Publish tiers are auto-scaled.** This removes manual JVM tuning but removes control over the scaling model.

| Decision Factor | AEMaaCS | Sanity |
|---|---|---|
| Existing Adobe suite investment | High fit | Low fit |
| Custom OSGi bundle dependencies | Requires refactor | Clean break |
| Need for MSM / Language Copy | Native | Requires custom architecture |
| Content Fragment-heavy architecture | Smooth migration | Good mapping |
| Page-component-heavy architecture | Manageable | Requires content restructuring |
| Frontend team language | Java | JavaScript/TypeScript |
| On-prem/private cloud requirement | AMS option | Not available |
| Budget ceiling | Six figures minimum | Starts free; scales with usage |

**The practical split:** Organizations with 10+ years of AEM investment, active Adobe Target and Analytics integrations, and multi-site governance requirements through MSM are better served by AEMaaCS. Organizations whose primary use case is structured content delivery across multiple frontends — and whose teams are JavaScript-native — face a lower migration cost and lower ongoing cost on Sanity.

## Total Cost of Ownership: AEM vs Sanity

Evaluating the [total cost of ownership](https://clonepartner.com/blog/blog/ektron-vs-coremedia-architecture-tco-migration-guide) is often the forcing function behind migration decisions. AEM's pricing is opaque by design. Sanity's is published.

### AEM Cost Structure

AEM pricing is not publicly listed. As a proprietary digital experience platform, AEM licensing starts around $40,000 annually, though costs increase substantially depending on company size, requirements, and modules required.

The license is only part of the picture:

- **Implementation:** Implementation costs for building an AEM site typically range from $500,000 to $1,000,000, requiring Adobe-certified developers. Complex, multi-site implementations can exceed $5,000,000.
- **Talent:** AEM is a Java-based platform that uses OSGi, Sling, and complex repository structures — requiring highly specialized developers. AEM developer pay averages between $57–$60/hour. The pool of certified AEM developers is structurally smaller than the pool of JavaScript developers: AEM certification is vendor-specific, while React/Node.js skills are generalist.
- **Support fees:** Training, third-party integrations, and support fees (15–25% of license price) can push total annual spend above $100,000 before infrastructure.
- **Infrastructure (on-prem):** Author, Publish, and Dispatcher tiers all require provisioning, monitoring, and patching. Scaling for traffic spikes means spinning up new JVMs, which is slow and resource-intensive.
- **Upgrades:** Upgrading a CQ5 instance (e.g., from 6.2 to 6.5) is a project-scale undertaking — regression testing every custom OSGi bundle, updating deprecated APIs, migrating the JCR content repository. These upgrade projects typically require 3 to 6 months of dedicated engineering time.

### Sanity Cost Structure

Sanity offers three pricing tiers:

- **Free:** Genuinely free with no hosting costs for the CMS backend. Includes real-time collaboration, GROQ and GraphQL APIs, and visual editing. Limits: 2 users, 3 projects, 10 GB bandwidth, 25 GB assets.
- **Growth ($15/seat/month):** Adds scheduled drafts, collaboration features, and higher limits. Up to 50,000 documents and 10,000 unique attributes per dataset. Viewers are free; only editors consume seats. A 5-person editing team costs $75/month.
- **Enterprise (custom pricing):** Includes SSO/SAML, audit logs, custom data residency, SLA guarantees, custom API volume limits, cross-dataset references, and dedicated account management.

**Maintenance cost profile:** Sanity's Content Lake requires zero database maintenance. Sanity Studio is a standard npm package — upgrading is generally as simple as bumping the version in `package.json` and resolving standard React/Node dependencies. There is no JVM tuning, no Dispatcher cache configuration, no OSGi bundle compatibility testing.

### Estimated Annual TCO

| Cost category | AEM | Sanity |
|---|---|---|
| **License / SaaS fee** | $40K–$250K+ | $0–$25K (Growth); custom (Enterprise) |
| **Implementation** | $500K–$5M | $50K–$300K |
| **Developer talent** | Scarce (Java/OSGi specialists) | Broad (JavaScript/React) |
| **Infrastructure** | Self-managed or AMS/Cloud fees | Fully managed (Content Lake) |
| **Ongoing maintenance** | 15–25% of license + DevOps overhead | API overage monitoring |
| **Version upgrades** | 3–6 month projects | npm version bump |

> [!NOTE]
> The real cost gap is not in the SaaS line item — it is in implementation and talent. The constraint on AEM is not just rate; it is availability. JavaScript developers who can configure Sanity schemas are available across the full React/Node.js ecosystem. Adobe-certified AEM developers are a specialist segment with vendor certification as the entry bar.

Sanity's lower entry cost does not mean cheap by default. Dataset structure is a real design choice — content cannot be referenced across projects, and cross-dataset references require datasets to live in the same project on an Enterprise plan. Growth plans have hard limits on documents, attributes, and API usage, with mutation rate limits of 25 requests per second per IP and a 4 MB mutation body cap.

> [!WARNING]
> If your security or procurement model requires a self-managed content repository, evaluate Sanity early in the process. The Studio can be hosted anywhere, but the backend is Sanity-hosted APIs plus Content Lake — there is no self-hosted backend option.

## When Each Platform Makes Sense

**AEM holds a structural advantage when:**

- You're deeply integrated into Adobe Experience Cloud (Target, Analytics, Campaign) and the cross-product data flow justifies the cost.
- You need **Multi-Site Manager (MSM)** for dozens of localized sites with inheritance-based governance and blueprint/live copy relationships.
- Your organization has invested heavily in AEM skills and your content operations team has years of institutional knowledge in the platform.
- You need AEM's mature **workflow engine** for complex, multi-step approval chains with role-based gates — Sanity's built-in workflow states are limited to draft/published, and replicating AEM-style approval chains requires third-party integration or custom development.
- Your compliance model requires on-premises hosting or Adobe Managed Services with specific data residency guarantees.

**Sanity holds a structural advantage when:**

- You need **omnichannel content delivery** — website, mobile app, digital signage, AI agents — from a single content model.
- Your team values **schema-as-code** and wants content types versioned alongside application code in the same pull request workflow.
- You want **real-time collaboration** without AEM's author-publish replication lag.
- Developer velocity matters more than suite integration.
- Your budget does not extend to six-figure licenses and specialized Java talent.

> [!TIP]
> Sanity is less suitable if you need a completely no-code CMS or if your team has zero engineering resources. The Studio's flexibility is its strength and its prerequisite — you need developer time to configure it well. A content team that needs an out-of-the-box authoring experience with no schema design phase is better served by a managed platform.

| Signal | Leans AEM | Leans Sanity |
|---|---|---|
| Deep Adobe suite investment | ✅ | |
| Team is Java/OSGi-skilled | ✅ | |
| Content is page-centric only | ✅ | |
| MSM / multi-site governance required | ✅ | |
| Complex approval workflows | ✅ | |
| On-prem or data residency requirement | ✅ | |
| Multi-frontend delivery needed | | ✅ |
| JavaScript/React team | | ✅ |
| Budget sensitivity | | ✅ |
| Content reuse across channels | | ✅ |
| Real-time collaboration needed | | ✅ |
| Speed-to-market priority | | ✅ |
| Schema versioned with application code | | ✅ |

## AEM 6.5 End-of-Support: The Migration Forcing Function

Current AEM 6.5 support timelines:

- **Adobe Managed Services:** Support ends August 31, 2026.
- **On-premises:** Core support ends February 2027; extended support available until February 2028.

AEM 6.5's end-of-support deadline is a direct consequence of Java 8 and 11 reaching end-of-life. The platform will not stop working after support ends, but enterprises will be left managing unpatched security vulnerabilities, compliance gaps, and growing integration limitations without Adobe's engineering response.

Organizations on AEM 6.5 face three paths:

1. **Upgrade to AEMaaCS** — Adobe's cloud-native version. Significant re-architecture required; content and code in `/apps` and `/libs` become read-only in AEMaaCS. Any custom code that expects to make changes there will fail. This is a re-platform within the Adobe ecosystem.
2. **Move to AEM 6.5 LTS** — The long-term support release for organizations planning to continue self-hosting or AMS-hosting their AEM 6.5 workloads. This buys time but does not change the underlying architectural constraints.
3. **Migrate to a modern headless CMS** like Sanity — A clean break from the Java/JCR stack, requiring a full content model redesign but eliminating the ongoing cost of the AEM maintenance cycle.

## Localization and Translation Architecture

Enterprise migrations from AEM to Sanity frequently underestimate the localization re-architecture required. These platforms handle internationalized content through structurally incompatible models.

### AEM Localization Model

AEM uses two primary mechanisms:

- **Language Copy:** A manual or assisted process that copies a content tree from a source locale (e.g., `/content/mysite/en`) to a target locale (`/content/mysite/fr`), after which the translated content is managed independently.
- **Multi-Site Manager (MSM) with Live Copy:** A blueprint/live copy relationship where a primary site (blueprint) pushes changes to regional or locale-specific live copies. MSM allows inheritance to be broken at specific nodes, creating hybrid shared/independent content trees.

Both models are tree-based and tightly coupled to the JCR hierarchy.

### Sanity Localization Patterns

Sanity does not have a native multi-locale content model. Three patterns are used in practice:

1. **Locale fields on a single document:** A `localizations` array within a single document, where each entry contains a locale code and the localized field values. Simplest to implement; loses document-level granularity.
2. **Separate documents per locale:** One document per locale variant (e.g., `article_en`, `article_fr`), linked by a shared `translationId` reference. Enables locale-specific publishing states but requires reference management.
3. **Separate datasets per locale:** Strongest isolation; most expensive operationally. Cross-dataset references require Enterprise plan. Used for markets with strict content governance separation.

The Sanity document internationalization plugin (`sanity-plugin-document-internationalization`) implements the separate-document pattern with tooling for managing translation state and linking locale variants.

**Migration implication:** AEM's MSM live copy relationships — where a field inherits from a blueprint unless explicitly overridden — have no direct Sanity equivalent. This inheritance model must be resolved before migration: decide which content is truly shared (becomes a reference in Sanity) and which content is locale-independent (becomes duplicated documents). Failing to make this decision early produces migrated content where shared components are duplicated across locales, recreating the maintenance burden the migration was meant to eliminate.

## Content Fragment Model to Sanity Schema Mapping

If your AEM implementation uses Content Fragments (introduced in AEM 6.2, significantly expanded in AEM 6.4+), these translate most directly to Sanity document types. This mapping is the cleanest migration path.

### Reference Content Fragment Model (AEM)

A typical AEM Content Fragment model for an article, defined in the Content Fragment Model Editor or as XML configuration:

```xml
<!-- AEM Content Fragment Model: Article -->
<jcr:root xmlns:jcr="..." xmlns:nt="..." xmlns:cq="..."
    jcr:primaryType="nt:unstructured">
  <elements jcr:primaryType="nt:unstructured">
    <title jcr:primaryType="nt:unstructured"
        fieldLabel="Title"
        valueType="string"/>
    <slug jcr:primaryType="nt:unstructured"
        fieldLabel="Slug"
        valueType="string"/>
    <body jcr:primaryType="nt:unstructured"
        fieldLabel="Body"
        valueType="string"
        contentType="text/html"/>
    <author jcr:primaryType="nt:unstructured"
        fieldLabel="Author"
        valueType="reference"
        fragmentPath="/content/dam/mysite/authors"/>
    <tags jcr:primaryType="nt:unstructured"
        fieldLabel="Tags"
        valueType="tags"/>
    <publishedAt jcr:primaryType="nt:unstructured"
        fieldLabel="Published At"
        valueType="calendar"/>
  </elements>
</jcr:root>
```

### Equivalent Sanity Schema

```typescript
// sanity/schemas/article.ts
import { defineType, defineField } from 'sanity'

export const article = defineType({
  name: 'article',
  title: 'Article',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: Rule => Rule.required()
    }),
    defineField({
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title' }
    }),
    defineField({
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [{ type: 'block' }]
      // NOTE: AEM's text/html field maps to Portable Text, not a string field.
      // The body content requires HTML-to-Portable-Text transformation during migration.
    }),
    defineField({
      name: 'author',
      title: 'Author',
      type: 'reference',
      to: [{ type: 'author' }]
      // NOTE: AEM's fragmentPath reference maps to a typed Sanity reference.
      // Author documents must be migrated before articles for referential integrity.
    }),
    defineField({
      name: 'tags',
      title: 'Tags',
      type: 'array',
      of: [{ type: 'reference', to: [{ type: 'tag' }] }]
      // NOTE: AEM's cq:tags map to reference arrays in Sanity.
      // The tag taxonomy must be migrated as its own document type first.
    }),
    defineField({
      name: 'publishedAt',
      title: 'Published At',
      type: 'datetime'
    }),
  ]
})
```

### Field Type Mapping Reference

| AEM Content Fragment field type | Sanity field type | Migration notes |
|---|---|---|
| `string` (single-line) | `string` | Direct mapping |
| `string` (multi-line, plain) | `text` | Direct mapping |
| `string` (multi-line, HTML) | `array` of `block` (Portable Text) | Requires HTML parsing and AST conversion |
| `number` | `number` | Direct mapping |
| `boolean` | `boolean` | Direct mapping |
| `calendar` / datetime | `datetime` | ISO 8601 format; verify timezone handling |
| `reference` (Content Fragment) | `reference` | Target document type must be migrated first |
| `tags` (cq:tags) | `array` of `reference` | Migrate tag taxonomy as standalone documents |
| `content-reference` (asset) | `image` or `file` | Requires DAM asset migration; replace paths with Sanity asset IDs |
| Enumeration | `string` with `options.list` | Map enum values directly |
| Fragment variation | No direct equivalent | Resolve to separate documents or fields |

**For standard CQ5 page components** (not Content Fragments), the same field mapping applies, but extraction requires the Sling JSON endpoint rather than the Content Fragments API, and the nesting structure is significantly deeper.

## How to Migrate from Adobe CQ5/AEM to Sanity

The migration from AEM to Sanity is not a lift-and-shift. It is a **content model transformation.** As with [other JCR-based migrations](https://clonepartner.com/blog/blog/bloomreach-to-wordpress-migration-a-technical-guide), you are moving from a hierarchical, page-centric repository to a flat, document-based content lake. That difference shapes every step.

### Step 1: Audit Your AEM Content Model

Before writing migration scripts, inventory what you actually have:

- **Page templates and components:** Export your `cq:Page` and `cq:Component` definitions. Map which components carry content vs. which are purely structural.
- **Content Fragments vs. page content:** If you're on AEM 6.3+, you may already have Content Fragment models — these translate more naturally to Sanity document types via the field mapping table above. Standard CQ5 page trees require significantly more transformation work.
- **DAM assets:** Catalog your `/content/dam` structure. Sanity stores assets in its Content Lake, so you'll need to migrate binaries and preserve metadata.
- **Taxonomy and tags:** AEM's tag framework (`/content/cq:tags`) needs to be mapped to Sanity's reference-based taxonomy documents.
- **Downstream integrations:** Include every consumer that reads from AEM publish today — search, analytics, forms, translation, personalization, feeds, and internal apps.
- **Active content vs. archived content:** Audit actual page traffic before migrating. Migrating 10,000 pages when only 3,000 are actively linked wastes time and increases transformation risk. Use AEM QueryBuilder to identify pages by last-modified date and cross-reference against analytics data.

AEM's JCR offers a massively flexible content taxonomy compared to relational repositories, but that flexibility produces highly irregular structures. Expect to discover pages where the same "component" has completely different property sets depending on who authored it and when.

### Step 2: Design the Target Sanity Schema

Do not replicate AEM's content tree in Sanity. Instead:

- **Flatten the hierarchy.** AEM stores a blog post as a `cq:Page` node with child `jcr:content` nodes containing component data nested several levels deep. In Sanity, that same post becomes a single `blogPost` document type with typed fields.
- **Extract reusable content types.** Components shared across templates (hero banners, CTAs, author bios) become their own Sanity document types or object types, referenced where needed.
- **Model relationships explicitly.** AEM uses path-based references (`/content/mysite/en/about`). Sanity uses typed references (`{ _type: 'reference', _ref: 'document-id' }`). Design your Sanity document IDs to be deterministic (e.g., a hash of the AEM path) so migration scripts produce stable IDs across reruns.
- **Decide localization strategy early.** Choose between locale fields on a single document, separate documents per locale, or separate datasets per locale. This decision affects permissions, reference structure, and long-term operating cost. It cannot be easily changed after content is loaded.
- **Decide tenancy early.** Whether brands, markets, or environments belong in separate datasets, separate document types, or localized fields affects dataset limits and cross-reference capabilities.

### Step 3: Extract Content from AEM

AEM exposes content through several channels. The right extraction method depends on your AEM version and content type:

- **Sling JSON export:** Append `.infinity.json` to any AEM path (e.g., `http://localhost:4502/content/mysite/en.infinity.json`) to get the full node tree as JSON. This is the fastest way to bulk-export page content but produces extremely noisy output including thousands of internal AEM properties.
- **QueryBuilder API:** Query for all pages of a specific template: `/bin/querybuilder.json?type=cq:Page&path=/content/mysite`
- **AEM GraphQL API:** AEM's GraphQL API is specifically designed to query Content Fragments stored in the JCR repository. If your content is in Content Fragments, this is the cleanest extraction path — the output structure already maps to Sanity field types.
- **Package Manager / VLT:** Export content packages (`.zip`) from CRX for offline processing.

> [!NOTE]
> **Write a custom servlet for extraction.** The default `.infinity.json` dump includes thousands of AEM-specific properties (`jcr:createdBy`, `cq:lastModified`, `sling:resourceType`) that you'll strip out in transformation. A custom Sling servlet lets you clean the data at the source and serialize only the properties you need, reducing transformation complexity substantially.

> [!WARNING]
> Migrations between different CMS architectures never achieve a 1:1 node-to-document mapping. You will face a completely different approach to content hierarchy and storage. Plan for content restructuring, not just data copying. This is the correct mental model regardless of the tooling you use.

### Step 4: Transform Content — Especially Rich Text

This is the most labor-intensive phase. CQ5 stores rich text (from its internal RTE component) as raw HTML strings within the JCR. Sanity does not use HTML for rich text. It uses **Portable Text**, an open-source JSON specification for block content.

Storing HTML in a headless CMS defeats the purpose of structured content — it locks the text to web-based rendering. You must parse CQ5 HTML strings into an Abstract Syntax Tree (AST) and map the HTML tags to Portable Text blocks.

```json
// CQ5 HTML output
{
  "text": "<p>This is <b>important</b> data.</p>"
}

// Required Sanity Portable Text output
[
  {
    "_type": "block",
    "children": [
      { "_type": "span", "text": "This is " },
      { "_type": "span", "text": "important", "marks": ["strong"] },
      { "_type": "span", "text": " data." }
    ]
  }
]
```

Use `@sanity/block-tools` alongside a DOM parser (like JSDOM in Node.js) to automate this conversion. Write custom deserializers for:

- **Internal AEM links** (`/content/mysite/en/page.html`) — must be resolved to Sanity document references or remapped to new frontend URL patterns
- **Embedded images in rich text** — AEM RTE images reference `/content/dam/...` paths; these must be replaced with Sanity asset references after DAM migration
- **Complex tables** — AEM's RTE table HTML often contains colspan/rowspan combinations and inline styles that have no direct Portable Text equivalent and require custom marks or block types
- **Component markers** — Some AEM implementations embed parsys or experience fragment references as HTML comments or data attributes within rich text

AEM rich text fields often accumulate broken markup over years of authoring. Run an HTML lint pass before attempting AST conversion to identify and remediate malformed input that will cause parser failures.

### Step 5: Migrate Digital Assets (DAM)

CQ5 often doubles as a Digital Asset Management system. The migration process:

1. Query `/content/dam` for all asset nodes.
2. Download the original binary files. **Ignore CQ5-generated renditions** — Sanity's image pipeline generates these on the fly via Sanity Image CDN (which uses image transformation parameters in the URL, not pre-generated files).
3. Upload the binaries to Sanity using `client.assets.upload`.
4. Store the returned Sanity Asset ID.
5. Update your transformed content JSON to reference the new Sanity Asset IDs instead of old CQ5 `/content/dam/...` paths.
6. **Preserve metadata.** AEM's DAM stores XMP, IPTC, and custom metadata properties on asset nodes. This metadata lives in the `jcr:content/metadata` child node of each asset. If you don't map these properties to Sanity asset metadata or linked document fields, you lose searchability and rights management data.

**DAM performance note:** Uploading large DAM libraries to Sanity is network-bound. Parallelize asset uploads to the mutation rate limit (25 requests/second per IP), implement exponential backoff on 429 responses, and run the asset migration as a separate phase before content migration to ensure asset IDs are stable and resolvable when content documents are created.

### Step 6: Import, Validate, and Cut Over

Format your transformed data as Newline Delimited JSON (NDJSON) and import using the Sanity CLI:

```bash
# Import transformed content
npx sanity@latest datasets import aem-export.ndjson production --replace

# Run code-based migrations for cleanup
npx sanity@latest migrations create normalize-links

# Validate the entire dataset
npx sanity@latest documents validate
```

For large datasets (100,000+ documents), batch aggressively and monitor limits. Use Sanity's transaction API with `async` or `deferred` visibility modes when you need higher throughput than fully synchronized writes. Use deterministic, stable IDs (e.g., a hash of the AEM node path) so reruns are idempotent.

**Validation checklist:**

- **Diff content counts.** Compare document counts between AEM (via QueryBuilder) and Sanity (via GROQ: `count(*[_type == 'page'])`).
- **Spot-check rich text.** Automated tests should compare rendered HTML output from both systems on a sample of pages.
- **Verify asset integrity.** Check that DAM assets in Sanity match source dimensions, file sizes, and metadata.
- **Test in a dataset fork.** Run the migration in a Sanity dataset copy before touching production.
- **Deploy URL redirects.** AEM's URL structure (often `/content/mysite/en/page.html`) will not match your new frontend routes. A comprehensive redirect map is required before cutover or you will lose search indexing.
- **Run parallel reads before cutover.** Keep AEM as the source of truth during backfill, compare rendered output and key API responses, then narrow the freeze window to the smallest possible delta cut.
- **Define a rollback trigger.** Establish specific, measurable criteria (e.g., error rate exceeds X%, more than Y redirects returning 404) that trigger a rollback to AEM before decommissioning. Document the rollback procedure before cutover begins, not after.

### Rollback Planning

Rollback from a Sanity migration is only possible if AEM remains live through cutover. The rollback window has a hard boundary: once AEM is decommissioned and DNS is fully transferred, rollback requires restoring AEM infrastructure from backup and re-pointing DNS.

Define the rollback trigger conditions before cutover:
- Maximum acceptable 404 rate on redirect validation (e.g., 0.5%)
- Maximum acceptable API error rate from the new frontend
- Maximum acceptable difference in content counts between systems

Keep AEM publish in a frozen (read-only) state during the cutover window. Do not allow new content to be created in AEM after content freeze begins — any post-freeze content in AEM will not exist in Sanity and creates a gap that is difficult to reconcile without re-running the migration.

## Common Migration Pitfalls

These patterns represent structural failure modes observed across CMS-to-CMS migrations, not edge cases:

1. **Underestimating rich text complexity.** AEM rich text is HTML soup — inline styles, embedded images, link variants, component markers. Converting it to Portable Text requires handling every HTML pattern your authors have ever produced. Running a frequency analysis on your actual RTE output before writing deserializers avoids discovering edge cases in production.
2. **Losing DAM metadata.** AEM's DAM stores XMP, IPTC, and custom metadata on the `jcr:content/metadata` node of each asset. Losing these means losing searchability and rights management data. Audit the metadata schema before writing the asset migration.
3. **Skipping the content audit.** Migrating all pages without filtering by active traffic wastes time and increases transformation risk. Cross-reference page paths against analytics sessions before deciding the migration scope.
4. **Replicating AEM's tree in Sanity.** Sanity is not a hierarchical CMS. Forcing a tree structure defeats the purpose of the migration and produces complex, fragile GROQ queries that simulate path traversal.
5. **Forgetting URL redirects.** AEM's URL structure (`/content/mysite/en/page.html`) will not match your new frontend routes. A comprehensive redirect map is a launch-day dependency, not a post-launch task.
6. **Duplicating shared content instead of referencing it.** Content copy-pasted across pages in CQ5 should become one referenced document in Sanity. Failing to identify shared content before migration recreates the same maintenance burden.
7. **Assuming MSM live copy relationships have a Sanity equivalent.** They do not. Blueprint/live copy inheritance must be resolved into either shared references or independent documents before migration. This is a content governance decision, not a technical one.
8. **Ignoring approval workflow redesign.** AEM's workflow engine supports arbitrarily complex, role-gated approval chains. Sanity's built-in states are draft and published. Replicating AEM-style workflows in Sanity requires custom integration with tools like Zapier, a custom API, or a purpose-built workflow service. Plan for this before migration, not after authors start using the new system.

## Migration Trade-offs to Prepare For

Moving to Sanity is a significant change in developer velocity and infrastructure simplicity, but it comes with trade-offs that require organizational preparation:

- **Loss of WYSIWYG page editing:** CQ5 authors see exactly what the web page looks like while they edit (in-context editing via Sidekick/Touch UI). Sanity is data-first. Sanity offers Live Preview and Presentation, but the engineering team must build and maintain the preview routing. Authors used to visual page editing face a workflow adjustment.
- **Front-end ownership:** CQ5 handles routing, rendering, and caching out of the box. With Sanity, your team owns the entire front-end stack — hosting, CDN caching strategies (e.g., stale-while-revalidate), and routing logic. This is a capability gain for engineering teams and a new operational responsibility for teams that relied on AEM to handle rendering.
- **Schema discipline required:** Sanity's Content Lake is schemaless at the database level — it will accept any valid JSON document regardless of schema. Maintaining clean data models falls entirely on the developers defining Studio schemas and enforcing validation rules. Poorly designed schemas accumulate technical debt that makes GROQ queries progressively more complex.
- **No native staging environment:** AEM's author-publish replication model provides a built-in staging layer. Sanity's Content Releases feature (available on Growth and Enterprise) provides scheduled publishing and draft management, but staging environments that mirror production require application-level implementation using Sanity's Perspectives API.

## Choosing Between CQ5/AEM and Sanity: Decision Framework

As with [other hybrid vs. API-first evaluations](https://clonepartner.com/blog/blog/hygraph-vs-sitefinity-architecture-tco-and-migration-guide), the wrong way to compare CQ5 and Sanity is feature-by-feature. The right framework evaluates four structural questions:

1. **Where does content live relative to presentation?** If your content and rendering are tightly coupled (CQ5 components, JSP/HTL templates, Sling URL mapping), migrating is a re-architecture. If you've already separated content into Content Fragments, the migration scope is smaller.
2. **What are your compliance and hosting boundaries?** If your procurement model requires a self-managed content repository, Sanity is disqualified. If on-premises hosting is not a requirement, Sanity's fully managed model reduces infrastructure overhead.
3. **What is your localization model?** If MSM live copy relationships are central to your governance model, AEMaaCS preserves this. Sanity requires redesigning the localization architecture.
4. **Who owns change after launch?** AEM requires Java/OSGi expertise to modify templates, add components, or change rendering logic. Sanity requires JavaScript/TypeScript for schema changes, but those changes live in the same repository as the application code and go through the same pull request workflow.

The industry trend is toward [headless content platforms](https://clonepartner.com/blog/blog/duda-vs-kontentai-architecture-tco-migration-guide) that treat content as a reusable, structured asset independent of presentation. The headless model adapts to more use cases — additional frontends, AI content pipelines, API consumers — with less engineering overhead per new channel. That is the architectural reason organizations migrate, and it is the primary capability difference between the two platforms.

## Frequently asked questions

### Is Adobe CQ5 the same as AEM?

Yes. Adobe CQ5 (Communiqué 5) was developed by Day Software and acquired by Adobe in 2010. It was rebranded as Adobe Experience Manager (AEM) in 2013. CQ5 versions (5.x) are end-of-life and no longer sold. The latest self-hosted version is AEM 6.5, with AEM as a Cloud Service (AEMaaCS) being the current cloud offering.

### How much does AEM cost compared to Sanity?

AEM licensing starts around $40,000/year but enterprise deployments typically cost six figures annually before implementation ($500K–$5M). Sanity offers a free tier, a Growth plan at $15/seat/month, and custom Enterprise pricing. The biggest cost gap is in implementation and developer talent — AEM requires scarce Java/OSGi specialists, while Sanity uses widely available JavaScript developers.

### Can you migrate content from AEM to Sanity?

Yes, but it's not a lift-and-shift. AEM stores content in a hierarchical Java Content Repository (JCR), while Sanity uses a flat, document-based Content Lake. Migration requires auditing your AEM content model, designing a new Sanity schema, extracting content via Sling JSON exports or QueryBuilder, transforming data (especially HTML to Portable Text), and bulk-importing into Sanity.

### When does AEM 6.5 reach end of support?

Adobe Managed Services support for AEM 6.5 ends August 31, 2026. On-premises core support is planned to end by February 2027, with extended support through February 2028. AEM 6.5 LTS extends the self-hosted option, but the underlying Java 8/11 dependencies are themselves approaching end-of-life.

### What is the hardest part of migrating from AEM to Sanity?

The most labor-intensive step is transforming AEM's rich text (stored as raw HTML with inline styles, embedded component references, and broken markup) into Sanity's Portable Text format. Other challenges include flattening AEM's hierarchical content tree, migrating DAM assets with metadata, and building a comprehensive URL redirect map.
