Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ logs
# aem-commerce-prerender related
*.env
*.aio.json
.aem-commerce-prerender.json
.aem-commerce-prerender.json

# AI
.cursor/
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
* `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`
* `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
* `LOCALES`: Comma-separated list of locales (e.g., `en-us,en-gb,fr-fr`) or empty for non-localized sites
* `ACO_CATEGORY_FAMILIES`: *(Commerce Optimizer only)* Comma-separated list of Commerce Optimizer category family identifiers (e.g., `electronics,apparel`). When the product catalog contains 10,000 or fewer products, the system fetches all products for PDP pre-rendering in a single pass regardless of this setting. For catalogs exceeding 10,000 products, the system fetches the first 10,000 products from the catalog and additionally discovers all category slugs from the configured families to retrieve up to 10,000 products per category, deduplicating across both sets. If this setting is not configured, only the first 10,000 products will be pre-rendered. These category families also define which categories are included for PLP pre-rendering.
* `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.

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

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

Expand All @@ -87,9 +88,9 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
}
}
```
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)
1. Deploy the solution with `npm run deploy`
1. **Testing Actions Manually**: Before enabling automated triggers, verify that each action works correctly by invoking them manually:
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)
4. Deploy the solution with `npm run deploy`
5. **Testing Actions Manually**: Before enabling automated triggers, verify that each action works correctly by invoking them manually:
```bash
# Fetch all products from Catalog Service and store them in default-products.json
aio rt action invoke aem-commerce-ssg/fetch-all-products
Expand All @@ -100,7 +101,7 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
# Clean up and unpublish deleted products
aio rt action invoke aem-commerce-ssg/mark-up-clean-up
```
1. **Enable Automated Triggers**: Once you've confirmed that all actions work correctly, uncomment the triggers and rules sections in `app.config.yaml`:
6. **Enable Automated Triggers**: Once you've confirmed that all actions work correctly, uncomment the triggers and rules sections in `app.config.yaml`:
```yaml
triggers:
productPollerTrigger:
Expand All @@ -127,7 +128,7 @@ For detailed setup instructions, see the [Step-by-Step Configuration](#step-by-s
action: "mark-up-clean-up"
```
Then redeploy the solution: `npm run deploy`
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:
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:

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

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

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

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

### Management UI Setup

Expand Down
126 changes: 126 additions & 0 deletions actions/categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*

Copyright 2026 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.

*/

const { requestSaaS } = require("./utils");
const {
CategoriesQuery,
CategoryTreeQuery,
CategoryTreeBySlugsQuery,
} = require("./queries");

const MAX_TREE_DEPTH = 3;

/**
* Checks whether category families are configured (not the [null] default).
*
* @param {Array} families - The categoryFamilies array from runtime config.
* @returns {boolean}
*/
function hasFamilies(families) {
return Array.isArray(families) && families.length > 0 && families[0] !== null;
}

/**
* Resolves all category slugs belonging to the given ACO category families.
*
* Uses BFS traversal of the categoryTree API:
* 1. Query each family's root categories and their immediate childrenSlugs.
* 2. Query those children (with depth) to retrieve their descendants.
* 3. Repeat until no unresolved childrenSlugs remain.
*
* Handles trees of arbitrary depth even when the API caps depth at
* MAX_TREE_DEPTH per call — each iteration advances up to that many levels.
*
* @param {Object} context - Request context (config, logger, headers, etc.).
* @param {string[]} families - ACO category family identifiers.
* @returns {Promise<string[]>} Flat array of all unique category slugs.
*/
async function getCategorySlugsFromFamilies(context, families) {
console.debug("Getting category slugs from families:", families);
const allSlugs = new Set();

for (const family of families) {
console.debug("Getting category slugs from family:", family);
// Get root-level categories for this family
const firstLevel = await requestSaaS(
CategoryTreeQuery,
"getCategoryTree",
{ family },
context,
);

let pending = [];
for (const cat of firstLevel.data.categoryTree) {
allSlugs.add(cat.slug);
pending.push(...(cat.childrenSlugs || []));
}

// BFS: resolve children level by level until no new slugs remain
while (pending.length) {
// Mark pending as seen before querying to prevent re-processing
for (const slug of pending) allSlugs.add(slug);

const childrenRes = await requestSaaS(
CategoryTreeBySlugsQuery,
"getCategoryTreeBySlugs",
{ family, slugs: pending, depth: MAX_TREE_DEPTH },
context,
);

// First pass: capture any descendant slugs included due to depth traversal
for (const cat of childrenRes.data.categoryTree) {
allSlugs.add(cat.slug);
}

// Second pass: collect only new childrenSlugs for next iteration
pending = [];
for (const cat of childrenRes.data.categoryTree) {
for (const child of cat.childrenSlugs || []) {
if (!allSlugs.has(child)) pending.push(child);
}
}
}
}
console.debug("Category slugs resolved:", [...allSlugs]);

return [...allSlugs];
}

/**
* Retrieves all ACCS categories grouped by level.
*
* Returns a sparse array indexed by category level so callers can iterate
* shallowest levels first (used for the early-exit optimization when
* fetching products by category).
*
* @param {Object} context - Request context (config, logger, headers, etc.).
* @returns {Promise<string[][]>} Sparse array where index N holds urlPath strings at level N.
*/
async function getCategories(context) {
const categoriesRes = await requestSaaS(
CategoriesQuery,
"getCategories",
{},
context,
);
const byLevel = [];
for (const { urlPath, level } of categoriesRes.data.categories) {
const idx = parseInt(level);
byLevel[idx] = byLevel[idx] || [];
byLevel[idx].push(urlPath);
}
return byLevel;
}

module.exports = { getCategorySlugsFromFamilies, getCategories, hasFamilies };
Loading
Loading