Skip to content

Headless GraphQL — AEM 6.5 On-Premise


Yêu Cầu Cài Đặt

GraphQL API có từ AEM 6.5 SP10. Phải cài thêm index package riêng:

AEM VersionYêu cầu
6.5.10 — 6.5.22Cài Content Fragment with GraphQL Index Package 1.0.5
6.5.23+Cài Index Package 1.1.1 (bắt buộc, nếu thiếu sẽ slow/failed queries)
6.5.17+Hỗ trợ optimized GraphQL filtering (cần chạy migration procedure)

Không cài index package = query chạy trên Lucene generic index, rất chậm khi data lớn.


Cách Hoạt Động

AEM tự sinh GraphQL schema từ Content Fragment Models. Mỗi model = 1 GraphQL type, mỗi field = 1 queryable property, fragment reference = nested type. Không cần viết schema thủ công.

Mapping CF Model Field → GraphQL Type

CF Model Field TypeGraphQL TypeGhi chú
Single-line textString
Multi-line textStringCó sub-fields: html, plaintext, markdown
Number (Integer)Int
Number (Float)Float
BooleanBoolean
Date and TimeCalendarISO 8601 string
EnumerationStringVới defined values
Tags[String]Mảng Tag ID
Content ReferenceStringPath tới asset/page
Fragment ReferenceNested typeTrở thành relationship, query được
JSON Objectjson scalarChỉ AEMaaCS, 6.5 không hỗ trợ

Tạo GraphQL Endpoint

Qua UI

  1. Tools → General → GraphQL
  2. Click Create:
    • Name: myproject
    • Configuration: chọn config chứa CF Models (vd: /conf/myproject)
  3. Endpoint available tại:
/content/cq:graphql/myproject/endpoint.json

# hoặc JCR-safe encoding:
/content/_cq_graphql/myproject/endpoint.json

Global vs Site-specific

EndpointPathKhi nào dùng
Global/content/cq:graphql/global/endpoint.jsonDev/test, truy cập mọi model
Site-specific/content/cq:graphql/myproject/endpoint.jsonProduction — giới hạn schema scope

Dùng site-specific trên production. Global endpoint expose toàn bộ model của mọi site trên cùng instance — security risk.

Tạo endpoint bằng curl (automation)

bash
curl -u admin:admin \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "jcr:primaryType": "cq:Page",
    "jcr:content": {
      "jcr:primaryType": "cq:PageContent",
      "jcr:title": "myproject",
      "sling:resourceType": "cq/graphql/components/endpoint",
      "configurationName": "/conf/myproject"
    }
  }' \
  "http://localhost:4502/content/cq:graphql/myproject"

GraphiQL IDE

AEM 6.5 ship sẵn GraphiQL IDE:

http://localhost:4502/content/graphiql.html

Tính năng: schema explorer, auto-complete, query history, variables panel.

Chọn endpoint ở dropdown phía trên trước khi query. Nếu không thấy endpoint → kiểm tra đã tạo và publish CF Model chưa.


Query Cơ Bản

List query — lấy nhiều fragments

graphql
{
    articleList {
        items {
            _path
            title
            body {
                html
            }
            publishDate
            featured
            category
        }
    }
}

AEM sinh ra 2 query types cho mỗi model Article:

  • articleList — lấy nhiều, hỗ trợ filter/sort/pagination
  • articleByPath — lấy 1 fragment theo path

Single fragment by path

graphql
{
    articleByPath(
        _path: "/content/dam/myproject/articles/getting-started"
    ) {
        item {
            title
            body {
                html
                plaintext
            }
            publishDate
            author {
                name
                bio { plaintext }
            }
        }
    }
}

Multi-line text sub-fields

Multi-line text field expose nội dung theo nhiều format:

graphql
{
    articleList {
        items {
            body {
                html         # HTML formatted
                plaintext    # Plain text, tags stripped
                markdown     # Markdown (nếu authored bằng Markdown)
            }
        }
    }
}

Chỉ chọn format cần dùng — html + plaintext + markdown cùng lúc = over-fetch.

Fragment references (nested queries)

Fragment reference tự động resolve thành nested type:

graphql
{
    articleList {
        items {
            title

            # Single fragment reference
            author {
                name
                bio { html }
            }

            # Multi-valued fragment reference
            relatedArticles {
                title
                _path
            }
        }
    }
}

Content reference / Image reference

graphql
{
    articleList {
        items {
            title
            featuredImage {
                ... on ImageRef {
                    _path
                    _authorUrl
                    _publishUrl
                    mimeType
                    width
                    height
                }
            }
        }
    }
}

_authorUrl / _publishUrl trả về URL đầy đủ — dùng trong SPA để render image không cần hardcode hostname.


