Skip to content

Commit 5b08368

Browse files
authored
SITES-28176 - Support product URLs with only urlKeys (#18)
* Introduce path format and fetching products by urlKey * Fixes and testing * SITES-28179 - Add support for locale URLs (#19) * Add support for locale URLs * Allow config overrides from context variable * Adjust poller to use same configuration parameters * Fixes * Use HLX_LOCALES for allowedLocales * Update mapLocale to throw instead of using a default value
1 parent e442dfb commit 5b08368

File tree

16 files changed

+703
-265
lines changed

16 files changed

+703
-265
lines changed

actions/check-product-changes/lib/aem.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,17 +123,17 @@ class AdminAPI {
123123

124124
doPreview(item) {
125125
this.trackInFlight(`preview ${item.record.path}`, async (complete) => {
126-
const { log } = this.context;
126+
const { logger } = this.context;
127127
const { record } = item;
128128
const start = new Date();
129129

130130
try {
131131
record.previewedAt = new Date();
132132
await this.execAdminRequest('POST', 'preview', record.path);
133-
log.info(`Previewed ${record.path}`);
133+
logger.info(`Previewed ${record.path}`);
134134
this.publishQueue.push(item);
135135
} catch (e) {
136-
log.error(e);
136+
logger.error(e);
137137
// only resolve the item promise in case of an error
138138
item.resolve(record);
139139
} finally {
@@ -145,18 +145,18 @@ class AdminAPI {
145145

146146
doPublish(items) {
147147
this.trackInFlight(`publish ${items.length}`, async (complete) => {
148-
const { log } = this.context;
148+
const { logger } = this.context;
149149

150150
try {
151151
const paths = items.map(({ record }) => record.path);
152152
const body = { forceUpdate: false, paths };
153153
await this.execAdminRequest('POST', 'live', '/*', body);
154-
log.info(`Published ${items.length} items`);
154+
logger.info(`Published ${items.length} items`);
155155

156156
// set published date after publishing done
157157
items.forEach(({ record }) => record.publishedAt = new Date());
158158
} catch (e) {
159-
log.error(e);
159+
logger.error(e);
160160
} finally {
161161
complete();
162162
// resolve the original promises
@@ -167,16 +167,16 @@ class AdminAPI {
167167

168168
doUnpublishAndDelete(item) {
169169
this.trackInFlight(`unpublish ${item.record.path}`, async (complete) => {
170-
const { log } = this.context;
170+
const { logger } = this.context;
171171
const { record } = item;
172172

173173
try {
174174
await this.execAdminRequest('DELETE', 'live', record.path);
175175
await this.execAdminRequest('DELETE', 'preview', record.path);
176-
log.info(`Unpublished ${record.path}`);
176+
logger.info(`Unpublished ${record.path}`);
177177
record.deletedAt = new Date();
178178
} catch (e) {
179-
log.error(e);
179+
logger.error(e);
180180
} finally {
181181
complete();
182182
item.resolve(record);
@@ -186,8 +186,8 @@ class AdminAPI {
186186

187187
processQueues() {
188188
if (this.lastStatusLog < new Date() - 60000) {
189-
const { log } = this.context;
190-
log.info(`Queues: preview=${this.previewQueue.length},`
189+
const { logger } = this.context;
190+
logger.info(`Queues: preview=${this.previewQueue.length},`
191191
+ ` publish=${this.publishQueue.length},`
192192
+ ` unpublish=${this.unpublishQueue.length},`
193193
+ ` inflight=${this.inflight.length}`);

actions/check-product-changes/poller.js

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ governing permissions and limitations under the License.
1212

1313
const { Timings, aggregate } = require('./lib/benchmark');
1414
const { AdminAPI } = require('./lib/aem');
15-
const { requestSaaS, requestSpreadsheet, isValidUrl } = require('../utils');
15+
const { requestSaaS, requestSpreadsheet, isValidUrl, getProductUrl, mapLocale } = require('../utils');
1616
const { GetAllSkusQuery, GetLastModifiedQuery } = require('../queries');
1717
const { Core } = require('@adobe/aio-sdk');
1818

1919
const BATCH_SIZE = 50;
2020

21-
async function loadState(storeCode, stateLib) {
22-
const stateKey = storeCode ? `${storeCode}` : 'default';
21+
async function loadState(locale, stateLib) {
22+
const stateKey = locale ? `${locale}` : 'default';
2323
const stateData = await stateLib.get(stateKey);
2424
if (!stateData?.value) {
2525
return {
26-
storeCode,
26+
locale,
2727
skusLastQueriedAt: new Date(0),
2828
skus: {},
2929
};
@@ -34,7 +34,7 @@ async function loadState(storeCode, stateLib) {
3434
// folloed by a pair of SKUs and timestamps which are the last preview times per SKU
3535
const [catalogQueryTimestamp, ...skus] = stateData && stateData.value ? stateData.value.split(',') : [0];
3636
return {
37-
storeCode,
37+
locale,
3838
skusLastQueriedAt: new Date(parseInt(catalogQueryTimestamp)),
3939
skus: Object.fromEntries(skus
4040
.map((sku, i, arr) => (i % 2 === 0 ? [sku, new Date(parseInt(arr[i + 1]))] : null))
@@ -43,11 +43,11 @@ async function loadState(storeCode, stateLib) {
4343
}
4444

4545
async function saveState(state, stateLib) {
46-
let { storeCode } = state;
47-
if (!storeCode) {
48-
storeCode = 'default';
46+
let { locale } = state;
47+
if (!locale) {
48+
locale = 'default';
4949
}
50-
const stateKey = `${storeCode}`;
50+
const stateKey = `${locale}`;
5151
const stateData = [
5252
state.skusLastQueriedAt.getTime(),
5353
...Object.entries(state.skus).flatMap(([sku, lastPreviewedAt]) => [sku, lastPreviewedAt.getTime()]),
@@ -61,28 +61,28 @@ async function saveState(state, stateLib) {
6161
* state accordingly.
6262
*
6363
* @param {Object} params - The parameters object.
64-
* @param {string} params.siteName - The name of the site (repo or repoless).
65-
* @param {string} params.PDPURIPrefix - The URI prefix for Product Detail Pages.
64+
* @param {string} params.HLX_SITE_NAME - The name of the site (repo or repoless).
65+
* @param {string} params.HLX_PATH_FORMAT - The URL format for product detail pages.
6666
* @param {string} params.PLPURIPrefix - The URI prefix for Product List Pages.
67-
* @param {string} params.orgName - The name of the organization.
68-
* @param {string} params.configName - The name of the configuration json/xlsx.
67+
* @param {string} params.HLX_ORG_NAME - The name of the organization.
68+
* @param {string} params.HLX_CONFIG_NAME - The name of the configuration json/xlsx.
6969
* @param {number} [params.requestPerSecond=5] - The number of requests per second allowed by the throttling logic.
7070
* @param {string} params.authToken - The authentication token.
7171
* @param {number} [params.skusRefreshInterval=600000] - The interval for refreshing SKUs in milliseconds.
72-
* @param {string} [params.storeUrl] - The store's base URL.
73-
* @param {string} [params.storeCodes] - Comma-separated list of store codes.
72+
* @param {string} [params.HLX_STORE_URL] - The store's base URL.
73+
* @param {string} [params.HLX_LOCALES] - Comma-separated list of allowed locales.
7474
* @param {string} [params.LOG_LEVEL] - The log level.
7575
* @param {Object} stateLib - The state provider object.
7676
* @returns {Promise<Object>} The result of the polling action.
7777
*/
7878
function checkParams(params) {
79-
const requiredParams = ['siteName', 'PDPURIPrefix', 'PLPURIPrefix', 'orgName', 'configName', 'authToken'];
79+
const requiredParams = ['HLX_SITE_NAME', 'HLX_PATH_FORMAT', 'PLPURIPrefix', 'HLX_ORG_NAME', 'HLX_CONFIG_NAME', 'authToken'];
8080
const missingParams = requiredParams.filter(param => !params[param]);
8181
if (missingParams.length > 0) {
8282
throw new Error(`Missing required parameters: ${missingParams.join(', ')}`);
8383
}
8484

85-
if (params.storeUrl && !isValidUrl(params.storeUrl)) {
85+
if (params.HLX_STORE_URL && !isValidUrl(params.HLX_STORE_URL)) {
8686
throw new Error('Invalid storeUrl');
8787
}
8888
}
@@ -110,43 +110,47 @@ function shouldProcessProduct(product) {
110110
async function poll(params, stateLib) {
111111
checkParams(params);
112112

113-
const log = Core.Logger('main', { level: params.LOG_LEVEL || 'info' });
113+
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' });
114114
const {
115-
siteName,
116-
PDPURIPrefix,
117-
orgName,
118-
configName,
115+
HLX_SITE_NAME: siteName,
116+
HLX_PATH_FORMAT: pathFormat,
117+
HLX_ORG_NAME: orgName,
118+
HLX_CONFIG_NAME: configName,
119119
requestPerSecond = 5,
120120
authToken,
121121
skusRefreshInterval = 600000,
122122
} = params;
123-
const storeUrl = params.storeUrl ? params.storeUrl : `https://main--${siteName}--${orgName}.aem.live`;
124-
const storeCodes = params.storeCodes ? params.storeCodes.split(',') : [null];
123+
const storeUrl = params.HLX_STORE_URL ? params.HLX_STORE_URL : `https://main--${siteName}--${orgName}.aem.live`;
124+
const locales = params.HLX_LOCALES ? params.HLX_LOCALES.split(',') : [null];
125125

126126
const counts = {
127127
published: 0, unpublished: 0, ignored: 0, failed: 0,
128128
};
129129
const sharedContext = {
130-
storeUrl, configName, log, counts,
130+
storeUrl, configName, logger, counts, pathFormat,
131131
};
132132
const timings = new Timings();
133133
const adminApi = new AdminAPI({
134134
org: orgName,
135135
site: siteName,
136136
}, sharedContext, { requestPerSecond, authToken });
137137

138-
log.info(`Starting poll from ${storeUrl} for store codes ${storeCodes}`);
138+
logger.info(`Starting poll from ${storeUrl} for locales ${locales}`);
139139

140140
try {
141141
// start processing preview and publish queues
142142
await adminApi.startProcessing();
143143

144-
const results = await Promise.all(storeCodes.map(async (storeCode) => {
144+
const results = await Promise.all(locales.map(async (locale) => {
145145
const timings = new Timings();
146146
// load state
147-
const state = await loadState(storeCode, stateLib);
147+
const state = await loadState(locale, stateLib);
148148
timings.sample('loadedState');
149-
const context = { ...sharedContext, storeCode };
149+
150+
let context = { ...sharedContext };
151+
if (locale) {
152+
context = { ...context, ...mapLocale(locale, context) };
153+
}
150154

151155
// setup preview / publish queues
152156

@@ -173,7 +177,7 @@ async function poll(params, stateLib) {
173177
const skus = Object.keys(state.skus);
174178
const lastModifiedResp = await requestSaaS(GetLastModifiedQuery, 'getLastModified', { skus }, context);
175179
timings.sample('fetchedLastModifiedDates');
176-
log.info(`Fetched last modified date for ${lastModifiedResp.data.products.length} skus, total ${skus.length}`);
180+
logger.info(`Fetched last modified date for ${lastModifiedResp.data.products.length} skus, total ${skus.length}`);
177181

178182
// group preview in batches of 50
179183
let products = lastModifiedResp.data.products
@@ -198,7 +202,7 @@ async function poll(params, stateLib) {
198202
const batches = products.filter(shouldProcessProduct)
199203
.reduce((acc, product, i, arr) => {
200204
const { sku, urlKey } = product;
201-
const path = (storeCode ? `/${storeCode}${PDPURIPrefix}/${urlKey}/${sku}` : `${PDPURIPrefix}/${urlKey}/${sku}`).toLowerCase();
205+
const path = getProductUrl({ urlKey, sku }, context, false).toLowerCase();
202206
const req = adminApi.previewAndPublish({ path, sku });
203207
acc.push(req);
204208
if (acc.length === BATCH_SIZE || i === arr.length - 1) {
@@ -251,7 +255,7 @@ async function poll(params, stateLib) {
251255
}
252256
} catch (e) {
253257
// in case the index doesn't yet exist or any other error
254-
log.error(e);
258+
logger.error(e);
255259
}
256260

257261
timings.sample('unpublishedPaths');
@@ -277,14 +281,14 @@ async function poll(params, stateLib) {
277281
}
278282
timings.measures.previewDuration = aggregate(adminApi.previewDurations);
279283
} catch (e) {
280-
log.error(e);
284+
logger.error(e);
281285
// wait for queues to finish, even in error case
282286
await adminApi.stopProcessing();
283287
}
284288

285289
const elapsed = new Date() - timings.now;
286290

287-
log.info(`Finished polling, elapsed: ${elapsed}ms`);
291+
logger.info(`Finished polling, elapsed: ${elapsed}ms`);
288292

289293
return {
290294
state: 'completed',

actions/pdp-renderer/index.js

Lines changed: 58 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ const path = require('path');
1515

1616
const { Core } = require('@adobe/aio-sdk')
1717
const Handlebars = require('handlebars');
18-
const { errorResponse, stringParameters, requestSaaS } = require('../utils');
18+
const { errorResponse, stringParameters, requestSaaS, mapLocale } = require('../utils');
1919
const { extractPathDetails, findDescription, prepareBaseTemplate, getPrimaryImage, generatePriceString, getImageList } = require('./lib');
20-
const { ProductQuery } = require('../queries');
20+
const { ProductQuery, ProductByUrlKeyQuery } = require('../queries');
2121
const { generateLdJson } = require('./ldJson');
2222

2323
function toTemplateProductData(baseProduct) {
@@ -39,41 +39,78 @@ function toTemplateProductData(baseProduct) {
3939
* Parameters
4040
* @param {Object} params The parameters object
4141
* @param {string} params.__ow_path The path of the request
42-
* @param {string} params.__ow_query The query parameters of the request
43-
* @param {string} params.__ow_query.configName Overwrite for HLX_CONFIG_NAME
44-
* @param {string} params.__ow_query.contentUrl Overwrite for HLX_CONTENT_URL
45-
* @param {string} params.__ow_query.storeUrl Overwrite for HLX_STORE_URL
46-
* @param {string} params.__ow_query.productsTemplate Overwrite for HLX_PRODUCTS_TEMPLATE
42+
* @param {string} params.configName Overwrite for HLX_CONFIG_NAME using query parameter
43+
* @param {string} params.contentUrl Overwrite for HLX_CONTENT_URL using query parameter
44+
* @param {string} params.storeUrl Overwrite for HLX_STORE_URL using query parameter
45+
* @param {string} params.productsTemplate Overwrite for HLX_PRODUCTS_TEMPLATE using query parameter
46+
* @param {string} params.pathFormat Overwrite for HLX_PATH_FORMAT using query parameter
4747
* @param {string} params.HLX_CONFIG_NAME The config sheet to use (e.g. configs for prod, configs-dev for dev)
4848
* @param {string} params.HLX_CONTENT_URL Edge Delivery URL of the store (e.g. aem.live)
4949
* @param {string} params.HLX_STORE_URL Public facing URL of the store
5050
* @param {string} params.HLX_PRODUCTS_TEMPLATE URL to the products template page
51+
* @param {string} params.HLX_PATH_FORMAT The path format to use for parsing
5152
*/
5253
async function main (params) {
5354
const logger = Core.Logger('main', { level: params.LOG_LEVEL || 'info' })
5455

5556
try {
5657
logger.debug(stringParameters(params))
57-
const { __ow_path, __ow_query, HLX_STORE_URL, HLX_CONTENT_URL, HLX_CONFIG_NAME, HLX_PRODUCTS_TEMPLATE } = params;
58-
const { sku } = extractPathDetails(__ow_path);
59-
60-
const configName = __ow_query?.configName || HLX_CONFIG_NAME;
61-
const contentUrl = __ow_query?.contentUrl || HLX_CONTENT_URL;
62-
const storeUrl = __ow_query?.storeUrl || HLX_STORE_URL || contentUrl;
63-
const productsTemplate = __ow_query?.productsTemplate || HLX_PRODUCTS_TEMPLATE;
64-
const context = { contentUrl, storeUrl, configName, logger };
65-
66-
if (!sku || !contentUrl) {
58+
const {
59+
__ow_path,
60+
pathFormat : pathFormatQuery,
61+
configName : configNameQuery,
62+
contentUrl : contentUrlQuery,
63+
storeUrl : storeUrlQuery,
64+
productsTemplate : productsTemplateQuery,
65+
HLX_STORE_URL,
66+
HLX_CONTENT_URL,
67+
HLX_CONFIG_NAME,
68+
HLX_PRODUCTS_TEMPLATE,
69+
HLX_PATH_FORMAT,
70+
HLX_LOCALES,
71+
} = params;
72+
73+
const pathFormat = pathFormatQuery || HLX_PATH_FORMAT || '/products/{urlKey}/{sku}';
74+
const configName = configNameQuery || HLX_CONFIG_NAME;
75+
const contentUrl = contentUrlQuery || HLX_CONTENT_URL;
76+
const storeUrl = storeUrlQuery || HLX_STORE_URL || contentUrl;
77+
const productsTemplate = productsTemplateQuery || HLX_PRODUCTS_TEMPLATE;
78+
const allowedLocales = HLX_LOCALES ? HLX_LOCALES.split(',').map(a => a.trim()) : [];
79+
let context = { contentUrl, storeUrl, configName, logger, pathFormat, allowedLocales };
80+
81+
const result = extractPathDetails(__ow_path, pathFormat);
82+
logger.debug('Path parse results', JSON.stringify(result, null, 4));
83+
const { sku, urlKey, locale } = result;
84+
85+
if ((!sku && !urlKey) || !contentUrl) {
6786
return errorResponse(400, 'Invalid path', logger);
6887
}
6988

89+
// Map locale to context
90+
if (locale) {
91+
try {
92+
context = { ...context, ...mapLocale(locale, context) };
93+
// eslint-disable-next-line no-unused-vars
94+
} catch(e) {
95+
return errorResponse(400, 'Invalid locale', logger);
96+
}
97+
}
98+
7099
// Retrieve base product
71-
const baseProductData = await requestSaaS(ProductQuery, 'ProductQuery', { sku }, context);
72-
if (!baseProductData.data.products || baseProductData.data.products.length === 0) {
100+
let baseProduct;
101+
if (sku) {
102+
const baseProductData = await requestSaaS(ProductQuery, 'ProductQuery', { sku: sku.toUpperCase() }, context);
103+
if (!baseProductData?.data?.products || baseProductData?.data?.products?.length === 0) {
104+
return errorResponse(404, 'Product not found', logger);
105+
}
106+
baseProduct = baseProductData.data.products[0];
107+
} else if (urlKey) {
108+
const baseProductData = await requestSaaS(ProductByUrlKeyQuery, 'ProductByUrlKey', { urlKey }, context);
109+
if (!baseProductData?.data?.productSearch || baseProductData?.data?.productSearch?.items?.length === 0) {
73110
return errorResponse(404, 'Product not found', logger);
111+
}
112+
baseProduct = baseProductData.data.productSearch.items[0].productView;
74113
}
75-
const baseProduct = baseProductData.data.products[0];
76-
77114
logger.debug('Retrieved base product', JSON.stringify(baseProduct, null, 4));
78115

79116
// Assign meta tag data for template

actions/pdp-renderer/ldJson.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const { requestSaaS } = require('../utils');
2-
const { getProductUrl, findDescription, getPrimaryImage } = require('./lib');
1+
const { requestSaaS, getProductUrl } = require('../utils');
2+
const { findDescription, getPrimaryImage } = require('./lib');
33
const { VariantsQuery } = require('../queries');
44

55
function getOffer(product, url) {
@@ -64,9 +64,9 @@ async function getVariants(baseProduct, url, axes, context) {
6464
}
6565

6666
async function generateLdJson(product, context) {
67-
const { name, sku, urlKey, __typename } = product;
67+
const { name, sku, __typename } = product;
6868
const image = getPrimaryImage(product);
69-
const url = getProductUrl(urlKey, sku, context);
69+
const url = getProductUrl(product, context);
7070
const gtin = ''; // TODO: Add based on your data model (https://schema.org/gtin)
7171

7272
let ldJson;

0 commit comments

Comments
 (0)