Skip to content

Commit

Permalink
feat: add ratings to json-ld (#44)
Browse files Browse the repository at this point in the history
* feat: add ratings to json-ld

* fix: tweak rating object

* fix: cleanup

* chore: fix test
  • Loading branch information
maxakuru authored Nov 11, 2024
1 parent 87471aa commit 76858f4
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 77 deletions.
2 changes: 1 addition & 1 deletion src/content/adobe-commerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ async function fetchProduct(sku, config) {
if (!productData) {
throw errorWithResponse(404, 'could not find product', json.errors);
}
const product = productAdapter(productData);
const product = productAdapter(config, productData);
return product;
} catch (e) {
console.error('failed to parse product: ', e);
Expand Down
2 changes: 1 addition & 1 deletion src/content/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default async function contentHandler(ctx) {
if (!config.pageType) {
return errorResponse(400, 'invalid config for tenant site (missing pageType)');
}
console.log('config: ', JSON.stringify(config, null, 2));
ctx.log.debug('config: ', JSON.stringify(config, null, 2));

if (config.catalogSource === 'helix') {
return handleHelixCommerce(ctx);
Expand Down
20 changes: 16 additions & 4 deletions src/content/queries/cs-product.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
*/

import { forceImagesHTTPS } from '../../utils/http.js';
import { gql, parseSpecialToDate } from '../../utils/product.js';
import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js';

/**
* @param {Config} config
* @param {any} productData
* @returns {Product}
*/
export const adapter = (productData) => {
export const adapter = (config, productData) => {
let minPrice = productData.priceRange?.minimum ?? productData.price;
let maxPrice = productData.priceRange?.maximum ?? productData.price;

Expand All @@ -42,6 +43,8 @@ export const adapter = (productData) => {
externalId: productData.externalId,
images: forceImagesHTTPS(productData.images) ?? [],
attributes: productData.attributes ?? [],
attributeMap: Object.fromEntries((productData.attributes ?? [])
.map(({ name, value }) => [name, value])),
options: (productData.options ?? []).map((option) => ({
id: option.id,
label: option.title,
Expand Down Expand Up @@ -76,25 +79,34 @@ export const adapter = (productData) => {
currency: minPrice.regular.amount.currency,
maximumAmount: maxPrice.regular.amount.value,
minimumAmount: minPrice.regular.amount.value,
// TODO: add variant?
},
final: {
// TODO: determine whether to use min or max
amount: minPrice.final.amount.value,
currency: minPrice.final.amount.currency,
maximumAmount: maxPrice.final.amount.value,
minimumAmount: minPrice.final.amount.value,
// TODO: add variant?
},
visible: minPrice.roles?.includes('visible'),
} : null,
};

if (config.attributeOverrides?.product) {
Object.entries(config.attributeOverrides.product).forEach(([key, value]) => {
product[key] = product.attributeMap[value] ?? product[key];
});
}

const specialToDate = parseSpecialToDate(product);
if (specialToDate) {
product.specialToDate = specialToDate;
}

const rating = parseRating(product);
if (rating) {
product.rating = rating;
}

return product;
};

Expand Down
20 changes: 14 additions & 6 deletions src/content/queries/cs-variants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
*/

import { forceImagesHTTPS } from '../../utils/http.js';
import { gql, parseSpecialToDate } from '../../utils/product.js';
import { gql, parseRating, parseSpecialToDate } from '../../utils/product.js';

/**
* @param {Config} config
* @param {any} variants
* @returns {Variant[]}
*/
Expand All @@ -30,6 +31,8 @@ export const adapter = (config, variants) => variants.map(({ selections, product
inStock: product.inStock,
images: forceImagesHTTPS(product.images) ?? [],
attributes: product.attributes ?? [],
attributeMap: Object.fromEntries((product.attributes ?? [])
.map(({ name, value }) => [name, value])),
externalId: product.externalId,
prices: {
regular: {
Expand All @@ -50,15 +53,20 @@ export const adapter = (config, variants) => variants.map(({ selections, product
selections: (selections ?? []).sort(),
};

const specialToDate = parseSpecialToDate(product);
if (config.attributeOverrides?.variant) {
Object.entries(config.attributeOverrides.variant).forEach(([key, value]) => {
variant[key] = variant.attributeMap[value] ?? variant[key];
});
}

const specialToDate = parseSpecialToDate(variant);
if (specialToDate) {
variant.specialToDate = specialToDate;
}

if (config.attributeOverrides?.variant) {
Object.entries(config.attributeOverrides.variant).forEach(([key, value]) => {
variant[key] = product.attributes?.find((attr) => attr.name === value)?.value;
});
const rating = parseRating(variant);
if (rating) {
variant.rating = rating;
}

return variant;
Expand Down
68 changes: 40 additions & 28 deletions src/templates/json/JSONTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,17 @@ export class JSONTemplate {
* @param {Variant} [variant]
*/
constructMPN(variant) {
const { attributeMap: productAttrs } = this.product;
const { attributeMap: variantAttrs } = variant || {};

return variant
? variant.attributes.find((attr) => attr.name.toLowerCase() === 'mpn')?.value ?? this.constructMPN()
: this.product.attributes.find((attr) => attr.name.toLowerCase() === 'mpn')?.value ?? undefined;
? variantAttrs.mpn ?? this.constructMPN()
: productAttrs.mpn ?? undefined;
}

renderBrand() {
const { attributes } = this.product;
const brandName = attributes?.find((attr) => attr.name === 'brand')?.value;
const { attributeMap: attrs } = this.product;
const brandName = attrs.brand;
if (!brandName) {
return undefined;
}
Expand All @@ -91,6 +94,32 @@ export class JSONTemplate {
};
}

/**
* @param {Variant} [variant]
*/
renderRating(variant) {
const { rating } = variant || this.product;
if (!rating) {
return undefined;
}

const {
count,
reviews,
value,
best,
worst,
} = rating;
return pruneUndefined({
'@type': 'AggregateRating',
ratingValue: value,
ratingCount: count,
reviewCount: reviews,
bestRating: best,
worstRating: worst,
});
}

renderOffers() {
const image = this.product.images?.[0]?.url
?? findProductImage(this.product, this.variants)?.url;
Expand All @@ -114,30 +143,26 @@ export class JSONTemplate {
availability: v.inStock ? 'InStock' : 'OutOfStock',
price: finalPrice,
priceCurrency: variantPrices.final?.currency,
gtin: v.gtin,
priceValidUntil: v.specialToDate,
aggregateRating: this.renderRating(v),
};

if (finalPrice < regularPrice) {
offer.priceSpecification = this.renderOffersPriceSpecification(v);
}

if (v.gtin) {
offer.gtin = v.gtin;
}

if (v.specialToDate) {
offer.priceValidUntil = v.specialToDate;
}

return pruneUndefined(offer);
}).filter(Boolean),
],
};
}

/**
* @param {Variant} variant
*/
renderOffersPriceSpecification(variant) {
const { prices } = variant;
const { regular } = prices;
const { amount, currency } = regular;
const { prices: { regular: { amount, currency } } } = variant;
return {
'@type': 'UnitPriceSpecification',
priceType: 'https://schema.org/ListPrice',
Expand All @@ -152,8 +177,6 @@ export class JSONTemplate {
name,
metaDescription,
images,
reviewCount,
ratingValue,
} = this.product;

const productUrl = this.constructProductURL();
Expand All @@ -172,17 +195,6 @@ export class JSONTemplate {
productID: sku,
...this.renderOffers(),
...(this.renderBrand() ?? {}),
...(typeof reviewCount === 'number'
&& typeof ratingValue === 'number'
&& reviewCount > 0
? {
aggregateRating: {
'@type': 'AggregateRating',
ratingValue,
reviewCount,
},
}
: {}),
}), undefined, 2);
}
}
40 changes: 32 additions & 8 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ declare global {

export interface AttributeOverrides {
variant: {
// expected attribute name => actual attribute name
[key: string]: string;
};
product: {
// expected attribute name => actual attribute name
[key: string]: string;
}
}

/**
Expand All @@ -35,14 +40,14 @@ declare global {
sku?: string;
matchedPatterns: string[];
imageRoles?: string[];

confMap: ConfigMap;
host: string;
params: Record<string, string>;
headers: Record<string, string>;
host: string;
offerVariantURLTemplate: string;
attributeOverrides: AttributeOverrides;
siteOverrides: Record<string, Record<string, unknown>>;
offerVariantURLTemplate?: string;
attributeOverrides?: AttributeOverrides;
siteOverrides?: Record<string, Record<string, unknown>>;

confMap: ConfigMap;
}

export interface Env {
Expand Down Expand Up @@ -90,13 +95,15 @@ declare global {
externalId?: string;
variants?: Variant[]; // variants exist on products in helix commerce but not on magento
specialToDate?: string;
rating?: Rating

// not handled currently:
externalParentId?: string;
variantSku?: string;
reviewCount?: number;
ratingValue?: number;
optionUIDs?: string[];

// internal use:
attributeMap: Record<string, string>;
}

export interface Variant {
Expand All @@ -112,6 +119,23 @@ declare global {
externalId: string;
specialToDate?: string;
gtin?: string;
rating?: Rating;

// internal use:
attributeMap: Record<string, string>;
}

interface Rating {
// number of ratings
count?: number;
// number of reviews
reviews?: number;
// rating value
value: number | string;
// range of ratings, highest
best?: number | string;
// range of ratings, lowest
worst?: number | string;
}

interface Image {
Expand Down
Loading

0 comments on commit 76858f4

Please sign in to comment.