An Adobe App Builder extension for AEM as a Cloud Service that reports which components are
actually used across published pages, and which component definitions in /apps are unused
(dead).
- Frontend — React 18 + React Spectrum (
@react-spectrum/*) - Backend — Adobe I/O Runtime serverless action (
nodejs18) - Data — AEM Query Builder API (
/bin/querybuilder.json) with facet aggregation - Auth — Adobe IMS OAuth Server-to-Server (
client_credentials) → AEM bearer token - Hosting — Experience Cloud Shell extension (
dx/excshell/1) surfaced via the AEM Extension Manager
Scope note. The report counts page-level
sling:resourceTypevalues, i.e. the components rendering each published page. It does not enumerate every embedded component instance inside page trees. Counting instance-level usage requires walking eachjcr:contentsubtree and is significantly more expensive on large sites.
.
├── app.config.yaml # Extension + Runtime action registration
├── package.json
├── actions/
│ └── queryComponents.js # I/O Runtime action (IMS auth, QueryBuilder, aggregation)
├── src/
│ ├── index.html # SPA entry (referenced by app.config.yaml `impl`)
│ ├── index.js # React root
│ ├── App.js # Fetches from the action, renders the report
│ ├── webpack.config.js # Injects AIO_RUNTIME_NAMESPACE at build time
│ └── components/
│ ├── UsageTable.js # React Spectrum Table + dead-components notice
│ ├── DetailView.js # Row drill-down, pages grouped by site/section
│ └── ExportButton.js # CSV / JSON export
├── .aio # Generated by `aio app use` — do not edit
└── .env # Local secrets — do not commit
| Requirement | Notes |
|---|---|
| Node.js 18+ | Matches engines.node and the nodejs18 action runtime |
| Adobe I/O CLI v8+ | v8 is required for app.config.yaml support; legacy .aio/manifest.yaml projects cannot use this layout |
| Adobe Developer Console project | With the App Builder entitlement |
| AEM as a Cloud Service environment | Author tier reachable over HTTPS from I/O Runtime |
| Adobe IMS Org Admin role | To create OAuth Server-to-Server credentials and a technical account |
Install the CLI:
npm install -g @adobe/aio-cli
aio login
aio --version # confirm 8.x or newerAll variables live in .env locally and are injected into the action as inputs via
app.config.yaml. Never commit .env or .aio.
| Variable | Required | Default | Purpose |
|---|---|---|---|
AEM_AUTHOR_BASE_URL |
recommended | — | AEM author URL, e.g. https://author-p1234-e5678.adobeaemcloud.com. Queried by default because publication status lives on author. |
AEM_BASE_URL |
fallback | — | Used when AEM_AUTHOR_BASE_URL is unset. |
AEM_CONTENT_PATH |
no | /content |
Root scope for the usage query. A caller may narrow this, but cannot widen it. |
AEM_APPS_PATH |
no | /apps |
Root scanned for component definitions when detecting dead components. |
IMS_CLIENT_ID |
yes | — | OAuth Server-to-Server client ID from Developer Console. Also sent as X-Api-Key. |
IMS_CLIENT_SECRET |
yes | — | OAuth Server-to-Server client secret. |
IMS_ORG_ID |
yes | — | IMS Organization ID. |
IMS_SCOPES |
yes | — | Comma-separated scopes. See below. |
IMS_TECHNICAL_ACCOUNT_ID |
yes | — | Technical account ID associated with the credential. |
AIO_RUNTIME_NAMESPACE |
auto | — | Generated by the CLI. Injected into the web bundle at build time by webpack DefinePlugin; must not be hand-edited. |
openid,AdobeID,read_organizations,additional_info.projectedProductContext
Confirm the exact scope list shown on the OAuth Server-to-Server credential page in your Developer Console project — Adobe documents a fixed set rather than granular per-endpoint scopes for AEM access.
AEM_AUTHOR_BASE_URL=https://author-p1234-e5678.adobeaemcloud.com
AEM_CONTENT_PATH=/content
AEM_APPS_PATH=/apps
IMS_CLIENT_ID=
IMS_CLIENT_SECRET=
IMS_ORG_ID=
IMS_TECHNICAL_ACCOUNT_ID=
IMS_SCOPES=openid,AdobeID,read_organizations,additional_info.projectedProductContext
# auto-generated — do not edit
# AIO_runtime_namespace=
# AIO_runtime_auth=
# AIO_runtime_apihost=The Runtime action authenticates as an IMS technical account, not as the signed-in author. That account needs repository read access:
| Path | Required privilege | Used for |
|---|---|---|
/content/** |
jcr:read |
Usage counts, sample pages, last-modified |
/apps/** |
jcr:read |
Dead-component detection |
/apps access is commonly not granted by default. If it is missing, the report still
renders usage counts but the dead-components section reports
status: "forbidden" in the response and shows an actionable message in the UI rather than
silently appearing empty.
npm install
aio login
aio app use -w Stage
aio app runaio app run starts the Runtime action locally and proxies the SPA. Open the URL printed by
the CLI. The action is reached through the dev proxy at
/api/v1/web/default/queryComponents, which is why App.js falls back to a relative URL
when AIO_RUNTIME_NAMESPACE is not injected.
# Requires a valid IMS access token: the action sets require-adobe-auth: true.
curl.exe -s -X POST "http://localhost:8443/api/v1/web/default/queryComponents" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $IMS_ACCESS_TOKEN" \
-d '{"includeDead":true}'Get the token via OAuth Playground using your
Developer Console OAuth Server-to-Server credentials, then export it as IMS_ACCESS_TOKEN.
Without the header the action returns 401 before reaching AEM.
Inspect deadComponentsStatus to confirm the /apps scan actually ran:
status |
Meaning | Action |
|---|---|---|
ok |
definitions read, scan complete | — |
forbidden |
403 on /apps |
grant jcr:read on /apps/** |
unavailable |
other HTTP or transport failure | check reason |
unparsed |
nodes returned but no usable path key | log a raw hit to inspect the key |
partial |
some definitions unparseable | usually mixed hit writers |
ok + truncated: true |
hit the guessTotal cap |
raise the cap or narrow AEM_APPS_PATH |
Also check countsFromFacets. If it is false, usage counts were derived from the paged
page query rather than facet aggregation and totals are lower bounds.
aio app use -w Stage
aio app deployStage deployments are isolated and do not appear in the AEM Extension Manager. Use Stage to validate the action, IAM configuration, and QueryBuilder behaviour.
aio app use -w Production
aio app deployDeploying to the Production workspace makes the extension visible to Adobe App Registry. For third-party extensions, submit it for approval:
- Adobe Developer Console → your project → the extension → Production workspace.
- Submit for approval.
- Adobe Exchange → Manage → Apps pending review → accept.
- Approval injects the extension into all AEM as a Cloud Service author services in the organization.
Extensions deployed only to Stage are never injected into AEM. Production deployment plus approval is a prerequisite for the Extension Manager to list anything.
- In AEM Author, open Tools → Extension Manager.
(
https://<author-host>/ui#/aem/libs/granite/extensionmanager/content.html— exact path varies by release; the Tools menu entry is authoritative.) - Select your Program and Environment.
- Find AEM Component Usage Analytics in the listing. It appears once the Production deployment has been approved.
- Toggle it on for that environment. Enable/disable is per-instance, so Stage and Production AEM environments are controlled independently.
- Open the extension to render the report.
Roles required
| Role | Can |
|---|---|
| System Administrator | Enable/disable extensions on production environments; configure parameters |
| Developer | Add BYO extensions, preview, generate shareable preview links |
Preview before enabling. Use the preview icon in the extension listing to open the app in a preproduction environment without installing it for the whole environment.
Restricting to specific environments. Any approved extension is offered across all AEM
programs and environments in the organization. To self-restrict, inspect the repo query
parameter at registration time and skip registration when it does not match your target host.
App.jsPOSTs{ path?, includeDead }to the Runtime web action, forwarding the caller's IMS token from thewindow.dxcontext (the action setsrequire-adobe-auth: true).- The action exchanges IMS client credentials for an AEM bearer token via
@adobe/aio-lib-imsand callsGET {AEM_AUTHOR_BASE_URL}/bin/querybuilder.json. - Three queries run in parallel:
- Facet counts —
p.facets=trueover published pages, bucketed byjcr:content/sling:resourceType. This is the GROUP BY; QueryBuilder has no SQL-style aggregation. - Page details — selective hits carrying resource type,
cq:lastModified,cq:lastModifiedBy,jcr:title, used for last-modified, author, and sample pages. - Component definitions —
type=cq:Componentunder/apps. Definitions carry nosling:resourceType; the type is derived from the definition node's own path. Used components are subtracted to yield dead components.
- Facet counts —
- A caller-supplied
pathis validated to be/contentor a descendant, so the technical account cannot be steered outside the content root. - The action returns
{ scope, generatedAt, countsFromFacets, components[], deadComponents[], deadComponentsStatus }.
- Author tier, not publish. Publication status is only available on author. Querying the
publish tier would require the Dispatcher to allow
/bin/querybuilder.json, which the default publish filters block. - Page-level usage. Embedded component instances are not counted individually (see scope note above).
- Facet cap. Facet bucket counts are limited per query. If a site exceeds the cap, counts
are incomplete and
countsFromFacetsshould be verified. - Result caps. The page query and definition query use
p.guessTotaland will truncate on very large repositories;pageQueryTruncatedanddeadComponentsStatus.truncatedsurface this. - Token storage.
@adobe/aio-lib-imspersists tokens to App Builder State, which has usage limits that a busy report page can exhaust. Adobe's current guidance is to use@adobe/aio-lib-core-authfor Runtime token generation instead; this is a contained change to thegetAemTokenfunction only. - Index coverage. Facet performance depends on
sling:resourceTypebeing indexed under the content path. Verify with the AEM Query Performance tool (/ui#/aem/libs/granite/operations/content/diagnosistools/queryPerformance.html) before trusting counts on a large repository.