---
name: aem-headless
description: Design and build headless content delivery on AEM as a Cloud Service — Content Fragment Models, GraphQL persisted queries, CDN caching, CORS/dispatcher, auth and front-end consumers. Use this whenever the user designs Content Fragment Models or fragment references, writes AEM GraphQL queries (…List, …ByPath, …Paginated, _references, _dynamicUrl, filters), creates or calls persisted queries (/graphql/execute.json), tunes Cache-Control for GraphQL, hits CORS or dispatcher 404s on the GraphQL endpoint, authenticates against author or preview, uses @adobe/aem-headless-client-js from Next.js or React, evaluates the OpenAPI Content Fragment delivery APIs, instruments an app for Universal Editor (data-aue-*), asks about SPA Editor or Experience Fragments as JSON, or chooses between headless, hybrid, traditional and Edge Delivery. Also apply it when reviewing a front end that fetches content from AEM, even if the question is only about one query.
metadata:
  technology: AEM
  type: development
---

# AEM Headless Delivery

Headless AEM is a content API contract, not a page renderer: Content Fragment Models are your schema, persisted queries are your versioned endpoints, and the CDN is your runtime. Design models for authors and consumers together, never ship ad-hoc POST queries to production, and treat every model change as a public API change.

## 1. Decide the delivery model first

| Situation | Choose | Why |
|---|---|---|
| Multiple channels (app, web, kiosk) share structured content; a front-end team owns rendering | **Headless** (CF + GraphQL/OpenAPI) | One content source, any renderer |
| Marketers need in-context page editing of a JS app | **Headless + Universal Editor** | Visual editing without AEM owning the markup |
| Mostly pages authored in AEM, a few app-like areas | **Hybrid** (AEM Sites pages + headless islands / CF components) | Keep page authoring, fetch structured data where needed |
| Classic marketing site, heavy AEM component library, AEM-rendered HTML | **Traditional AEM Sites** (HTL/Sling Models) | Mature authoring, no separate front-end hosting |
| New marketing/content site where performance and authoring speed dominate | **Edge Delivery Services** | Document- or UE-based authoring, near-perfect Core Web Vitals |

Do not choose headless just to use React: you take on hosting, SSR, preview, SEO and routing that AEM Sites would give you.

## 2. Content Fragment Models

- Create models under a `/conf/<project>` configuration with **Content Fragment Models** and **GraphQL Persisted Queries** enabled in the Configuration Browser; apply the configuration to the DAM folder (`/content/dam/<project>`) via folder properties.
- Pick data types deliberately: Single-line text (titles, slugs), Multi-line text (rich text → query `html`, `plaintext` or `json`), Number, Boolean, Date and Time, Enumeration (closed lists), Tags, JSON Object (escape hatch — avoid for authored data), **Content Reference** (assets/pages), **Fragment Reference** (other fragments).
- **Fragment reference vs nested fields**: reference when the child is reused, independently governed or large (Author, Product, CTA); inline fields when it has no life of its own. Restrict allowed models on every fragment reference; cap nesting depth (2–3 levels) — deep graphs make slow, uncacheable queries.
- Use **Tab placeholders** to group fields for authors; add validation (required, max length, regex, allowed asset types, unique) at the model, not in the front end.
- Property names are the GraphQL field names: stable camelCase, no renames after go-live.
- **Evolving without breaking consumers**: additive changes only (new optional fields, new models). To replace a field, add the new one, migrate content, update queries, then deprecate. Never change a field's type or name in place; a model change affects every fragment and every persisted query that uses it. Lock models (disable editing) in higher environments and ship changes through code/content packages or an agreed promotion process.
- Organize fragments in DAM folders by domain/locale (`/content/dam/<project>/<locale>/articles`) so queries filter by `_path` and permissions follow folders. Use variations for channel-specific copy, not duplicate fragments.

## 3. GraphQL API and query design

- Endpoints are per configuration (e.g. `/content/cq:graphql/<project>/endpoint.json`, plus a global one). Author uses GraphiQL to build and persist queries; publish/preview serve them.
- Generated fields per model: `<model>List` (offset/limit), `<model>Paginated` (cursor: `first`, `after`), `<model>ByPath`. Prefer `Paginated` for large or infinite lists.
- Request only fields you render; use `_references` to collect linked assets/fragments once instead of re-fetching.
- Images: request `_dynamicUrl` / web-optimized delivery (and, where available, transform arguments) rather than `_publishUrl` of originals. Verify the exact image-delivery fields and arguments against current Adobe docs for your release.

```graphql
query ArticlesByTag($tag: String!, $first: Int = 10, $after: String) {
  articlePaginated(
    first: $first, after: $after,
    filter: { tags: { _expressions: [{ value: $tag, _operator: CONTAINS }] } }
    sort: "publishDate DESC"
  ) {
    edges { node {
      _path slug title publishDate
      heroImage { ... on ImageRef { _dynamicUrl width height } }
      author { ... on AuthorModel { name } }
    } }
    pageInfo { endCursor hasNextPage }
  }
}
```