Filtering, Sorting, Pagination

Filter cơ bản

graphql
{
    articleList(
        filter: {
            featured: { _expressions: [{ value: true }] }
            category: { _expressions: [{ value: "technology" }] }
        }
    ) {
        items {
            title
            category
        }
    }
}

Nhiều field trong cùng filter = AND logic.

Filter operators

OperatorÁp dụngÝ nghĩa
EQUALS (default)AllExact match
EQUALS_NOTAllNot equal
CONTAINSStringSubstring match (case-sensitive)
CONTAINS_NOTStringKhông chứa substring
STARTS_WITHStringPrefix match
LOWERNumber, DateLess than
LOWER_EQUALNumber, DateLess than or equal
GREATERNumber, DateGreater than
GREATER_EQUALNumber, DateGreater than or equal
ATDateExact date match
BEFOREDateBefore date
AFTERDateAfter date

Filter theo string

graphql
{
    articleList(
        filter: {
            title: {
                _expressions: [{
                    value: "AEM"
                    _operator: CONTAINS
                }]
            }
        }
    ) {
        items { title }
    }
}

Filter theo date range

graphql
{
    articleList(
        filter: {
            publishDate: {
                _expressions: [
                    { value: "2025-01-01T00:00:00Z", _operator: AFTER },
                    { value: "2025-12-31T23:59:59Z", _operator: BEFORE }
                ]
            }
        }
    ) {
        items { title publishDate }
    }
}

Sorting

graphql
{
    articleList(
        sort: "publishDate DESC"
    ) {
        items { title publishDate }
    }
}

# Multi-field sort
{
    articleList(
        sort: "category ASC, publishDate DESC"
    ) {
        items { title category publishDate }
    }
}

Pagination (offset-based)

AEM 6.5 chỉ hỗ trợ offset-based pagination. Cursor-based pagination (articlePaginated) chỉ có trên AEMaaCS.

graphql
# Trang 1
{
    articleList(offset: 0, limit: 10) {
        items { title }
    }
}

# Trang 2
{
    articleList(offset: 10, limit: 10) {
        items { title }
    }
}

Luôn set limit. Không set = AEM trả về default (thường 10, configurable). Không bao giờ query không có limit trên production.


Variations

Query variation khác ngoài master:

graphql
{
    articleList(variation: "summary") {
        items {
            title
            body { html }
        }
    }
}

Nếu field không có value trong variation → fallback về master.


Persisted Queries

Persisted queries là cách khuyến nghị cho production — query được lưu trên server, gọi qua GET request.

Tại sao cần dùng:

  • Cacheable bởi Dispatcher và CDN (GET request với URL ổn định)
  • Bảo mật — client không gửi arbitrary query
  • Nhanh hơn — server không parse query mỗi lần

Tạo persisted query

bash
curl -u admin:admin \
  -X PUT \
  -H "Content-Type: application/json" \
  -d '{
    "query": "{ articleList(sort: \"publishDate DESC\", limit: 10) { items { _path title body { html } publishDate category } } }"
  }' \
  "http://localhost:4502/graphql/persist.json/myproject/latest-articles"

Gọi persisted query

bash
# GET request — cacheable
curl "http://localhost:4502/graphql/execute.json/myproject/latest-articles"

Persisted query với variables

bash
# Tạo
curl -u admin:admin \
  -X PUT \
  -H "Content-Type: application/json" \
  -d '{
    "query": "query ArticlesByCategory($category: String!, $limit: Int = 10) { articleList(filter: { category: { _expressions: [{ value: $category }] } }, limit: $limit, sort: \"publishDate DESC\") { items { _path title publishDate category } } }"
  }' \
  "http://localhost:4502/graphql/persist.json/myproject/articles-by-category"

# Gọi với variables trong URL path
curl "http://localhost:4502/graphql/execute.json/myproject/articles-by-category;category=technology;limit=5"

URL patterns

PatternMethodMô tả
/graphql/persist.json/{config}/{name}PUTTạo/update persisted query
/graphql/execute.json/{config}/{name}GETChạy persisted query
/graphql/execute.json/{config}/{name};var1=val1;var2=val2GETChạy với variables
/graphql/list.json/{config}GETList tất cả persisted queries

Persisted queries phải được replicate sang Publish. Nếu tạo trên Author mà quên activate → 404 trên Publish.


Frontend Integration

Fetch helper (JavaScript / TypeScript)

typescript
const AEM_HOST = process.env.AEM_PUBLISH_HOST || 'https://publish.myproject.com';

