Skip to content

Commit 84a64eb

Browse files
rossbrandonsirugh
andauthored
USF-3785: ACO PDP support (#265)
Add PDP pre-render support for Commerce Optimizer --------- Co-authored-by: Stephen <sirugh@users.noreply.github.com>
1 parent 484decf commit 84a64eb

10 files changed

Lines changed: 534 additions & 128 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,7 @@ logs
4343
# aem-commerce-prerender related
4444
*.env
4545
*.aio.json
46-
.aem-commerce-prerender.json
46+
.aem-commerce-prerender.json
47+
48+
# AI
49+
.cursor/

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
6868
* `PRODUCTS_TEMPLATE`: The URL for the product template page (auto-populated by wizard). For localized sites with URLs like `https://main--site--org.aem.page/en-us/products/default`, you can use the `{locale}` token: `https://main--site--org.aem.page/{locale}/products/default`
6969
* `PRODUCT_PAGE_URL_FORMAT`: The URL pattern for product pages (auto-populated by wizard). Supports tokens: `{locale}`, `{urlKey}`, `{sku}`. Default pattern: `/{locale}/products/{urlKey}`. For live environments, consider using a different prefix like `/{locale}/products-prerendered/{urlKey}` for logical separation
7070
* `LOCALES`: Comma-separated list of locales (e.g., `en-us,en-gb,fr-fr`) or empty for non-localized sites
71+
* `ACO_CATEGORY_FAMILIES`: *(Commerce Optimizer only)* Comma-separated list of category family identifiers (e.g., `electronics,apparel`). Determines which categories are included for PLP pre-rendering. For PDP pre-rendering, catalogs with 10,000 or fewer products are fetched in full regardless of this setting. For larger catalogs, the system fetches the first 10,000 products plus up to 10,000 per category discovered from these families, deduplicated by SKU. If not configured, only the first 10,000 products are pre-rendered.
7172
* `AEM_ADMIN_API_AUTH_TOKEN`: Long-lived authentication token for AEM Admin API (valid for 1 year). During setup, the wizard will exchange your temporary 24-hour token from [admin.hlx.page](https://admin.hlx.page/) for this long-lived token automatically.
7273

7374
You can modify the environment-specific variables by editing the `.env` file directly or by re-running the setup wizard with `npm run setup`.
74-
1. **After Setup Completion**: Once the setup process is complete, the following configurations will be automatically applied:
75+
2. **After Setup Completion**: Once the setup process is complete, the following configurations will be automatically applied:
7576

7677
* **Site Context**: A Site Context will be created and stored in your localStorage. This serves as the authentication medium required to operate the [Storefront Prerender Management UI](https://prerender.aem-storefront.com) (you will be redirected to this address).
7778

@@ -87,9 +88,9 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
8788
}
8889
}
8990
```
90-
1. [Customize the code](/docs/CUSTOMIZE.md) that contains the rendering logic according to your requirements, for [structured data](/actions/pdp-renderer/ldJson.js), [markup](/actions/pdp-renderer/render.js) and [templates](https://github.com/adobe-rnd/aem-commerce-prerender/tree/main/actions/pdp-renderer/templates)
91-
1. Deploy the solution with `npm run deploy`
92-
1. **Testing Actions Manually**: Before enabling automated triggers, verify that each action works correctly by invoking them manually:
91+
3. [Customize the code](/docs/CUSTOMIZE.md) that contains the rendering logic according to your requirements, for [structured data](/actions/pdp-renderer/ldJson.js), [markup](/actions/pdp-renderer/render.js) and [templates](https://github.com/adobe-rnd/aem-commerce-prerender/tree/main/actions/pdp-renderer/templates)
92+
4. Deploy the solution with `npm run deploy`
93+
5. **Testing Actions Manually**: Before enabling automated triggers, verify that each action works correctly by invoking them manually:
9394
```bash
9495
# Fetch all products from Catalog Service and store them in default-products.json
9596
aio rt action invoke aem-commerce-ssg/fetch-all-products
@@ -100,7 +101,7 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
100101
# Clean up and unpublish deleted products
101102
aio rt action invoke aem-commerce-ssg/mark-up-clean-up
102103
```
103-
1. **Enable Automated Triggers**: Once you've confirmed that all actions work correctly, uncomment the triggers and rules sections in `app.config.yaml`:
104+
6. **Enable Automated Triggers**: Once you've confirmed that all actions work correctly, uncomment the triggers and rules sections in `app.config.yaml`:
104105
```yaml
105106
triggers:
106107
productPollerTrigger:
@@ -127,7 +128,7 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
127128
action: "mark-up-clean-up"
128129
```
129130
Then redeploy the solution: `npm run deploy`
130-
1. **Management UI Overview**: Navigate to the [Storefront Prerender Management UI](https://prerender.aem-storefront.com) to monitor and manage your prerender deployment. The UI provides several tabs:
131+
7. **Management UI Overview**: Navigate to the [Storefront Prerender Management UI](https://prerender.aem-storefront.com) to monitor and manage your prerender deployment. The UI provides several tabs:
131132

132133
* **Published Products** (`#/products`): Displays the list of products published on your store, as retrieved from your site's `published-products-index.json`. For sites with over a thousand products, use the pagination interface to navigate through results. The search functionality allows you to filter products on the current page.
133134

@@ -148,7 +149,7 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
148149

149150
* **Settings** (`#/settings`): Allows you to access and modify your personal context file. The context file contains information about the prerender app's namespace, authentication token, and the currently active Helix token. Editing the context file enables you to use the prerender UI to manage other App Builder applications.
150151

151-
1. The system is now up and running. In the first cycle of operation, it will publish all products in the catalog. Subsequent runs will only process products that have changed.
152+
8. The system is now up and running. In the first cycle of operation, it will publish all products in the catalog. Subsequent runs will only process products that have changed.
152153

153154
### Management UI Setup
154155

actions/categories.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
3+
Copyright 2026 Adobe. All rights reserved.
4+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License. You may obtain a copy
6+
of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
8+
Unless required by applicable law or agreed to in writing, software distributed under
9+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
10+
OF ANY KIND, either express or implied. See the License for the specific language
11+
governing permissions and limitations under the License.
12+
13+
*/
14+
15+
const { requestSaaS } = require("./utils");
16+
const {
17+
CategoriesQuery,
18+
CategoryTreeQuery,
19+
CategoryTreeBySlugsQuery,
20+
} = require("./queries");
21+
22+
const MAX_TREE_DEPTH = 3;
23+
24+
/**
25+
* Checks whether category families are configured (not the [null] default).
26+
*
27+
* @param {Array} families - The categoryFamilies array from runtime config.
28+
* @returns {boolean}
29+
*/
30+
function hasFamilies(families) {
31+
return Array.isArray(families) && families.length > 0;
32+
}
33+
34+
/**
35+
* Resolves all category slugs belonging to the given ACO category families.
36+
*
37+
* Uses BFS traversal of the categoryTree API:
38+
* 1. Query each family's root categories and their immediate childrenSlugs.
39+
* 2. Query those children (with depth) to retrieve their descendants.
40+
* 3. Repeat until no unresolved childrenSlugs remain.
41+
*
42+
* Handles trees of arbitrary depth even when the API caps depth at
43+
* MAX_TREE_DEPTH per call — each iteration advances up to that many levels.
44+
*
45+
* @param {Object} context - Request context (config, logger, headers, etc.).
46+
* @param {string[]} families - ACO category family identifiers.
47+
* @returns {Promise<string[]>} Flat array of all unique category slugs.
48+
*/
49+
async function getCategorySlugsFromFamilies(context, families) {
50+
console.debug("Getting category slugs from families:", families);
51+
const allSlugs = new Set();
52+
53+
for (const family of families) {
54+
console.debug("Getting category slugs from family:", family);
55+
// Get root-level categories for this family
56+
const firstLevel = await requestSaaS(
57+
CategoryTreeQuery,
58+
"getCategoryTree",
59+
{ family },
60+
context,
61+
);
62+
63+
let pending = [];
64+
for (const cat of firstLevel.data.categoryTree) {
65+
allSlugs.add(cat.slug);
66+
pending.push(...(cat.childrenSlugs || []));
67+
}
68+
69+
// BFS: resolve children level by level until no new slugs remain
70+
while (pending.length) {
71+
// Mark pending as seen before querying to prevent re-processing
72+
for (const slug of pending) allSlugs.add(slug);
73+
74+
const childrenRes = await requestSaaS(
75+
CategoryTreeBySlugsQuery,
76+
"getCategoryTreeBySlugs",
77+
{ family, slugs: pending, depth: MAX_TREE_DEPTH },
78+
context,
79+
);
80+
81+
// First pass: capture any descendant slugs included due to depth traversal
82+
for (const cat of childrenRes.data.categoryTree) {
83+
allSlugs.add(cat.slug);
84+
}
85+
86+
// Second pass: collect only new childrenSlugs for next iteration
87+
pending = [];
88+
for (const cat of childrenRes.data.categoryTree) {
89+
for (const child of cat.childrenSlugs || []) {
90+
if (!allSlugs.has(child)) pending.push(child);
91+
}
92+
}
93+
}
94+
}
95+
console.debug("Category slugs resolved:", [...allSlugs]);
96+
97+
return [...allSlugs];
98+
}
99+
100+
/**
101+
* Retrieves all ACCS categories grouped by level.
102+
*
103+
* Returns a sparse array indexed by category level so callers can iterate
104+
* shallowest levels first (used for the early-exit optimization when
105+
* fetching products by category).
106+
*
107+
* @param {Object} context - Request context (config, logger, headers, etc.).
108+
* @returns {Promise<string[][]>} Sparse array where index N holds urlPath strings at level N.
109+
*/
110+
async function getCategories(context) {
111+
const categoriesRes = await requestSaaS(
112+
CategoriesQuery,
113+
"getCategories",
114+
{},
115+
context,
116+
);
117+
const byLevel = [];
118+
for (const { urlPath, level } of categoriesRes.data.categories) {
119+
const idx = parseInt(level);
120+
byLevel[idx] = byLevel[idx] || [];
121+
byLevel[idx].push(urlPath);
122+
}
123+
return byLevel;
124+
}
125+
126+
module.exports = { getCategorySlugsFromFamilies, getCategories, hasFamilies };

0 commit comments

Comments
 (0)