Org Skills

aem-headless

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.

Download .zip Raw Source
When agents use itUse 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.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill aem-headless -a github-copilot
# or with the org installer (adds .github/skills/aem-headless):
npx -y github:AGCO-Global/org-skills add skill aem-headless

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill aem-headless
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill aem-headless -g

Installs the aem-headless-skills plugin, which bundles all AEM / Headless skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-headless-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-headless -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill aem-headless -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-headless -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill aem-headless -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Skill contents

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.
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

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