async function fetchPersistedQuery<T>(
    queryPath: string,
    variables?: Record<string, string | number>
): Promise<T> {

    let url = `${AEM_HOST}/graphql/execute.json/${queryPath}`;

    if (variables) {
        const params = Object.entries(variables)
            .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
            .join(';');
        url += `;${params}`;
    }

    const response = await fetch(url, {
        method: 'GET',
        headers: { 'Content-Type': 'application/json' },
    });

    if (!response.ok) {
        throw new Error(`GraphQL error: ${response.status} ${response.statusText}`);
    }

    const json = await response.json();

    if (json.errors) {
        throw new Error(
            json.errors.map((e: { message: string }) => e.message).join(', ')
        );
    }

    return json.data;
}

Sử dụng

typescript
interface Article {
    _path: string;
    title: string;
    body: { html: string };
    publishDate: string;
}

interface ArticleListResponse {
    articleList: { items: Article[] };
}

// Fetch latest articles
const data = await fetchPersistedQuery<ArticleListResponse>(
    'myproject/latest-articles'
);
const articles = data.articleList.items;

// Fetch by category
const tech = await fetchPersistedQuery<ArticleListResponse>(
    'myproject/articles-by-category',
    { category: 'technology', limit: 5 }
);

React hook

typescript
import { useState, useEffect } from 'react';

function useArticles(category?: string) {
    const [articles, setArticles] = useState<Article[]>([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        setLoading(true);

        const path = category
            ? 'myproject/articles-by-category'
            : 'myproject/latest-articles';
        const vars = category ? { category } : undefined;

        fetchPersistedQuery<ArticleListResponse>(path, vars)
            .then(data => setArticles(data.articleList.items))
            .catch(setError)
            .finally(() => setLoading(false));
    }, [category]);

    return { articles, loading, error };
}

Dispatcher Configuration (AEM 6.5)

Filter rules

# Allow persisted queries (GET)
/0100 {
    /type "allow"
    /method "GET"
    /url "/graphql/execute.json/*"
}

# Allow GraphQL endpoint (GET + POST + OPTIONS)
/0101 {
    /type "allow"
    /method "(GET|POST|OPTIONS)"
    /url "/content/_cq_graphql/*/endpoint.json"
}

Client headers

# dispatcher/clientheaders.any
$include "./default_clientheaders.any"
"Origin"
"Access-Control-Request-Method"
"Access-Control-Request-Headers"
"Authorization"

Cache rules

# Cache persisted query responses
/0100 {
    /glob "*.json"
    /type "allow"
}

TTL cho GraphQL responses

apache
# vhost config
<LocationMatch "^/graphql/execute\.json/.*$">
    Header set Cache-Control "public, max-age=300"
    Header set Surrogate-Control "max-age=600"
</LocationMatch>

Cache invalidation

Khi Content Fragment được publish, AEM gửi flush request tới Dispatcher. Cấu hình invalidation cho .json:

# dispatcher/cache/invalidate.any
/0001 {
    /glob "*.json"
    /type "allow"
}

CORS Configuration

Bắt buộc khi SPA ở domain khác gọi GraphQL API:

json
// ui.config/.../com.adobe.granite.cors.impl.CORSPolicyImpl~graphql.cfg.json
{
    "supportscredentials": false,
    "supportedmethods": ["GET", "HEAD", "POST", "OPTIONS"],
    "alloworigin": [
        "https://www.myproject.com",
        "https://app.myproject.com"
    ],
    "maxage:Integer": 1800,
    "alloworiginregexp": [
        "http://localhost:.*"
    ],
    "allowedpaths": [
        "/content/_cq_graphql/myproject/endpoint.json",
        "/graphql/execute.json/.*"
    ],
    "supportedheaders": [
        "Origin",
        "Accept",
        "X-Requested-With",
        "Content-Type",
        "Access-Control-Request-Method",
        "Access-Control-Request-Headers",
        "Authorization"
    ]
}

Không dùng wildcard * cho alloworigin trên production. Liệt kê từng domain.

Referrer filter

json
// ui.config/.../org.apache.sling.security.impl.ReferrerFilter~graphql.cfg.json
{
    "allow.empty": false,
    "allow.hosts": [
        "www.myproject.com",
        "app.myproject.com"
    ],
    "allow.hosts.regexp": [
        "http://localhost:.*"
    ],
    "filter.methods": ["POST", "PUT", "DELETE"]
}

Authentication (AEM 6.5)

Publish — Anonymous access

Publish instance thường cho phép anonymous GET requests tới persisted queries. Không cần authentication nếu content đã được publish.

Author — Basic auth (dev only)

bash
curl -u admin:admin \
  "http://localhost:4502/graphql/execute.json/myproject/latest-articles"

Author — Token-based (production)

