Writes, reviews and debugs AEM Dispatcher and CDN configuration — flexible-mode file layout, deny-by-default filters, cache rules, statfileslevel and invalidation, TTLs, rewrites and redirects, headers and CORS, and Cloud Service CDN rules for traffic filtering and WAF.
When agents use itUse this whenever the user edits or reviews dispatcher .any, .farm, .vhost, .rules or .vars files (filters.any, rules.any, clientheaders.any, virtualhosts.any, rewrite.rules), runs the dispatcher SDK validator or docker_run.sh, asks why a page is not cached or not invalidated, sees a 404 from dispatcher, exposes .json or .model.json, configures redirects, vanity URLs, CORS or security headers, or edits cdn.yaml. Also apply it when a performance or security issue on AEM publish is really a cache or filter problem.
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-dispatcher -a github-copilot
# or with the org installer (adds .github/skills/aem-dispatcher):
npx -y github:AGCO-Global/org-skills add skill aem-dispatcher
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.
Installs the aem-architecture-skills plugin, which bundles all AEM / Architecture skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install aem-architecture-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill aem-dispatcher -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-dispatcher -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-dispatcher -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill aem-dispatcher -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 Dispatcher and CDN
Dispatcher is both the security perimeter and the cache for AEM publish. Every rule must answer two questions: can an attacker reach something they shouldn't through this? and will this response be cached and correctly invalidated? Deny by default, cache by default, and prove both with the validator and response headers.
Immutable files (overwritten or rejected by the pipeline): the default_* files (default_filters.any, default_rules.any, default_invalidate.any, default_clientheaders.any, default_virtualhosts.any, default_renders.any, default.farm, default.vhost), dispatcher.any, dispatcher_vhost.conf, and the Apache base config. Extend them via $include and your own files; treat the validator's list as authoritative.
Symlinks must be real relative symlinks (Windows checkouts break them).
Environment differences via Apache variables (${ENVIRONMENT_TYPE} and Defines), not per-environment file copies.
Validate before every commit:
./bin/validate.sh ./src # syntax, immutability, allowed directives
./bin/docker_run.sh ./src host.docker.internal:4503 8080 # run against local publish SDK
On 6.5 the same principles apply, but you own the whole dispatcher.any and the Apache build.
2. Filters: allow-list only
Filters are evaluated top to bottom; last match wins. Start from deny-all (the Cloud defaults already do) and add narrow allows.
Match on /path, /selectors, /extension, /suffix, /method, /query — not a raw /url glob
Glob URLs are bypassable with selectors/suffixes
Never allow /crx/*, /system/*, /bin/*, /libs/* broadly
Consoles, Query Builder, servlets; allow one exact servlet path if needed
.json only with an explicit selector (model) and path
Default JSON servlet leaks content structure; .infinity.json dumps trees
Deny numeric selectors (.1.json, .-1.json)
Recursive JSON rendering
POST only to exact form-handler paths
Everything else on publish is read-only
Test the negative cases
A filter review isn't done until blocked URLs return 404
3. Cache rules
/rules: cache * under content paths; deny only what must never be cached (authenticated or per-user endpoints).
/ignoreUrlParams — ignore everything except parameters that change the response; otherwise every tracking parameter (utm_*, gclid) is a cache miss:
/ignoreUrlParams {
/0001 { /glob "*" /type "allow" } # allow = ignore for caching
/0002 { /glob "page" /type "deny" } # deny = response depends on it → not cached
}
/statfileslevel: set to the depth of your site roots (e.g. /content/brand/country/lang → 4 or 5) so publishing a page in one site doesn't invalidate every site. Too low → mass invalidation; too high → stale navigation/header fragments shared across the tree.
/invalidate: auto-invalidate *.html (and page .json) on activation; don't auto-invalidate clientlibs or DAM renditions — they use versioned/fingerprinted URLs.
/allowAuthorized "0" (default): requests with auth headers/cookies aren't cached. Turn it on only with permission-sensitive caching in place.
/enableTTL "1" makes dispatcher honour Cache-Control: max-age/Expires from publish — use it for API responses and content that should expire rather than be invalidated.
Headers: Cache-Control drives browsers and CDN; Surrogate-Control targets the CDN only. Set them in the vhost per path pattern (short TTL for HTML, long+immutable for fingerprinted clientlibs). Cache and serve only the headers you list in /headers.
4. Invalidation flow
AEM 6.5
Cloud Service
Dispatcher
Flush replication agent(s) on publish call /dispatcher/invalidate.cache
Invalidated automatically on publish/unpublish via content distribution
CDN
Your purge integration or short TTLs
Adobe CDN honours TTL headers; keep HTML TTLs short rather than rely on purges
Common bug
Flush agent on author only, or pointing at the wrong dispatcher
Stale header/footer XF because statfileslevel isolates it from the pages that include it
Content included into many pages (XFs, navigation, CF-driven lists) needs a deliberate strategy: shared statfile level, a TTL, or client-side/SDI inclusion.
5. Rewrites, redirects, vanity URLs
Short URLs: map /content/mysite/en/... ↔ /en/... with mod_rewrite [PT] rules plus Sling mappings/resource resolver config so links are rewritten on output.
Redirects: small, stable sets in rewrite.rules (RewriteRule ^/old$ /new [R=301,L]); large marketer-owned lists in a RewriteMap. Cloud Service also supports redirect maps maintained outside the code pipeline and CDN-level redirects — confirm the current mechanism in the product docs before designing around it.
Vanity URLs (sling:vanityPath): enable the dispatcher /vanity_urls feature and allow its endpoint in filters, or rewrite them at Apache; don't let every unknown URL fall through to publish.
Keep redirect chains to one hop; always L flag; test with curl -I.
6. Headers, CORS, security
Security headers in the vhost: Strict-Transport-Security, X-Content-Type-Options: nosniff, X-Frame-Options/CSP frame-ancestors, Referrer-Policy, a CSP owned by whoever owns the tag manager.
CORS for headless: configure the AEM CORS policy OSGi config (com.adobe.granite.cors.impl.CORSPolicyImpl~<name>) and pass Origin (plus Access-Control-Request-*) in clientheaders.any; cache per origin or restrict to one origin list so a cached response doesn't carry the wrong Access-Control-Allow-Origin.
Pass only needed client headers; never forward cookies to cacheable paths unless required.
7. CDN (Cloud Service) and BYO CDN
The Adobe-managed CDN (Fastly-based) sits in front of dispatcher. CDN rules live in config/cdn.yaml deployed through a Cloud Manager config pipeline: traffic filter rules (rate limits, geo/IP blocks), WAF rules (licence-dependent), request/response transformations, origin selectors and redirects. Check current syntax and entitlements in the docs rather than copying old examples.
Start WAF and rate-limit rules in log/alert mode, tune, then block.
BYO CDN: point it at the Adobe CDN (not around it), send the required host/secret headers per the docs, avoid double caching with conflicting TTLs, and ensure purges or short TTLs on both layers.
8. Debugging
Local: send X-Dispatcher-Info: true to the SDK container; the response explains cache/filter decisions. Check dispatcher.log with trace level (DISP_LOG_LEVEL=trace1).
Cloud: CDN response headers (x-cache: HIT/MISS, Age), dispatcher and CDN logs from Cloud Manager; calculate hit ratio per path pattern.
404 from dispatcher with a healthy publish = filter deny; 200 but always MISS = auth header/cookie, query string, Cache-Control: private, or /rules deny.
Deliverable format
For a config review, return a table per concern, then the corrected snippets:
URL / pattern
Expected
Actual (filter · cache · TTL)
Risk
Fix
/content/mysite/en.model.json
200, cached 5 min
allowed, never cached (query param)
Low hit ratio
Ignore utm_* in /ignoreUrlParams
/content/mysite/en.infinity.json
404
200
Content structure leak
Deny infinity selector
Close with: validator output, the negative test URL list, and expected cache hit ratio impact.
Anti-patterns to reject
Allow-all filters with a few denies; /url globs instead of structured matchers.
Editing immutable default_* files or dispatcher.any; skipping validate.sh.
/statfileslevel "0" on multi-site estates, or so high that shared fragments go stale.
Caching every query-string variant or never caching any; tracking params not ignored.
.json, .infinity.json, Query Builder, /crx or /system/console reachable on publish.
/allowAuthorized "1" without permission-sensitive caching.
Long CDN TTLs on HTML with no purge strategy; redirect chains and 302s for permanent moves.
Access-Control-Allow-Origin: * on authenticated or cached per-origin responses.
---
name: aem-dispatcher
description: Writes, reviews and debugs AEM Dispatcher and CDN configuration — flexible-mode file layout, deny-by-default filters, cache rules, statfileslevel and invalidation, TTLs, rewrites and redirects, headers and CORS, and Cloud Service CDN rules for traffic filtering and WAF. Use this whenever the user edits or reviews dispatcher .any, .farm, .vhost, .rules or .vars files (filters.any, rules.any, clientheaders.any, virtualhosts.any, rewrite.rules), runs the dispatcher SDK validator or docker_run.sh, asks why a page is not cached or not invalidated, sees a 404 from dispatcher, exposes .json or .model.json, configures redirects, vanity URLs, CORS or security headers, or edits cdn.yaml. Also apply it when a performance or security issue on AEM publish is really a cache or filter problem.
metadata:
technology: AEM
type: architecture
---
# AEM Dispatcher and CDN
Dispatcher is both the security perimeter and the cache for AEM publish. Every rule must answer two questions: *can an attacker reach something they shouldn't through this?* and *will this response be cached and correctly invalidated?* Deny by default, cache by default, and prove both with the validator and response headers.
## 1. File layout (Cloud Service, flexible mode)
```
dispatcher/src/
├── opt-in/USE_SOURCES_DIRECTLY ← enables flexible mode
├── conf.d/
│ ├── available_vhosts/<site>.vhost ← your vhosts
│ ├── enabled_vhosts/<site>.vhost ← symlink → ../available_vhosts/<site>.vhost
│ ├── rewrites/rewrite.rules ← mod_rewrite rules (included from vhost)
│ └── variables/custom.vars ← Define ${CONTENT_FOLDER_NAME} etc.
└── conf.dispatcher.d/
├── available_farms/<site>.farm enabled_farms/<site>.farm → symlink
├── filters/filters.any ← $include "./default_filters.any" + yours
├── cache/rules.any ← cacheable paths
├── clientheaders/clientheaders.any ← headers passed to publish
├── virtualhosts/virtualhosts.any ← hostnames for the farm
└── renders/ ← publish render (Adobe-managed default)
```
- **Immutable files** (overwritten or rejected by the pipeline): the `default_*` files (`default_filters.any`, `default_rules.any`, `default_invalidate.any`, `default_clientheaders.any`, `default_virtualhosts.any`, `default_renders.any`, `default.farm`, `default.vhost`), `dispatcher.any`, `dispatcher_vhost.conf`, and the Apache base config. Extend them via `$include` and your own files; treat the validator's list as authoritative.
- Symlinks must be real relative symlinks (Windows checkouts break them).
- Environment differences via Apache variables (`${ENVIRONMENT_TYPE}` and `Define`s), not per-environment file copies.
**Validate before every commit:**
```bash
./bin/validate.sh ./src # syntax, immutability, allowed directives
./bin/docker_run.sh ./src host.docker.internal:4503 8080 # run against local publish SDK
```
On 6.5 the same principles apply, but you own the whole `dispatcher.any` and the Apache build.
## 2. Filters: allow-list only
Filters are evaluated top to bottom; **last match wins**. Start from deny-all (the Cloud defaults already do) and add narrow allows.
```
/0100 { /type "allow" /method "GET" /path "/content/mysite/*" /extension "html" }
/0101 { /type "allow" /method "GET" /path "/content/mysite/*" /selectors "model" /extension "json" }
/0102 { /type "allow" /method "GET" /url "/graphql/execute.json/mysite/*" } # persisted queries, exact prefix
/0110 { /type "deny" /selectors "(feed|rss|pages|languages|blueprint|infinity|tidy|sysview|docview|query|[0-9-]+)" /extension "json" }
/0111 { /type "deny" /path "/content/*" /query "debug=*" }
```
| Rule | Why |
|---|---|
| Match on `/path`, `/selectors`, `/extension`, `/suffix`, `/method`, `/query` — not a raw `/url` glob | Glob URLs are bypassable with selectors/suffixes |
| Never allow `/crx/*`, `/system/*`, `/bin/*`, `/libs/*` broadly | Consoles, Query Builder, servlets; allow one exact servlet path if needed |
| `.json` only with an explicit selector (`model`) and path | Default JSON servlet leaks content structure; `.infinity.json` dumps trees |
| Deny numeric selectors (`.1.json`, `.-1.json`) | Recursive JSON rendering |
| POST only to exact form-handler paths | Everything else on publish is read-only |
| Test the negative cases | A filter review isn't done until blocked URLs return 404 |
## 3. Cache rules
- `/rules`: cache `*` under content paths; deny only what must never be cached (authenticated or per-user endpoints).
- **`/ignoreUrlParams`** — ignore everything except parameters that change the response; otherwise every tracking parameter (`utm_*`, `gclid`) is a cache miss:
```
/ignoreUrlParams {
/0001 { /glob "*" /type "allow" } # allow = ignore for caching
/0002 { /glob "page" /type "deny" } # deny = response depends on it → not cached
}
```
- **`/statfileslevel`**: set to the depth of your site roots (e.g. `/content/brand/country/lang` → 4 or 5) so publishing a page in one site doesn't invalidate every site. Too low → mass invalidation; too high → stale navigation/header fragments shared across the tree.
- **`/invalidate`**: auto-invalidate `*.html` (and page `.json`) on activation; don't auto-invalidate clientlibs or DAM renditions — they use versioned/fingerprinted URLs.
- **`/allowAuthorized "0"`** (default): requests with auth headers/cookies aren't cached. Turn it on only with permission-sensitive caching in place.
- **`/enableTTL "1"`** makes dispatcher honour `Cache-Control: max-age`/`Expires` from publish — use it for API responses and content that should expire rather than be invalidated.
- **Headers**: `Cache-Control` drives browsers and CDN; `Surrogate-Control` targets the CDN only. Set them in the vhost per path pattern (short TTL for HTML, long+immutable for fingerprinted clientlibs). Cache and serve only the headers you list in `/headers`.
## 4. Invalidation flow
| | AEM 6.5 | Cloud Service |
|---|---|---|
| Dispatcher | Flush replication agent(s) on publish call `/dispatcher/invalidate.cache` | Invalidated automatically on publish/unpublish via content distribution |
| CDN | Your purge integration or short TTLs | Adobe CDN honours TTL headers; keep HTML TTLs short rather than rely on purges |
| Common bug | Flush agent on author only, or pointing at the wrong dispatcher | Stale header/footer XF because statfileslevel isolates it from the pages that include it |
Content included into many pages (XFs, navigation, CF-driven lists) needs a deliberate strategy: shared statfile level, a TTL, or client-side/SDI inclusion.
## 5. Rewrites, redirects, vanity URLs
- **Short URLs**: map `/content/mysite/en/...` ↔ `/en/...` with mod_rewrite `[PT]` rules plus Sling mappings/resource resolver config so links are rewritten on output.
- **Redirects**: small, stable sets in `rewrite.rules` (`RewriteRule ^/old$ /new [R=301,L]`); large marketer-owned lists in a `RewriteMap`. Cloud Service also supports redirect maps maintained outside the code pipeline and CDN-level redirects — confirm the current mechanism in the product docs before designing around it.
- **Vanity URLs** (`sling:vanityPath`): enable the dispatcher `/vanity_urls` feature and allow its endpoint in filters, or rewrite them at Apache; don't let every unknown URL fall through to publish.
- Keep redirect chains to one hop; always `L` flag; test with `curl -I`.
## 6. Headers, CORS, security
- Security headers in the vhost: `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options`/CSP `frame-ancestors`, `Referrer-Policy`, a CSP owned by whoever owns the tag manager.
- **CORS** for headless: configure the AEM CORS policy OSGi config (`com.adobe.granite.cors.impl.CORSPolicyImpl~<name>`) and pass `Origin` (plus `Access-Control-Request-*`) in `clientheaders.any`; cache per origin or restrict to one origin list so a cached response doesn't carry the wrong `Access-Control-Allow-Origin`.
- Pass only needed client headers; never forward cookies to cacheable paths unless required.
## 7. CDN (Cloud Service) and BYO CDN
- The Adobe-managed CDN (Fastly-based) sits in front of dispatcher. CDN rules live in `config/cdn.yaml` deployed through a Cloud Manager **config pipeline**: traffic filter rules (rate limits, geo/IP blocks), WAF rules (licence-dependent), request/response transformations, origin selectors and redirects. Check current syntax and entitlements in the docs rather than copying old examples.
- Start WAF and rate-limit rules in log/alert mode, tune, then block.
- **BYO CDN**: point it at the Adobe CDN (not around it), send the required host/secret headers per the docs, avoid double caching with conflicting TTLs, and ensure purges or short TTLs on both layers.
## 8. Debugging
- Local: send `X-Dispatcher-Info: true` to the SDK container; the response explains cache/filter decisions. Check `dispatcher.log` with trace level (`DISP_LOG_LEVEL=trace1`).
- Cloud: CDN response headers (`x-cache: HIT/MISS`, `Age`), dispatcher and CDN logs from Cloud Manager; calculate hit ratio per path pattern.
- `404` from dispatcher with a healthy publish = filter deny; `200` but always MISS = auth header/cookie, query string, `Cache-Control: private`, or `/rules` deny.
## Deliverable format
For a config review, return a table per concern, then the corrected snippets:
| URL / pattern | Expected | Actual (filter · cache · TTL) | Risk | Fix |
|---|---|---|---|---|
| `/content/mysite/en.model.json` | 200, cached 5 min | allowed, never cached (query param) | Low hit ratio | Ignore `utm_*` in `/ignoreUrlParams` |
| `/content/mysite/en.infinity.json` | 404 | 200 | Content structure leak | Deny `infinity` selector |
Close with: validator output, the negative test URL list, and expected cache hit ratio impact.
## Anti-patterns to reject
- Allow-all filters with a few denies; `/url` globs instead of structured matchers.
- Editing immutable `default_*` files or `dispatcher.any`; skipping `validate.sh`.
- `/statfileslevel "0"` on multi-site estates, or so high that shared fragments go stale.
- Caching every query-string variant or never caching any; tracking params not ignored.
- `.json`, `.infinity.json`, Query Builder, `/crx` or `/system/console` reachable on publish.
- `/allowAuthorized "1"` without permission-sensitive caching.
- Long CDN TTLs on HTML with no purge strategy; redirect chains and 302s for permanent moves.
- `Access-Control-Allow-Origin: *` on authenticated or cached per-origin responses.