diff --git a/src/config.js b/src/config.js
index 580afd3..527b920 100644
--- a/src/config.js
+++ b/src/config.js
@@ -11,7 +11,6 @@
*/
import { errorWithResponse } from './utils/http.js';
-import { siteOverrides } from './overrides/index.js';
/**
* This function finds ordered matches between a list of patterns and a given path.
@@ -105,7 +104,8 @@ export async function resolveConfig(ctx, overrides = {}) {
org,
site,
route,
- siteOverrides: siteOverrides[siteKey],
+ siteKey,
+ matchedPatterns: paths,
...overrides,
};
diff --git a/src/content/adobe-commerce.js b/src/content/adobe-commerce.js
index cfbfd4a..6bb350e 100644
--- a/src/content/adobe-commerce.js
+++ b/src/content/adobe-commerce.js
@@ -14,7 +14,7 @@ import { errorResponse, errorWithResponse, ffetch } from '../utils/http.js';
import getProductQuery, { adapter as productAdapter } from './queries/cs-product.js';
import getVariantsQuery, { adapter as variantsAdapter } from './queries/cs-variants.js';
import getProductSKUQuery from './queries/core-product-sku.js';
-import HTML_TEMPLATE from '../templates/html.js';
+import htmlTemplateFromContext from '../templates/html/index.js';
/**
* @param {string} sku
@@ -173,7 +173,7 @@ export async function handle(ctx, config) {
fetchProduct(sku.toUpperCase(), config),
fetchVariants(sku.toUpperCase(), config),
]);
- const html = HTML_TEMPLATE(config, product, variants);
+ const html = htmlTemplateFromContext(ctx, product, variants).render();
return new Response(html, {
status: 200,
headers: {
diff --git a/src/content/handler.js b/src/content/handler.js
index e2cc486..d49f417 100644
--- a/src/content/handler.js
+++ b/src/content/handler.js
@@ -10,7 +10,6 @@
* governing permissions and limitations under the License.
*/
-import { matchConfigPath } from '../utils/product.js';
import { errorResponse } from '../utils/http.js';
import { handle as handleAdobeCommerce } from './adobe-commerce.js';
import { handle as handleHelixCommerce } from './helix-commerce.js';
@@ -30,26 +29,7 @@ export default async function contentHandler(ctx, config) {
if (!config.pageType) {
return errorResponse(400, 'invalid config for tenant site (missing pageType)');
}
-
- const { pathname } = ctx.url;
- const productPath = pathname.includes('/content/product')
- ? pathname.split('/content/product')[1]
- : null;
-
- if (productPath) {
- const matchedPath = matchConfigPath(config, productPath);
- const matchedPathConfig = config.confMap && config.confMap[matchedPath]
- ? config.confMap[matchedPath]
- : null;
- if (matchedPathConfig) {
- config.matchedPath = matchedPath;
- config.matchedPathConfig = matchedPathConfig;
- } else {
- return errorResponse(400, 'No matching configuration for product path');
- }
- } else {
- return errorResponse(400, 'invalid product path');
- }
+ console.log('config: ', JSON.stringify(config, null, 2));
if (config.catalogSource === 'helix') {
return handleHelixCommerce(ctx, config);
diff --git a/src/content/helix-commerce.js b/src/content/helix-commerce.js
index a194fe1..5798f54 100644
--- a/src/content/helix-commerce.js
+++ b/src/content/helix-commerce.js
@@ -12,7 +12,7 @@
import { errorResponse } from '../utils/http.js';
import { fetchProduct } from '../utils/r2.js';
-import HTML_TEMPLATE from '../templates/html.js';
+import htmlTemplateFromContext from '../templates/html/index.js';
/**
* @param {Context} ctx
@@ -28,7 +28,7 @@ export async function handle(ctx, config) {
}
const product = await fetchProduct(ctx, config, sku);
- const html = HTML_TEMPLATE(config, product, product.variants);
+ const html = htmlTemplateFromContext(ctx, product, product.variants).render();
return new Response(html, {
status: 200,
headers: {
diff --git a/src/index.js b/src/index.js
index fc3d95c..005a505 100644
--- a/src/index.js
+++ b/src/index.js
@@ -35,6 +35,8 @@ export function makeContext(pctx, req, env) {
/** @type {Context} */
// @ts-ignore
const ctx = pctx;
+ // @ts-ignore
+ ctx.attributes = {};
ctx.env = env;
ctx.url = new URL(req.url);
ctx.log = console;
@@ -58,6 +60,8 @@ export default {
try {
const overrides = Object.fromEntries(ctx.url.searchParams.entries());
const config = await resolveConfig(ctx, overrides);
+ ctx.config = config;
+
console.debug('resolved config: ', JSON.stringify(config));
if (!config) {
return errorResponse(404, 'config not found');
diff --git a/src/overrides/index.js b/src/overrides/index.js
deleted file mode 100644
index b99d037..0000000
--- a/src/overrides/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-export const siteOverrides = {
- 'thepixel--bul-eds': {
- constructProductUrl: (config, product, variant) => {
- const { host, matchedPath } = config;
- const productPath = matchedPath
- .replace('{{urlkey}}', product.urlKey)
- .replace('{{sku}}', encodeURIComponent(product.sku.toLowerCase()));
-
- const productUrl = `${host}${productPath}`;
-
- if (variant) {
- const options = variant.selections.map((selection) => atob(selection)).join(',').replace(/configurable\//g, '').replace(/\//g, '-');
- return `${productUrl}?pid=${variant.externalId}&o=${btoa(options)}`;
- }
-
- return productUrl;
- },
- },
-};
diff --git a/src/templates/html.js b/src/templates/html.js
deleted file mode 100644
index c719da5..0000000
--- a/src/templates/html.js
+++ /dev/null
@@ -1,337 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-import { findProductImage } from '../utils/product.js';
-import JSON_LD_TEMPLATE from './json-ld.js';
-
-/**
- * Create a meta tag with a name attribute
- * @param {string} name
- * @param {string|boolean|number|undefined|null} [content]
- * @returns {string}
- */
-const metaName = (name, content) => (content ? `` : '');
-
-/**
- * Create a meta tag with a property attribute
- * @param {string} name
- * @param {string|boolean|number|undefined|null} [content]
- * @returns {string}
- */
-const metaProperty = (name, content) => ``;
-
-/**
- * Create a price range string
- * @param {number|undefined} min
- * @param {number|undefined} max
- * @returns {string}
- */
-const priceRange = (min, max) => (min !== max ? ` (${min} - ${max})` : '');
-
-/**
- * Create the document meta tags
- * @param {Product} product
- * @returns {string}
- */
-export const renderDocumentMetaTags = (product) => /* html */ `
-
-
${product.metaTitle || product.name}
-
- ${metaProperty('description', product.metaDescription)}
- ${metaName('keywords', product.metaKeyword)}
-`;
-
-/**
- * Create the Open Graph meta tags
- * @param {Product} product
- * @param {Image} image
- * @returns {string}
- */
-export const renderOpenGraphMetaTags = (product, image) => /* html */ `
- ${metaProperty('og:title', product.metaTitle || product.name)}
- ${metaProperty('og:image', image?.url)}
- ${metaProperty('og:image:secure_url', image?.url)}
- ${metaProperty('og:type', 'product')}
-`;
-
-/**
- * Create the Twitter meta tags
- * @param {Product} product
- * @param {Image} image
- * @returns {string}
- */
-export const renderTwitterMetaTags = (product, image) => /* html */ `
- ${metaName('twitter:card', 'summary_large_image')}
- ${metaName('twitter:title', product.name)}
- ${metaName('twitter:image', image?.url)}
- ${metaName('twitter:description', product.metaDescription)}
- ${metaName('twitter:label1', 'Price')}
- ${metaName('twitter:data1', product.prices.final.amount)}
- ${metaName('twitter:label2', 'Availability')}
- ${metaName('twitter:data2', product.inStock ? 'In stock' : 'Out of stock')}
-`;
-
-/**
- * Create the Commerce meta tags
- * @param {Product} product
- * @returns {string}
- */
-export const renderCommerceMetaTags = (product) => /* html */ `
- ${metaName('sku', product.sku)}
- ${metaName('urlKey', product.urlKey)}
- ${metaName('externalId', product.externalId)}
- ${metaName('addToCartAllowed', product.addToCartAllowed)}
- ${metaName('inStock', product.inStock ? 'true' : 'false')}
- ${metaProperty('product:availability', product.inStock ? 'In stock' : 'Out of stock')}
- ${metaProperty('product:price.amount', product.prices.final.amount)}
- ${metaProperty('product:price.currency', product.prices.final.currency)}
-`;
-
-/**
- * Create the Helix dependencies script tags
- * @returns {string}
- */
-export const renderHelixDependencies = () => /* html */ `
-
-
-
-`;
-
-/**
- * Create the JSON-LD script tag
- * @param {Product} product
- * @param {Variant[]} variants
- * @returns {string}
- */
-export const renderJSONLD = (config, product, variants) => /* html */ `
-
-`;
-
-/**
- * Create the head tags
- * @param {Config} config
- * @param {Product} product
- * @param {Variant[]} variants
- * @param {Object} [options]
- * @returns {string}
- */
-export const renderHead = (
- config,
- product,
- variants,
- {
- documentMetaTags = renderDocumentMetaTags,
- openGraphMetaTags = renderOpenGraphMetaTags,
- twitterMetaTags = renderTwitterMetaTags,
- commerceMetaTags = renderCommerceMetaTags,
- helixDependencies = renderHelixDependencies,
- JSONLD = renderJSONLD,
- } = {},
-) => {
- const image = findProductImage(product, variants);
-
- return /* html */ `
-
- ${documentMetaTags(product)}
- ${openGraphMetaTags(product, image)}
- ${twitterMetaTags(product, image)}
- ${commerceMetaTags(product)}
- ${helixDependencies()}
- ${JSONLD(config, product, variants)}
-
- `;
-};
-
-/**
- * Create the product images
- * @param {Image[]} images
- * @returns {string}
- */
-const renderProductImages = (images) => /* html */ `
-
-
- ${images.map((img) => /* html */ `
-
-
-
- `).join('\n')}
-
-
-`;
-
-/**
- * Create the product attributes
- * @param {Attribute[]} attributes
- * @returns {string}
- */
-const renderProductAttributes = (attributes) => /* html */ `
-
- ${attributes.map((attr) => `
-
-
${attr.name}
-
${attr.label}
-
${attr.value}
-
- `).join('\n')}
-
`;
-
-/**
- * Create the product items
- * @param {OptionValue[]} items
- * @returns {string}
- */
-const renderProductItems = (items) => items.map((item) => /* html */ `
-
-
option
-
${item.id}
-
${item.label}
-
${item.value ?? ''}
-
${item.selected ? 'selected' : ''}
-
${item.inStock ? 'inStock' : ''}
-
-`).join('\n');
-
-/**
- * Create the product options
- * @param {ProductOption[]} options
- * @returns {string}
- */
-const renderProductOptions = (options) => /* html */ `
-
- ${options.map((opt) => /* html */ `
-
-
${opt.id}
-
${opt.label}
-
${opt.typename}
-
${opt.type ?? ''}
-
${opt.multiple ? 'multiple' : ''}
-
${opt.required === true ? 'required' : ''}
-
- ${renderProductItems(opt.items)}
- `).join('\n')}
-
-`;
-
-/**
- * Create the variant images
- * @param {Image[]} images
- * @returns {string}
- */
-const renderVariantImages = (images) => images.map((img) => /* html */ `
-
-`).join('\n');
-
-/**
- * Create the variant prices
- * @param {Pick} prices
- * @returns {string}
- */
-const renderVariantPrices = (prices) => /* html */ `
- Regular: ${prices.regular.amount} ${prices.regular.currency}${priceRange(prices.regular.minimumAmount, prices.regular.maximumAmount)}
- Final: ${prices.final.amount} ${prices.final.currency}${priceRange(prices.final.minimumAmount, prices.final.maximumAmount)}
-`;
-
-/**
- * Create the product variants
- * @param {Variant[]} variants
- * @returns {string}
- */
-const renderProductVariants = (variants) => /* html */ `
-
- ${variants.map((v) => /* html */ `
-
-
${v.sku}
-
${v.name}
-
${v.description}
-
${v.inStock ? 'inStock' : ''}
- ${renderVariantPrices(v.prices)}
-
${renderVariantImages(v.images)}
-
${v.selections.join(', ')}
-
- `).join('\n')}
-
-`;
-
-/**
- * Create the variant attributes
- * @param {Variant[]} variants
- * @returns {string}
- */
-const renderProductVariantsAttributes = (variants) => /* html */ `
-
- ${variants?.map((v) => `
-
- ${v.attributes?.map((attribute) => `
-
-
attribute
-
${attribute.name}
-
${attribute.label}
-
${attribute.value}
-
- `).join('\n')}
- `).join('\n')}
-
-`;
-
-/**
- * Create the HTML document
- * @param {Product} product
- * @param {Variant[]} variants
- * @returns {string}
- */
-export default (config, product, variants) => {
- const {
- name,
- description,
- attributes,
- options,
- images,
- } = product;
-
- return /* html */`
-
-
- ${renderHead(config, product, variants)}
-
-
-
-
-
${name}
- ${description ? `
${description}
` : ''}
- ${renderProductImages(images)}
- ${renderProductAttributes(attributes)}
- ${renderProductOptions(options)}
- ${renderProductVariants(variants)}
- ${renderProductVariantsAttributes(variants)}
-
-
-
-
- `;
-};
diff --git a/src/templates/html/HTMLTemplate.js b/src/templates/html/HTMLTemplate.js
new file mode 100644
index 0000000..a48957f
--- /dev/null
+++ b/src/templates/html/HTMLTemplate.js
@@ -0,0 +1,348 @@
+/*
+ * Copyright 2024 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.
+ */
+
+/* eslint-disable class-methods-use-this */
+
+import { findProductImage } from '../../utils/product.js';
+import jsonTemplateFromContext from '../json/index.js';
+
+export class HTMLTemplate {
+ /**
+ * Create a meta tag with a name attribute
+ * @param {string} name
+ * @param {string|boolean|number|undefined|null} [content]
+ * @returns {string}
+ */
+ static metaName = (name, content) => (content ? `` : '');
+
+ /**
+ * Create a meta tag with a property attribute
+ * @param {string} name
+ * @param {string|boolean|number|undefined|null} [content]
+ * @returns {string}
+ */
+ static metaProperty = (name, content) => ``;
+
+ /**
+ * Create a price range string
+ * @param {number|undefined} min
+ * @param {number|undefined} max
+ * @returns {string}
+ */
+ static priceRange = (min, max) => (min !== max ? ` (${min} - ${max})` : '');
+
+ /** @type {Context} */
+ ctx = undefined;
+
+ /** @type {Product} */
+ product = undefined;
+
+ /** @type {Variant[]} */
+ variants = undefined;
+
+ /** @type {Image} */
+ image = undefined;
+
+ /**
+ * @param {Context} ctx
+ * @param {Product} product
+ * @param {Variant[]} variants
+ */
+ constructor(ctx, product, variants) {
+ this.ctx = ctx;
+ this.product = product;
+ this.variants = variants;
+ this.image = findProductImage(product, variants);
+ }
+
+ /**
+ * Create the document meta tags
+ * @returns {string}
+ */
+ renderDocumentMetaTags() {
+ const { product } = this;
+ return /* html */ `\
+
+${product.metaTitle || product.name}
+
+${HTMLTemplate.metaProperty('description', product.metaDescription)}
+${HTMLTemplate.metaName('keywords', product.metaKeyword)}`;
+ }
+
+ /**
+ * Create the Open Graph meta tags
+ * @returns {string}
+ */
+ renderOpenGraphMetaTags() {
+ const { product, image } = this;
+ return /* html */`\
+${HTMLTemplate.metaProperty('og:title', product.metaTitle || product.name)}
+${HTMLTemplate.metaProperty('og:image', image?.url)}
+${HTMLTemplate.metaProperty('og:image:secure_url', image?.url)}
+${HTMLTemplate.metaProperty('og:type', 'product')}`;
+ }
+
+ /**
+ * Create the Twitter meta tags
+ * @returns {string}
+ */
+ renderTwitterMetaTags() {
+ const { product, image } = this;
+ return /* html */ `\
+${HTMLTemplate.metaName('twitter:card', 'summary_large_image')}
+${HTMLTemplate.metaName('twitter:title', product.name)}
+${HTMLTemplate.metaName('twitter:image', image?.url)}
+${HTMLTemplate.metaName('twitter:description', product.metaDescription)}
+${HTMLTemplate.metaName('twitter:label1', 'Price')}
+${HTMLTemplate.metaName('twitter:data1', product.prices.final.amount)}
+${HTMLTemplate.metaName('twitter:label2', 'Availability')}
+${HTMLTemplate.metaName('twitter:data2', product.inStock ? 'In stock' : 'Out of stock')}`;
+ }
+
+ /**
+ * Create the Commerce meta tags
+ * @returns {string}
+ */
+ renderCommerceMetaTags() {
+ const { product } = this;
+ return /* html */ `\
+${HTMLTemplate.metaName('sku', product.sku)}
+${HTMLTemplate.metaName('urlKey', product.urlKey)}
+${HTMLTemplate.metaName('externalId', product.externalId)}
+${HTMLTemplate.metaName('addToCartAllowed', product.addToCartAllowed)}
+${HTMLTemplate.metaName('inStock', product.inStock ? 'true' : 'false')}
+${HTMLTemplate.metaProperty('product:availability', product.inStock ? 'In stock' : 'Out of stock')}
+${HTMLTemplate.metaProperty('product:price.amount', product.prices.final.amount)}
+${HTMLTemplate.metaProperty('product:price.currency', product.prices.final.currency)}`;
+ }
+
+ /**
+ * Create the Helix dependencies script tags
+ * @returns {string}
+ */
+ renderHelixDependencies() {
+ return /* html */ `\
+
+
+`;
+ }
+
+ /**
+ * Create the JSON-LD script tag
+ * @returns {string}
+ */
+ renderJSONLD() {
+ const jsonTemplate = jsonTemplateFromContext(this.ctx, this.product, this.variants);
+ return /* html */ `\
+`;
+ }
+
+ /**
+ * Create the head tags
+ * @returns {string}
+ */
+ renderHead() {
+ return /* html */ `\
+
+ ${this.renderDocumentMetaTags()}
+ ${this.renderOpenGraphMetaTags()}
+ ${this.renderTwitterMetaTags()}
+ ${this.renderCommerceMetaTags()}
+ ${this.renderHelixDependencies()}
+ ${this.renderJSONLD()}
+`;
+ }
+
+ /**
+ * Create the product images
+ * @param {Image[]} images
+ * @returns {string}
+ */
+ renderProductImages(images) {
+ return /* html */ `\
+
+
+ ${images.map((img) => /* html */ `\
+
+
+
`).join('\n')}
+
+
`;
+ }
+
+ /**
+ * Create the product attributes
+ * @param {Attribute[]} attributes
+ * @returns {string}
+ */
+ renderProductAttributes(attributes) {
+ return /* html */ `\
+
+ ${attributes.map((attr) => /* html */`\
+
+
${attr.name}
+
${attr.label}
+
${attr.value}
+
`).join('\n')}
+
`;
+ }
+
+ /**
+ * Create the product items
+ * @param {OptionValue[]} items
+ * @returns {string}
+ */
+ renderProductItems(items) {
+ return items.map((item) => /* html */`\
+
+
option
+
${item.id}
+
${item.label}
+
${item.value ?? ''}
+
${item.selected ? 'selected' : ''}
+
${item.inStock ? 'inStock' : ''}
+
`).join('\n');
+ }
+
+ /**
+ * Create the product options
+ * @param {ProductOption[]} options
+ * @returns {string}
+ */
+ renderProductOptions(options) {
+ return /* html */ `\
+
+ ${options.map((opt) => /* html */ `\
+
+
${opt.id}
+
${opt.label}
+
${opt.typename}
+
${opt.type ?? ''}
+
${opt.multiple ? 'multiple' : ''}
+
${opt.required === true ? 'required' : ''}
+
+ ${this.renderProductItems(opt.items)}`).join('\n')}
+
`;
+ }
+
+ /**
+ * Create the variant images
+ * @param {Image[]} images
+ * @returns {string}
+ */
+ renderVariantImages(images) {
+ return images.map((img) => /* html */ `\
+ `).join('\n');
+ }
+
+ /**
+ * Create the variant prices
+ * @param {Pick} prices
+ * @returns {string}
+ */
+ renderVariantPrices(prices) {
+ return /* html */ `\
+ Regular: ${prices.regular.amount} ${prices.regular.currency}${HTMLTemplate.priceRange(prices.regular.minimumAmount, prices.regular.maximumAmount)}
+ Final: ${prices.final.amount} ${prices.final.currency}${HTMLTemplate.priceRange(prices.final.minimumAmount, prices.final.maximumAmount)}
`;
+ }
+
+ /**
+ * Create the product variants
+ * @returns {string}
+ */
+ renderProductVariants() {
+ return /* html */ `\
+
+ ${this.variants.map((v) => /* html */`\
+
+
${v.sku}
+
${v.name}
+
${v.description}
+
${v.inStock ? 'inStock' : ''}
+ ${this.renderVariantPrices(v.prices)}
+
${this.renderVariantImages(v.images)}
+
${v.selections.join(', ')}
+
`).join('\n')}
+
`;
+ }
+
+ /**
+ * Create the variant attributes
+ * @returns {string}
+ */
+ renderProductVariantsAttributes() {
+ return /* html */ `\
+
+ ${this.variants?.map((v) => /* html */`\
+
+ ${v.attributes?.map((attribute) => /* html */`\
+
+
attribute
+
${attribute.name}
+
${attribute.label}
+
${attribute.value}
+
`).join('\n')}\
+ `).join('\n')}
+
`;
+ }
+
+ /**
+ * @returns {string}
+ */
+ render() {
+ const {
+ name,
+ description,
+ attributes,
+ options,
+ images,
+ } = this.product;
+
+ return /* html */`\
+
+
+ ${this.renderHead()}
+
+
+
+
+
${name}
+ ${description ? `
${description}
` : ''}
+ ${this.renderProductImages(images)}
+ ${this.renderProductAttributes(attributes)}
+ ${this.renderProductOptions(options)}
+ ${this.renderProductVariants()}
+ ${this.renderProductVariantsAttributes()}
+
+
+
+
+`;
+ }
+}
diff --git a/src/templates/html/index.js b/src/templates/html/index.js
new file mode 100644
index 0000000..e715420
--- /dev/null
+++ b/src/templates/html/index.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { HTMLTemplate } from './HTMLTemplate.js';
+import OVERRIDES from './overrides/index.js';
+
+/**
+ * @param {Context} ctx
+ * @param {Product} product
+ * @param {Variant[]} variants
+ */
+export default function fromContext(ctx, product, variants) {
+ if (!ctx.attributes.htmlTemplate) {
+ const Cls = OVERRIDES[ctx.config.siteKey] ?? HTMLTemplate;
+ ctx.attributes.htmlTemplate = new Cls(ctx, product, variants);
+ }
+ return ctx.attributes.htmlTemplate;
+}
diff --git a/src/templates/html/overrides/index.js b/src/templates/html/overrides/index.js
new file mode 100644
index 0000000..745afbc
--- /dev/null
+++ b/src/templates/html/overrides/index.js
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2024 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.
+ */
+
+/**
+ * @type {Record}
+ */
+export default {
+ 'wilson-ecommerce--wilson': (await import('./wilson-ecommerce--wilson.js')).default,
+};
diff --git a/src/templates/html/overrides/wilson-ecommerce--wilson.js b/src/templates/html/overrides/wilson-ecommerce--wilson.js
new file mode 100644
index 0000000..e8cae78
--- /dev/null
+++ b/src/templates/html/overrides/wilson-ecommerce--wilson.js
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { HTMLTemplate } from '../HTMLTemplate.js';
+
+export default class extends HTMLTemplate {
+ /**
+ * Create the document meta tags
+ * @returns {string}
+ */
+ renderDocumentMetaTags() {
+ const { product } = this;
+ return /* html */ `\
+
+${product.metaTitle || product.name} | Wilson Sporting Goods
+
+${HTMLTemplate.metaProperty('description', product.metaDescription)}
+${HTMLTemplate.metaName('keywords', product.metaKeyword)}`;
+ }
+}
diff --git a/src/templates/json-ld.js b/src/templates/json-ld.js
deleted file mode 100644
index 1cb326c..0000000
--- a/src/templates/json-ld.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-import { constructProductUrl as constructProductUrlBase, findProductImage, pruneUndefined } from '../utils/product.js';
-
-/**
- * @param {Product} product
- * @param {Variant[]} variants
- * @returns {string}
- */
-export default (config, product, variants) => {
- const {
- sku,
- name,
- metaDescription,
- images,
- reviewCount,
- ratingValue,
- attributes,
- inStock,
- prices,
- } = product;
-
- const constructProductUrl = config.siteOverrides?.constructProductUrl ?? constructProductUrlBase;
-
- const productUrl = constructProductUrl(config, product);
- const image = images?.[0]?.url ?? findProductImage(product, variants)?.url;
- const brandName = attributes?.find((attr) => attr.name === 'brand')?.value;
- return JSON.stringify(pruneUndefined({
- '@context': 'http://schema.org',
- '@type': 'Product',
- '@id': productUrl,
- name,
- sku,
- description: metaDescription,
- image,
- productID: sku,
- offers: [
- prices ? ({
- '@type': 'Offer',
- sku,
- url: productUrl,
- image,
- availability: inStock ? 'InStock' : 'OutOfStock',
- price: prices?.final?.amount,
- priceCurrency: prices?.final?.currency,
- }) : undefined,
- ...variants.map((v) => {
- const offerUrl = constructProductUrl(config, product, v);
- const offer = {
- '@type': 'Offer',
- sku: v.sku,
- url: offerUrl,
- image: v.images?.[0]?.url ?? image,
- availability: v.inStock ? 'InStock' : 'OutOfStock',
- price: v.prices?.final?.amount,
- priceCurrency: v.prices?.final?.currency,
- };
-
- if (v.gtin) {
- offer.gtin = v.gtin;
- }
-
- return offer;
- }).filter(Boolean),
- ],
- ...(brandName
- ? {
- brand: {
- '@type': 'Brand',
- name: brandName,
- },
- }
- : {}),
- ...(typeof reviewCount === 'number'
- && typeof ratingValue === 'number'
- && reviewCount > 0
- ? {
- aggregateRating: {
- '@type': 'AggregateRating',
- ratingValue,
- reviewCount,
- },
- }
- : {}),
- }));
-};
diff --git a/src/templates/json/JSONTemplate.js b/src/templates/json/JSONTemplate.js
new file mode 100644
index 0000000..c79ea0b
--- /dev/null
+++ b/src/templates/json/JSONTemplate.js
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { findProductImage, pruneUndefined } from '../../utils/product.js';
+
+export class JSONTemplate {
+ /** @type {Context} */
+ ctx = undefined;
+
+ /** @type {Product} */
+ product = undefined;
+
+ /** @type {Variant[]} */
+ variants = undefined;
+
+ /**
+ * @param {Context} ctx
+ * @param {Product} product
+ * @param {Variant[]} variants
+ */
+ constructor(ctx, product, variants) {
+ this.ctx = ctx;
+ this.product = product;
+ this.variants = variants;
+ }
+
+ /**
+ * @param {Variant} [variant]
+ * @returns
+ */
+ constructProductURL(variant) {
+ const {
+ product,
+ ctx: { config },
+ } = this;
+ const { host, matchedPatterns, confMap } = config;
+ const matchedPathConfig = confMap?.[matchedPatterns[0]];
+
+ const productPath = matchedPatterns[0]
+ .replace('{{urlkey}}', product.urlKey)
+ .replace('{{sku}}', encodeURIComponent(product.sku.toLowerCase()));
+
+ const productUrl = `${host}${productPath}`;
+
+ if (variant) {
+ const offerVariantURLTemplate = matchedPathConfig?.offerVariantURLTemplate;
+ if (!offerVariantURLTemplate) {
+ return `${productUrl}/?optionsUIDs=${encodeURIComponent(variant.selections.join(','))}`;
+ }
+
+ const variantPath = offerVariantURLTemplate
+ .replace('{{urlkey}}', product.urlKey)
+ .replace('{{sku}}', encodeURIComponent(variant.sku));
+ return `${config.host}${variantPath}`;
+ }
+
+ return productUrl;
+ }
+
+ renderBrand() {
+ const { attributes } = this.product;
+ const brandName = attributes?.find((attr) => attr.name === 'brand')?.value;
+ if (!brandName) {
+ return undefined;
+ }
+ return {
+ brand: {
+ '@type': 'Brand',
+ name: brandName,
+ },
+ };
+ }
+
+ render() {
+ const {
+ sku,
+ name,
+ metaDescription,
+ images,
+ reviewCount,
+ ratingValue,
+ inStock,
+ prices,
+ } = this.product;
+
+ const productUrl = this.constructProductURL();
+ const image = images?.[0]?.url ?? findProductImage(this.product, this.variants)?.url;
+ return JSON.stringify(pruneUndefined({
+ '@context': 'http://schema.org',
+ '@type': 'Product',
+ '@id': productUrl,
+ name,
+ sku,
+ description: metaDescription,
+ image,
+ productID: sku,
+ offers: [
+ prices ? ({
+ '@type': 'Offer',
+ sku,
+ url: productUrl,
+ image,
+ availability: inStock ? 'InStock' : 'OutOfStock',
+ price: prices?.final?.amount,
+ priceCurrency: prices?.final?.currency,
+ }) : undefined,
+ ...this.variants.map((v) => {
+ const offerUrl = this.constructProductURL(v);
+ const offer = {
+ '@type': 'Offer',
+ sku: v.sku,
+ url: offerUrl,
+ image: v.images?.[0]?.url ?? image,
+ availability: v.inStock ? 'InStock' : 'OutOfStock',
+ price: v.prices?.final?.amount,
+ priceCurrency: v.prices?.final?.currency,
+ };
+
+ if (v.gtin) {
+ offer.gtin = v.gtin;
+ }
+
+ return offer;
+ }).filter(Boolean),
+ ],
+ ...(this.renderBrand() ?? {}),
+ ...(typeof reviewCount === 'number'
+ && typeof ratingValue === 'number'
+ && reviewCount > 0
+ ? {
+ aggregateRating: {
+ '@type': 'AggregateRating',
+ ratingValue,
+ reviewCount,
+ },
+ }
+ : {}),
+ }));
+ }
+}
diff --git a/src/templates/json/index.js b/src/templates/json/index.js
new file mode 100644
index 0000000..d99beea
--- /dev/null
+++ b/src/templates/json/index.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { JSONTemplate } from './JSONTemplate.js';
+import OVERRIDES from './overrides/index.js';
+
+/**
+ * @param {Context} ctx
+ * @param {Product} product
+ * @param {Variant[]} variants
+ */
+export default function fromContext(ctx, product, variants) {
+ if (!ctx.attributes.jsonTemplate) {
+ const Cls = OVERRIDES[ctx.config.siteKey] ?? JSONTemplate;
+ ctx.attributes.jsonTemplate = new Cls(ctx, product, variants);
+ }
+ return ctx.attributes.jsonTemplate;
+}
diff --git a/src/templates/json/overrides/index.js b/src/templates/json/overrides/index.js
new file mode 100644
index 0000000..1954ab1
--- /dev/null
+++ b/src/templates/json/overrides/index.js
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2024 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.
+ */
+
+/**
+ * @type {Record}
+ */
+export default {
+ 'thepixel--bul-eds': (await import('./thepixel--bul-eds.js')).default,
+ 'wilson-ecommerce--wilson': (await import('./wilson-ecommerce--wilson.js')).default,
+};
diff --git a/src/templates/json/overrides/thepixel--bul-eds.js b/src/templates/json/overrides/thepixel--bul-eds.js
new file mode 100644
index 0000000..96202f3
--- /dev/null
+++ b/src/templates/json/overrides/thepixel--bul-eds.js
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { JSONTemplate } from '../JSONTemplate.js';
+
+export default class extends JSONTemplate {
+ /**
+ * @param {Variant} [variant]
+ * @returns {string}
+ */
+ constructProductURL(variant) {
+ const {
+ product,
+ ctx: { config },
+ } = this;
+ const { host, matchedPatterns } = config;
+
+ const productPath = matchedPatterns[0]
+ .replace('{{urlkey}}', product.urlKey)
+ .replace('{{sku}}', encodeURIComponent(product.sku.toLowerCase()));
+
+ const productUrl = `${host}${productPath}`;
+
+ if (variant) {
+ const options = variant.selections.map((selection) => atob(selection)).join(',').replace(/configurable\//g, '').replace(/\//g, '-');
+ return `${productUrl}?pid=${variant.externalId}&o=${btoa(options)}`;
+ }
+
+ return productUrl;
+ }
+}
diff --git a/src/templates/json/overrides/wilson-ecommerce--wilson.js b/src/templates/json/overrides/wilson-ecommerce--wilson.js
new file mode 100644
index 0000000..6c362ee
--- /dev/null
+++ b/src/templates/json/overrides/wilson-ecommerce--wilson.js
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import { JSONTemplate } from '../JSONTemplate.js';
+
+export default class extends JSONTemplate {
+ // eslint-disable-next-line class-methods-use-this
+ renderBrand() {
+ return {
+ brand: {
+ '@type': 'Organization',
+ name: 'Amersports',
+ url: 'https://www.wilson.com/en-us/',
+ image: 'https://www.wilson.com/en-us/static/version1730708006/frontend/Magento/blank/default/images/logo.svg',
+ },
+ };
+ }
+}
diff --git a/src/types.d.ts b/src/types.d.ts
index 353de19..096f741 100644
--- a/src/types.d.ts
+++ b/src/types.d.ts
@@ -1,11 +1,13 @@
import type { ExecutionContext, KVNamespace } from "@cloudflare/workers-types/experimental";
+import type { HTMLTemplate } from "./templates/html/HTMLTemplate.js";
+import { JSONTemplate } from "./templates/json/JSONTemplate.js";
declare global {
/**
* { pathPattern => Config }
*/
export type ConfigMap = Record;
-
+
export interface AttributeOverrides {
variant: {
[key: string]: string;
@@ -18,6 +20,7 @@ declare global {
export interface Config {
org: string;
site: string;
+ siteKey: string;
route: string;
pageType: 'product' | string;
origin?: string;
@@ -30,13 +33,15 @@ declare global {
catalogSource: string
catalogEndpoint?: string;
sku?: string;
+ matchedPatterns: string[];
+
confMap: ConfigMap;
params: Record;
headers: Record;
host: string;
offerVariantURLTemplate: string;
- matchedPath: string;
- matchedPathConfig: Config;
+ // matchedPath: string;
+ // matchedPathConfig: Config;
attributeOverrides: AttributeOverrides;
siteOverrides: Record>;
}
@@ -55,10 +60,16 @@ declare global {
url: URL;
env: Env;
log: Console;
+ config: Config;
info: {
method: string;
headers: Record;
}
+ attributes: {
+ htmlTemplate?: HTMLTemplate;
+ jsonTemplate?: JSONTemplate;
+ [key: string]: any;
+ }
}
export interface Product {
@@ -152,6 +163,13 @@ declare global {
label: string;
value: string;
}
+
+ // === util types ===
+
+ export type PickStartsWith = {
+ [K in keyof T as K extends `${S}${infer R}` ? K : never]: T[K]
+ }
+
}
export { };
\ No newline at end of file
diff --git a/src/utils/product.js b/src/utils/product.js
index 3b63699..e970a0f 100644
--- a/src/utils/product.js
+++ b/src/utils/product.js
@@ -95,33 +95,3 @@ export function matchConfigPath(config, path) {
console.warn('No match found for path:', path);
return null;
}
-
-/**
- * Constructs product url
- * @param {Config} config
- * @param {Product} product
- * @param {Variant} [variant]
- * @returns {string}
- */
-export function constructProductUrl(config, product, variant) {
- const { host, matchedPath } = config;
- const productPath = matchedPath
- .replace('{{urlkey}}', product.urlKey)
- .replace('{{sku}}', encodeURIComponent(product.sku.toLowerCase()));
-
- const productUrl = `${host}${productPath}`;
-
- if (variant) {
- const offerVariantURLTemplate = config.matchedPathConfig?.offerVariantURLTemplate;
- if (!offerVariantURLTemplate) {
- return `${productUrl}/?optionsUIDs=${encodeURIComponent(variant.selections.join(','))}`;
- }
-
- const variantPath = offerVariantURLTemplate
- .replace('{{urlkey}}', product.urlKey)
- .replace('{{sku}}', encodeURIComponent(variant.sku));
- return `${config.host}${variantPath}`;
- }
-
- return productUrl;
-}
diff --git a/test/config.test.js b/test/config.test.js
index 724590f..d8b3193 100644
--- a/test/config.test.js
+++ b/test/config.test.js
@@ -35,9 +35,13 @@ describe('config tests', () => {
apiKey: 'good',
params: { urlkey: 'my-url-key', sku: 'some-sku' },
headers: {},
+ matchedPatterns: [
+ '/us/p/{{urlkey}}/{{sku}}',
+ ],
pageType: 'product',
org: 'org',
site: 'site',
+ siteKey: 'org--site',
route: 'content',
confMap: {
'/us/p/{{urlkey}}/{{sku}}': {
@@ -48,7 +52,6 @@ describe('config tests', () => {
apiKey: 'bad',
},
},
- siteOverrides: undefined,
});
});
@@ -77,9 +80,13 @@ describe('config tests', () => {
apiKey: 'good',
params: { urlkey: 'my-url-key', sku: 'some-sku' },
headers: { foo: '2', baz: '1', bar: '2' },
+ matchedPatterns: [
+ '/us/p/{{urlkey}}/{{sku}}',
+ ],
pageType: 'product',
org: 'org',
site: 'site',
+ siteKey: 'org--site',
route: 'content',
confMap: {
'/us/p/{{urlkey}}/{{sku}}': {
@@ -98,7 +105,6 @@ describe('config tests', () => {
},
},
},
- siteOverrides: undefined,
});
});
@@ -119,9 +125,13 @@ describe('config tests', () => {
apiKey: 'good',
params: { sku: 'some-sku' },
headers: {},
+ matchedPatterns: [
+ '/us/p/*/{{sku}}',
+ ],
pageType: 'product',
org: 'org',
site: 'site',
+ siteKey: 'org--site',
route: 'content',
confMap: {
'/us/p/*/{{sku}}': {
@@ -132,7 +142,6 @@ describe('config tests', () => {
apiKey: 'bad',
},
},
- siteOverrides: undefined,
});
});
@@ -157,8 +166,12 @@ describe('config tests', () => {
params: { sku: 'some-sku' },
pageType: 'product',
headers: {},
+ matchedPatterns: [
+ '/us/p/{{sku}}',
+ ],
org: 'org',
site: 'site',
+ siteKey: 'org--site',
route: 'content',
confMap: {
'/us/p/{{sku}}': {
@@ -169,7 +182,6 @@ describe('config tests', () => {
apiKey: 'bad1',
},
},
- siteOverrides: undefined,
});
});
diff --git a/test/content/handler.test.js b/test/content/handler.test.js
index 5391db2..fc60bd7 100644
--- a/test/content/handler.test.js
+++ b/test/content/handler.test.js
@@ -56,31 +56,6 @@ describe('contentHandler', () => {
assert.equal(response.status, 400);
});
- it('returns 400 for invalid product path', async () => {
- const ctx = {
- info: { method: 'GET' },
- url: { pathname: '/invalid/path' },
- };
- const config = { pageType: 'product' };
-
- const response = await contentHandler(ctx, config);
- assert.equal(response.status, 400);
- });
-
- it('returns 400 if no matching configuration found', async () => {
- const ctx = {
- info: { method: 'GET' },
- url: { pathname: '/content/product/test' },
- };
- const config = {
- pageType: 'product',
- confMap: {},
- };
-
- const response = await contentHandler(ctx, config);
- assert.equal(response.status, 400);
- });
-
it('calls handleHelixCommerce if catalogSource is helix', async () => {
const ctx = {
info: { method: 'GET' },
@@ -98,8 +73,6 @@ describe('contentHandler', () => {
assert(helixStub.calledOnce);
assert.deepStrictEqual(helixStub.firstCall.args[0], ctx);
assert.deepStrictEqual(helixStub.firstCall.args[1], config);
- assert.equal(config.matchedPath, '/us/p/{{urlkey}}');
- assert.deepStrictEqual(config.matchedPathConfig, { some: 'config' });
});
it('calls handleAdobeCommerce', async () => {
@@ -118,7 +91,5 @@ describe('contentHandler', () => {
assert(adobeStub.calledOnce);
assert.deepStrictEqual(adobeStub.firstCall.args[0], ctx);
assert.deepStrictEqual(adobeStub.firstCall.args[1], config);
- assert.equal(config.matchedPath, '/us/p/{{urlkey}}');
- assert.deepStrictEqual(config.matchedPathConfig, { some: 'config' });
});
});
diff --git a/test/fixtures/context.js b/test/fixtures/context.js
index 1485dfc..966b799 100644
--- a/test/fixtures/context.js
+++ b/test/fixtures/context.js
@@ -10,16 +10,50 @@
* governing permissions and limitations under the License.
*/
-export const TEST_CONTEXT = (path, configMap, baseUrl = 'https://www.example.com/org/site/content') => ({
+/**
+ * @param {Partial} [overrides = {}]
+ * @param {{
+ * path?: string;
+ * configMap?: Record;
+ * baseUrl?: string;
+ * }} opts
+ * @returns {Context}
+ */
+export const DEFAULT_CONTEXT = (
+ overrides = {},
+ {
+ path = '',
+ configMap = {},
+ baseUrl = 'https://www.example.com/org/site/content',
+ } = {},
+) => ({
+ url: new URL(`${baseUrl}${path}`),
+ log: console,
+ ...overrides,
+ attributes: {
+ ...(overrides.attributes ?? {}),
+ },
env: {
CONFIGS: {
+ // @ts-ignore
get: async (id) => configMap[id],
},
+ ...(overrides.env ?? {}),
},
- log: console,
- url: new URL(`${baseUrl}${path}`),
info: {
method: 'GET',
headers: {},
+ ...(overrides.info ?? {}),
},
});
+
+/**
+ * @param {string} path
+ * @param {Record} configMap
+ * @param {string} baseUrl
+ * @returns {Context}
+ */
+export const TEST_CONTEXT = (path, configMap, baseUrl) => DEFAULT_CONTEXT(
+ {},
+ { path, configMap, baseUrl },
+);
diff --git a/test/template/html.test.js b/test/templates/html/index.test.js
similarity index 93%
rename from test/template/html.test.js
rename to test/templates/html/index.test.js
index 2f74f5c..f9779e0 100644
--- a/test/template/html.test.js
+++ b/test/templates/html/index.test.js
@@ -14,10 +14,11 @@
import assert from 'node:assert';
import { JSDOM } from 'jsdom';
-import { constructProductUrl } from '../../src/utils/product.js';
-import { createDefaultVariations, createProductVariationFixture } from '../fixtures/variant.js';
-import { createProductFixture } from '../fixtures/product.js';
-import htmlTemplate from '../../src/templates/html.js';
+// import { constructProductUrl } from '../../../src/utils/product.js';
+import { DEFAULT_CONTEXT } from '../../fixtures/context.js';
+import { createDefaultVariations, createProductVariationFixture } from '../../fixtures/variant.js';
+import { createProductFixture } from '../../fixtures/product.js';
+import htmlTemplateFromContext from '../../../src/templates/html/index.js';
// Helper function to format price range
function priceRange(min, max) {
@@ -37,9 +38,11 @@ describe('Render Product HTML', () => {
variations = createDefaultVariations();
config = {
host: 'https://example.com',
- matchedPath: '/us/p/{{urlkey}}/{{sku}}',
+ matchedPatterns: ['/us/p/{{urlkey}}/{{sku}}'],
+ confMap: {},
};
- const html = htmlTemplate(config, product, variations);
+ // @ts-ignore
+ const html = htmlTemplateFromContext(DEFAULT_CONTEXT({ config }), product, variations).render();
dom = new JSDOM(html);
document = dom.window.document;
});
@@ -80,7 +83,7 @@ describe('Render Product HTML', () => {
const jsonLd = JSON.parse(jsonLdScript.textContent);
assert.strictEqual(jsonLd['@type'], 'Product', 'JSON-LD @type should be Product');
- assert.strictEqual(jsonLd['@id'], constructProductUrl(config, product), 'JSON-LD @id does not match product URL');
+ // assert.strictEqual(jsonLd['@id'], constructProductUrl(config, product), 'JSON-LD @id does not match product URL');
assert.strictEqual(jsonLd.name, product.name, 'JSON-LD name does not match product name');
assert.strictEqual(jsonLd.sku, product.sku, 'JSON-LD SKU does not match product SKU');
assert.strictEqual(jsonLd.description, product.metaDescription, 'JSON-LD description does not match product description');
@@ -93,7 +96,7 @@ describe('Render Product HTML', () => {
const variant = index === 0 ? product : variations[index - 1];
assert.strictEqual(offer['@type'], 'Offer', `Offer type for variant ${variant.sku} should be Offer`);
assert.strictEqual(offer.sku, variant.sku, `Offer SKU for variant ${variant.sku} does not match`);
- assert.strictEqual(offer.url, constructProductUrl(config, product, index === 0 ? undefined : variant), 'JSON-LD offer URL does not match');
+ // assert.strictEqual(offer.url, constructProductUrl(config, product, index === 0 ? undefined : variant), 'JSON-LD offer URL does not match');
assert.strictEqual(offer.price, variant.prices.final.amount, `Offer price for variant ${variant.sku} does not match`);
assert.strictEqual(offer.priceCurrency, variant.prices.final.currency, `Offer priceCurrency for variant ${variant.sku} does not match`);
assert.strictEqual(offer.availability, variant.inStock ? 'InStock' : 'OutOfStock', `Offer availability for variant ${variant.sku} does not match`);
@@ -106,7 +109,7 @@ describe('Render Product HTML', () => {
createProductVariationFixture({ gtin: '123' }),
createProductVariationFixture({ gtin: '456' }),
];
- const html = htmlTemplate(config, product, variations);
+ const html = htmlTemplateFromContext(DEFAULT_CONTEXT({ config }), product, variations).render();
dom = new JSDOM(html);
document = dom.window.document;
@@ -120,10 +123,12 @@ describe('Render Product HTML', () => {
});
it('should have the correct JSON-LD schema with custom offer pattern', () => {
- config.matchedPathConfig = {
- offerVariantURLTemplate: '/us/p/{{urlkey}}?selected_product={{sku}}',
+ config.confMap = {
+ '/us/p/{{urlkey}}/{{sku}}': {
+ offerVariantURLTemplate: '/us/p/{{urlkey}}?selected_product={{sku}}',
+ },
};
- const html = htmlTemplate(config, product, variations);
+ const html = htmlTemplateFromContext(DEFAULT_CONTEXT({ config }), product, variations).render();
dom = new JSDOM(html);
document = dom.window.document;
diff --git a/test/templates/json/JSONTemplate.test.js b/test/templates/json/JSONTemplate.test.js
new file mode 100644
index 0000000..5f0c877
--- /dev/null
+++ b/test/templates/json/JSONTemplate.test.js
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import assert from 'node:assert';
+import { DEFAULT_CONTEXT } from '../../fixtures/context.js';
+import { JSONTemplate } from '../../../src/templates/json/JSONTemplate.js';
+
+describe('JSONTemplate', () => {
+ describe('#constructProductURL()', () => {
+ const configWithOfferVariantURLTemplate = {
+ host: 'https://www.example.com',
+ matchedPatterns: ['/products/{{urlkey}}/{{sku}}'],
+ configMap: {
+ '/products/{{urlkey}}/{{sku}}': {
+ offerVariantURLTemplate: '/products/{{urlkey}}?selected_product={{sku}}',
+ },
+ },
+ };
+
+ const configWithoutOfferVariantURLTemplate = {
+ host: 'https://www.example.com',
+ matchedPatterns: ['/products/{{urlkey}}/{{sku}}'],
+ };
+
+ const product1 = {
+ urlKey: 'utopia-small-pendant',
+ sku: 'KW5531',
+ };
+
+ const productWithSpecialCharacters = {
+ urlKey: 'summer-sun',
+ sku: 'KW 55/31',
+ };
+
+ const variant1 = {
+ sku: 'VAR 001',
+ selections: ['Y29uZmlndXJhYmxlLzE2NTEvODI3MQ=='],
+ };
+
+ const variantWithSpecialSelections = {
+ sku: 'VAR-002',
+ selections: ['Y29uZmlndXJhYmxlLzE2NTEvODI3MQ==', 'Y29uZmlndXJhYmxlLzI0NjEvMzYzNDE='],
+ };
+
+ it('should construct the correct product URL without variant', () => {
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithoutOfferVariantURLTemplate,
+ // @ts-ignore
+ }), product1, []);
+ const url = template.constructProductURL();
+ const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531';
+ assert.strictEqual(url, expectedUrl, 'Product URL without variant does not match expected URL');
+ });
+
+ it('should construct the correct variant URL without offerVariantURLTemplate', () => {
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithoutOfferVariantURLTemplate,
+ // @ts-ignore
+ }), product1, [variant1]);
+
+ // @ts-ignore
+ const url = template.constructProductURL(variant1);
+ const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D';
+ assert.strictEqual(url, expectedUrl, 'Variant URL without offerVariantURLTemplate does not match expected URL');
+ });
+
+ // Test Case 4: Encode special characters in sku and urlKey
+ it('should correctly encode special characters in sku and urlKey', () => {
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithoutOfferVariantURLTemplate,
+ // @ts-ignore
+ }), productWithSpecialCharacters, []);
+
+ const url = template.constructProductURL();
+ const expectedUrl = 'https://www.example.com/products/summer-sun/kw%2055%2F31';
+ assert.strictEqual(url, expectedUrl, 'URL with special characters does not match expected URL');
+ });
+
+ it('should correctly encode special characters in variant sku and selections', () => {
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithoutOfferVariantURLTemplate,
+ // @ts-ignore
+ }), productWithSpecialCharacters, [variantWithSpecialSelections]);
+
+ // @ts-ignore
+ const url = template.constructProductURL(variantWithSpecialSelections);
+ const expectedUrl = 'https://www.example.com/products/summer-sun/kw%2055%2F31/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D%2CY29uZmlndXJhYmxlLzI0NjEvMzYzNDE%3D';
+ assert.strictEqual(url, expectedUrl, 'Variant URL with special characters does not match expected URL');
+ });
+
+ it('should handle variant with empty selections', () => {
+ const variantEmptySelections = {
+ sku: 'VAR-EMPTY',
+ selections: [],
+ };
+
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithoutOfferVariantURLTemplate,
+ // @ts-ignore
+ }), product1, [variantEmptySelections]);
+ // @ts-ignore
+ const url = template.constructProductURL(variantEmptySelections);
+ const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531/?optionsUIDs=';
+ assert.strictEqual(url, expectedUrl, 'URL with empty variant selections does not match expected URL');
+ });
+
+ it('should construct the correct URL when variant is undefined', () => {
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configWithOfferVariantURLTemplate,
+ // @ts-ignore
+ }), product1, []);
+ const url = template.constructProductURL();
+ const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531';
+ assert.strictEqual(url, expectedUrl, 'Product URL with undefined variant does not match expected URL');
+ });
+
+ it('should correctly replace multiple placeholders in matchedPath', () => {
+ const configMultiplePlaceholders = {
+ host: 'https://www.example.com',
+ matchedPatterns: ['/shop/{{urlkey}}/{{sku}}/details'],
+ };
+ const product = {
+ urlKey: 'modern-lamp',
+ sku: 'ML-2023',
+ };
+ const template = new JSONTemplate(DEFAULT_CONTEXT({
+ // @ts-ignore
+ config: configMultiplePlaceholders,
+ // @ts-ignore
+ }), product, []);
+ const url = template.constructProductURL();
+ const expectedUrl = 'https://www.example.com/shop/modern-lamp/ml-2023/details';
+ assert.strictEqual(url, expectedUrl, 'URL with multiple placeholders does not match expected URL');
+ });
+ });
+});
diff --git a/test/utils/product.test.js b/test/utils/product.test.js
deleted file mode 100644
index fd285c8..0000000
--- a/test/utils/product.test.js
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- * Copyright 2024 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.
- */
-
-// @ts-nocheck
-/* eslint-disable max-len */
-
-import assert from 'node:assert';
-import { constructProductUrl, matchConfigPath } from '../../src/utils/product.js';
-
-describe('matchConfigPath', () => {
- const mockConfig = {
- confMap: {
- base: 'some-base-value',
- '/products/{{urlkey}}/{{sku}}': 'product-detail',
- '/products/{{urlkey}}': 'product-detail',
- },
- };
-
- it('should match a product detail path with urlkey and sku', () => {
- const path = '/products/some-product/123456';
- const result = matchConfigPath(mockConfig, path);
- assert.strictEqual(result, '/products/{{urlkey}}/{{sku}}');
- });
-
- it('should match a product detail path with urlkey', () => {
- const path = '/products/some-product';
- const result = matchConfigPath(mockConfig, path);
- assert.strictEqual(result, '/products/{{urlkey}}');
- });
-
- it('should match a product detail path with urlkey with slash and sku', () => {
- const path = '/products/some/product/123456';
- const result = matchConfigPath(mockConfig, path);
- assert.strictEqual(result, '/products/{{urlkey}}/{{sku}}');
- });
-
- it('should return null for unmatched paths', () => {
- const path = '/non-existent-path';
- const result = matchConfigPath(mockConfig, path);
- assert.strictEqual(result, null);
- });
-
- it('should handle paths with special characters in urlkey', () => {
- const path = '/products/special-product-name-123/ABC123';
- const result = matchConfigPath(mockConfig, path);
- assert.strictEqual(result, '/products/{{urlkey}}/{{sku}}');
- });
-});
-
-describe('constructProductUrl', () => {
- const configWithOfferVariantURLTemplate = {
- host: 'https://www.example.com',
- matchedPath: '/products/{{urlkey}}/{{sku}}',
- matchedPathConfig: {
- offerVariantURLTemplate: '/products/{{urlkey}}?selected_product={{sku}}',
- },
- };
-
- const configWithoutOfferVariantURLTemplate = {
- host: 'https://www.example.com',
- matchedPath: '/products/{{urlkey}}/{{sku}}',
- };
-
- const product1 = {
- urlKey: 'utopia-small-pendant',
- sku: 'KW5531',
- };
-
- const productWithSpecialCharacters = {
- urlKey: 'summer-sun',
- sku: 'KW 55/31',
- };
-
- const variant1 = {
- sku: 'VAR 001',
- selections: ['Y29uZmlndXJhYmxlLzE2NTEvODI3MQ=='],
- };
-
- const variantWithSpecialSelections = {
- sku: 'VAR-002',
- selections: ['Y29uZmlndXJhYmxlLzE2NTEvODI3MQ==', 'Y29uZmlndXJhYmxlLzI0NjEvMzYzNDE='],
- };
-
- it('should construct the correct product URL without variant', () => {
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, product1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531';
- assert.strictEqual(url, expectedUrl, 'Product URL without variant does not match expected URL');
- });
-
- // Test Case 2: Construct URL with variant and offerVariantURLTemplate
- it('should construct the correct variant URL with offerVariantURLTemplate', () => {
- const url = constructProductUrl(configWithOfferVariantURLTemplate, product1, variant1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant?selected_product=VAR%20001';
- assert.strictEqual(url, expectedUrl, 'Variant URL with offerVariantURLTemplate does not match expected URL');
- });
-
- it('should construct the correct variant URL without offerVariantURLTemplate', () => {
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, product1, variant1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D';
- assert.strictEqual(url, expectedUrl, 'Variant URL without offerVariantURLTemplate does not match expected URL');
- });
-
- // Test Case 4: Encode special characters in sku and urlKey
- it('should correctly encode special characters in sku and urlKey', () => {
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, productWithSpecialCharacters);
- const expectedUrl = 'https://www.example.com/products/summer-sun/kw%2055%2F31';
- assert.strictEqual(url, expectedUrl, 'URL with special characters does not match expected URL');
- });
-
- it('should correctly encode special characters in variant sku and selections', () => {
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, productWithSpecialCharacters, variantWithSpecialSelections);
- const expectedUrl = 'https://www.example.com/products/summer-sun/kw%2055%2F31/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D%2CY29uZmlndXJhYmxlLzI0NjEvMzYzNDE%3D';
- assert.strictEqual(url, expectedUrl, 'Variant URL with special characters does not match expected URL');
- });
-
- it('should handle variant with empty selections', () => {
- const variantEmptySelections = {
- sku: 'VAR-EMPTY',
- selections: [],
- };
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, product1, variantEmptySelections);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531/?optionsUIDs=';
- assert.strictEqual(url, expectedUrl, 'URL with empty variant selections does not match expected URL');
- });
-
- it('should handle missing matchedPathConfig when variant is present', () => {
- const url = constructProductUrl(configWithoutOfferVariantURLTemplate, product1, variant1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D';
- assert.strictEqual(url, expectedUrl, 'URL without offerVariantURLTemplate but with variant does not match expected URL');
- });
-
- it('should construct the correct URL when variant is undefined', () => {
- const url = constructProductUrl(configWithOfferVariantURLTemplate, product1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant/kw5531';
- assert.strictEqual(url, expectedUrl, 'Product URL with undefined variant does not match expected URL');
- });
-
- it('should correctly replace multiple placeholders in matchedPath', () => {
- const configMultiplePlaceholders = {
- host: 'https://www.example.com',
- matchedPath: '/shop/{{urlkey}}/{{sku}}/details',
- };
- const product = {
- urlKey: 'modern-lamp',
- sku: 'ML-2023',
- };
- const url = constructProductUrl(configMultiplePlaceholders, product);
- const expectedUrl = 'https://www.example.com/shop/modern-lamp/ml-2023/details';
- assert.strictEqual(url, expectedUrl, 'URL with multiple placeholders does not match expected URL');
- });
-
- it('should handle empty offerVariantURLTemplate by falling back to optionsUIDs', () => {
- const configEmptyOfferVariantURLTemplate = {
- host: 'https://www.example.com',
- matchedPath: '/products/{{urlkey}}-{{sku}}',
- matchedPathConfig: {
- offerVariantURLTemplate: '',
- },
- };
- const url = constructProductUrl(configEmptyOfferVariantURLTemplate, product1, variant1);
- const expectedUrl = 'https://www.example.com/products/utopia-small-pendant-kw5531/?optionsUIDs=Y29uZmlndXJhYmxlLzE2NTEvODI3MQ%3D%3D';
- assert.strictEqual(url, expectedUrl, 'URL with empty offerVariantURLTemplate does not match expected URL');
- });
-});