Với AEM 6.5 on-premise, dùng token authentication cho server-to-server:

java
// Java backend gọi Author GraphQL
URL url = new URL(
    "http://author.myproject.com/graphql/execute.json/myproject/latest-articles"
);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Basic "
    + Base64.getEncoder().encodeToString("service-user:password".getBytes()));

// Hoặc dùng login token từ AEM:
// conn.setRequestProperty("Cookie", "login-token=" + token);

Không embed Author credentials trong client-side JavaScript. Author queries chỉ chạy từ server-side (API routes, build-time fetch, serverless functions).


Caching Strategy

LayerCache mechanismInvalidation
BrowserCache-Control headerTTL-based
CDNSurrogate-Control headerPurge API / TTL
DispatcherFile-based cache (.stat)Flush agent khi content publish
AEM PublishIn-memory query cacheTự động khi content thay đổi

Optimized Filtering (AEM 6.5.17+)

Từ SP17, AEM hỗ trợ optimized filtering cho GraphQL. Cần chạy migration 1 lần:

  1. Vào OSGi Console → Configuration
  2. Tìm Content Fragment Migration Job Configuration
  3. Set:
    • ContentFragmentMigration:Enabled = 1
    • ContentFragmentMigration:Enforce = 1
  4. Save → migration chạy tự động

Sau migration, property cfGlobalVersion xuất hiện tại /content/dam (kiểm tra trong CRXDE). Nếu import CF bằng content package sau migration → cần chạy lại.


AEM 6.5 vs AEMaaCS — Khác Biệt

FeatureAEM 6.5 (SP10+)AEMaaCS
GraphQL APICần cài index packageBuilt-in
GraphiQL IDE/content/graphiql.html/aem/graphiql.html
Persisted queriesHỗ trợHỗ trợ
Cursor-based paginationKhông
JSON Object field typeKhông
Optimized filteringSP17+ (cần migration)Tự động
Oak indexCấu hình thủ côngTự động
CDN cachingTự cấu hình DispatcherFastly tích hợp sẵn

Performance Best Practices

Luôn dùng persisted queries

Ad-hoc POST queries bypass Dispatcher/CDN cache. Trên Publish, chỉ dùng persisted queries.

Giới hạn query depth

graphql
# Tệ — 4 levels nested, gây expensive JCR traversals
{
    articleList {
        items {
            author {
                articles {
                    relatedArticles {
                        author { name }
                    }
                }
            }
        }
    }
}

# Tốt — flat, chỉ lấy cần thiết
{
    articleList(limit: 10) {
        items {
            title
            body { html }
            author { name }
        }
    }
}

Chỉ lấy field cần dùng

graphql
# Tệ — over-fetch
{
    articleList {
        items {
            title
            body { html plaintext markdown }
            author { name bio { html plaintext } }
            relatedArticles { title body { html } }
        }
    }
}

# Tốt — chỉ lấy cho UI card
{
    articleList(limit: 20) {
        items {
            title
            publishDate
            category
        }
    }
}

Tạo Oak index cho filter fields

Nếu filter trên field thường xuyên (vd: category, publishDate), tạo Oak property index tương ứng. Xem note Query Builder phần Oak indexing.

Chạy optimized filtering migration

Trên AEM 6.5.17+, migration giúp filter nhanh hơn bằng cách flatten CF data vào index-friendly properties.


Pitfalls Thường Gặp

Vấn đềNguyên nhânFix
POST query chậm trên productionBypass Dispatcher cacheChuyển sang persisted queries (GET)
CORS error trong browserThiếu CORS OSGi configCấu hình CORSPolicyImpl, bao gồm OPTIONS method
GraphQL trả null cho field có dataCF chưa publish sang PublishKiểm tra CF status, activate
Nested reference trả emptyCF được reference chưa publishPublish cả parent và referenced fragments
Query chậmThiếu index, query depth quá sâuTạo Oak index, giảm depth, thêm limit
Schema không show field mớiCF Model chưa publishPublish updated CF Model → schema regenerate
Persisted query 404 trên PublishChưa replicate persisted queryActivate persisted query từ Author sang Publish
.json cache không invalidateDispatcher flush agent không cover .jsonThêm *.json vào invalidate.any
Cannot resolve fieldTên field sai hoặc model chưa deployKiểm tra schema trong GraphiQL
403 ForbiddenReferrer filter block requestThêm domain vào ReferrerFilter config
Index package chưa càiQuery fail hoặc cực chậmCài đúng version index package cho SP đang dùng
Filter không chính xác sau import CFcfGlobalVersion mất sau import packageChạy lại optimized filtering migration

Tham Khảo

AEM 6.5 On-Premise Developer Notes