## 4. Persisted queries are mandatory in production

- Ad-hoc `POST` GraphQL is not cacheable by the CDN or dispatcher. Persist every production query and call it with `GET`: `/graphql/execute.json/<project>/articles-by-tag;tag=news;first=10` (variables are `;`-separated and URL-encoded).
- Persist via GraphiQL or `PUT /graphql/persist.json/<project>/<query-name>`; keep query source in the front-end repo and promote them through environments like code (CI job or package), never hand-typed per environment.
- Version names when changing shape (`articles-by-tag-v2`) so old app builds keep working.
- Set **cache headers per query** (`cache-control`, `surrogate-control`, `stale-while-revalidate`, `stale-if-error`) in GraphiQL or the persist request; long CDN TTLs + stale-while-revalidate for catalogs, short TTLs for news. Publishing invalidation behaviour differs from Sites pages — verify how your environment purges persisted-query responses before promising "instant" updates.
- Keep variable cardinality low (don't pass free text or timestamps as variables) or every request is a cache miss.

## 5. CORS, dispatcher and authentication

- CORS: add an OSGi `com.adobe.granite.cors.impl.CORSPolicyImpl~<app>.cfg.json` with explicit `alloworigin` / `alloworiginregexp` for known origins, allowed methods `GET, HEAD, OPTIONS`, and required headers. No `*` with credentials.
- Dispatcher: allow `GET` on `/graphql/execute.json/*` (and the endpoint only on author/preview), cache `.json` responses, pass through `Authorization` only where needed, and deny the persist/admin paths on publish.
- **Publish**: anonymous for public content (use CUGs/permission-sensitive caching only if you must). **Author and Preview**: authenticated — service credentials / technical account tokens for servers, local development tokens only for dev. Never ship a token to a browser bundle; proxy authenticated calls through your server (Next.js route handler / server component).

## 6. Consuming AEM from a front end

```js
// server-side only (e.g. Next.js server component)
import AEMHeadless from '@adobe/aem-headless-client-js';
const aem = new AEMHeadless({ serviceURL: process.env.AEM_HOST, endpoint: '/content/cq:graphql/project/endpoint.json' });

export async function getArticles(tag, after) {
  const { data } = await aem.runPersistedQuery('project/articles-by-tag', { tag, first: 10, after });
  return data.articlePaginated;
}
```

- Render on the server (SSR/SSG/ISR) for SEO; revalidate with a TTL aligned with the query's cache headers, or on-demand from a publish webhook/event.
- Map CF JSON to view models in one module; components never read raw GraphQL shapes.
- Rich text: render `json` or sanitized `html`; resolve internal links/references in one place.
- Newer **OpenAPI-based delivery APIs** for Content Fragments (and Assets) exist on AEM as a Cloud Service; they may suit REST-style consumers. Check current availability, auth model and caching in Adobe's docs before choosing them over GraphQL.
- Use the **Preview service/tier** for "see before publish" in your app: point a preview build of the front end at the preview host.

## 7. Editing experience

- **Universal Editor** is the current path for in-context editing of headless apps: instrument markup with `data-aue-resource` (a URN to the fragment), `data-aue-prop`, `data-aue-type` (`text`, `richtext`, `media`, `reference`, `container`, `component`) and `data-aue-label`, plus the connection meta tag and UE script. Only add instrumentation in the editing/preview build. Confirm attribute names and setup against current docs.
- **SPA Editor** is deprecated for new projects in favour of Universal Editor — verify Adobe's current guidance before proposing it; maintain existing SPA Editor apps but don't start new ones.
- **Experience Fragments** can be delivered as plain HTML (`.plain.html`) or JSON (model export) to reuse AEM-authored layout in other channels; treat them as HTML snippets, not structured data.

## Deliverable format

1. Decision (headless / hybrid / traditional / EDS) with the deciding reasons.
2. Model diagram: models, fields (name, type, required, validation), references and allowed models.
3. Folder and `/conf` layout, locales, permissions.
4. Persisted query list: name, GraphQL, variables, cache headers, consumer.
5. Delivery config: CORS, dispatcher filters/cache rules, auth per tier.
6. Front-end integration: fetch module, rendering mode, revalidation, preview, UE instrumentation.
7. Evolution plan: how models and queries change without breaking live consumers.

## Anti-patterns to reject

- Client-side `POST` GraphQL queries in production — persist and `GET` them.
- Tokens or author URLs in browser bundles; querying author from a public app.
- One mega-query pulling entire fragment graphs; unbounded `List` without limit/pagination.
- Renaming or retyping model fields in place; editing models directly in production.
- JSON Object fields as a substitute for modelling; unrestricted fragment references.
- `alloworigin: *` CORS; dispatcher allowing `/graphql/persist.json` on publish.
- High-cardinality variables that defeat caching; zero cache headers on persisted queries.
- Starting new SPA Editor projects; inventing API paths or fields instead of checking current Adobe docs.
