Skip to content

Commit

Permalink
Merge branch 'main' of github.com:hlxsites/aem-boilerplate-commerce i…
Browse files Browse the repository at this point in the history
…nto develop
  • Loading branch information
herzog31 committed Oct 2, 2024
2 parents 0c7289c + 08080d9 commit f5cd140
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 52 deletions.
38 changes: 29 additions & 9 deletions blocks/product-details-custom/product-details-custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
performCatalogServiceQuery,
refineProductQuery,
setJsonLd,
loadErrorPage,
loadErrorPage, variantsQuery,
} from '../../scripts/commerce.js';
import { readBlockConfig } from '../../scripts/aem.js';

Expand All @@ -40,18 +40,17 @@ async function setJsonLdProduct(product) {
const amount = priceRange?.minimum?.final?.amount || price?.final?.amount;
const brand = attributes.find((attr) => attr.name === 'brand');

setJsonLd({
// get variants
const { variants } = (await performCatalogServiceQuery(variantsQuery, { sku }))?.variants
|| { variants: [] };

const ldJson = {
'@context': 'http://schema.org',
'@type': 'Product',
name,
description,
image: images[0]?.url,
offers: [{
'@type': 'http://schema.org/Offer',
price: amount?.value,
priceCurrency: amount?.currency,
availability: inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
}],
offers: [],
productID: sku,
brand: {
'@type': 'Brand',
Expand All @@ -60,7 +59,28 @@ async function setJsonLdProduct(product) {
url: new URL(`/products/${urlKey}/${sku}`, window.location),
sku,
'@id': new URL(`/products/${urlKey}/${sku}`, window.location),
}, 'product');
};

if (variants.length > 1) {
ldJson.offers.push(...variants.map((variant) => ({
'@type': 'Offer',
name: variant.product.name,
image: variant.product.images[0]?.url,
price: variant.product.price.final.amount.value,
priceCurrency: variant.product.price.final.amount.currency,
availability: variant.product.inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
sku: variant.product.sku,
})));
} else {
ldJson.offers.push({
'@type': 'Offer',
price: amount?.value,
priceCurrency: amount?.currency,
availability: inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
});
}

setJsonLd(ldJson, 'product');
}

class ProductDetailPage extends Component {
Expand Down
73 changes: 43 additions & 30 deletions blocks/product-details/product-details.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
getProduct,
getSkuFromUrl,
setJsonLd,
loadErrorPage,
loadErrorPage, performCatalogServiceQuery, variantsQuery,
} from '../../scripts/commerce.js';
import { getConfigValue } from '../../scripts/configs.js';
import { fetchPlaceholders } from '../../scripts/aem.js';
Expand All @@ -37,34 +37,47 @@ async function setJsonLdProduct(product) {
const amount = priceRange?.minimum?.final?.amount || price?.final?.amount;
const brand = attributes.find((attr) => attr.name === 'brand');

setJsonLd(
{
'@context': 'http://schema.org',
'@type': 'Product',
name,
description,
image: images[0]?.url,
offers: [
{
'@type': 'http://schema.org/Offer',
price: amount?.value,
priceCurrency: amount?.currency,
availability: inStock
? 'http://schema.org/InStock'
: 'http://schema.org/OutOfStock',
},
],
productID: sku,
brand: {
'@type': 'Brand',
name: brand?.value,
},
url: new URL(`/products/${urlKey}/${sku}`, window.location),
sku,
'@id': new URL(`/products/${urlKey}/${sku}`, window.location),
// get variants
const { variants } = (await performCatalogServiceQuery(variantsQuery, { sku }))?.variants
|| { variants: [] };

const ldJson = {
'@context': 'http://schema.org',
'@type': 'Product',
name,
description,
image: images[0]?.url,
offers: [],
productID: sku,
brand: {
'@type': 'Brand',
name: brand?.value,
},
'product',
);
url: new URL(`/products/${urlKey}/${sku}`, window.location),
sku,
'@id': new URL(`/products/${urlKey}/${sku}`, window.location),
};

if (variants.length > 1) {
ldJson.offers.push(...variants.map((variant) => ({
'@type': 'Offer',
name: variant.product.name,
image: variant.product.images[0]?.url,
price: variant.product.price.final.amount.value,
priceCurrency: variant.product.price.final.amount.currency,
availability: variant.product.inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
sku: variant.product.sku,
})));
} else {
ldJson.offers.push({
'@type': 'Offer',
price: amount?.value,
priceCurrency: amount?.currency,
availability: inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
});
}

setJsonLd(ldJson, 'product');
}

function createMetaTag(property, content, type) {
Expand Down Expand Up @@ -99,13 +112,13 @@ function setMetaTags(product) {
? product.priceRange.minimum.final.amount
: product.price.final.amount;

createMetaTag('title', product.metaTitle, 'name');
createMetaTag('title', product.metaTitle || product.name, 'name');
createMetaTag('description', product.metaDescription, 'name');
createMetaTag('keywords', product.metaKeyword, 'name');

createMetaTag('og:type', 'og:product', 'property');
createMetaTag('og:description', product.shortDescription, 'property');
createMetaTag('og:title', product.metaTitle, 'property');
createMetaTag('og:title', product.metaTitle || product.name, 'property');
createMetaTag('og:url', window.location.href, 'property');
const mainImage = product?.images?.filter((image) => image.roles.includes('thumbnail'))[0];
const metaImage = mainImage?.url || product?.images[0]?.url;
Expand Down
22 changes: 22 additions & 0 deletions scripts/commerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,28 @@ export const productDetailQuery = `query ProductQuery($sku: String!) {
}
${priceFieldsFragment}`;

export const variantsQuery = `
query($sku: String!) {
variants(sku: $sku) {
variants {
product {
sku
name
inStock
images(roles: ["image"]) {
url
}
...on SimpleProductView {
price {
final { amount { currency value } }
}
}
}
}
}
}
`;

/* Common functionality */

export async function performCatalogServiceQuery(query, variables) {
Expand Down
5 changes: 3 additions & 2 deletions tools/pdp-metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## Overview
It is recommended to import product metadata into Edge Delivery so that it can be rendered server-side on product detail pages.
This is important so social media sites which do not parse JavaScript can pick it up.
This is important so Google Merchant Center can reliably verify entries from your product sheet.
Also social media sites which usually do not parse JavaScript can leverage this metadata to display rich previews of your product page links.

This project is designed to fetch product data from a catalog service, process it, and generate a metadata spreadsheet in XLSX format which can be used for the https://www.aem.live/docs/bulk-metadata feature in Edge Delivery.
This project is designed to fetch product data from catalog service, process it, and generate a metadata spreadsheet in XLSX format which can be used for the https://www.aem.live/docs/bulk-metadata feature in Edge Delivery.

## Prerequisites
- Node.js installed on your machine.
Expand Down
94 changes: 88 additions & 6 deletions tools/pdp-metadata/pdp-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import XLSX from 'xlsx';
import fs from 'fs';
import he from 'he';
import productSearchQuery from './queries/products.graphql.js';
import { variantsFragment } from './queries/variants.graphql.js';

const basePath = 'https://main--aem-boilerplate-commerce--hlxsites.hlx.live';
const configFile = `${basePath}/configs.json?sheet=prod`;
Expand All @@ -19,13 +20,14 @@ async function performCatalogServiceQuery(config, query, variables) {
};

const apiCall = new URL(config['commerce-endpoint']);
apiCall.searchParams.append('query', query.replace(/(?:\r\n|\r|\n|\t|[\s]{4})/g, ' ')
.replace(/\s\s+/g, ' '));
apiCall.searchParams.append('variables', variables ? JSON.stringify(variables) : null);

const response = await fetch(apiCall, {
method: 'GET',
method: 'POST',
headers,
body: JSON.stringify({
query: query.replace(/(?:\r\n|\r|\n|\t|[\s]{4})/g, ' ').replace(/\s\s+/g, ' '),
variables,
}),
});

if (!response.ok) {
Expand All @@ -37,6 +39,58 @@ async function performCatalogServiceQuery(config, query, variables) {
return queryResponse.data;
}

function getJsonLd(product, { variants }) {
const amount = product.priceRange?.minimum?.final?.amount || product.price?.final?.amount;
const brand = product.attributes.find((attr) => attr.name === 'brand');

const schema = {
'@context': 'http://schema.org',
'@type': 'Product',
name: product.name,
description: product.meta_description,
image: product['og:image'],
offers: [],
productID: product.sku,
sku: product.sku,
url: product.path,
'@id': product.path,
};

if (brand?.value) {
product.brand = {
'@type': 'Brand',
name: brand?.value,
};
}

if (variants.length <= 1) {
// simple products
if (amount?.value && amount?.currency) {
schema.offers.push({
'@type': 'Offer',
price: amount?.value,
priceCurrency: amount?.currency,
availability: product.inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
});
}
} else {
// complex products
variants.forEach((variant) => {
schema.offers.push({
'@type': 'Offer',
name: variant.product.name,
image: variant.product.images[0]?.url,
price: variant.product.price.final.amount.value,
priceCurrency: variant.product.price.final.amount.currency,
availability: variant.product.inStock ? 'http://schema.org/InStock' : 'http://schema.org/OutOfStock',
sku: variant.product.sku
});
})
}

return JSON.stringify(schema);
}

/**
* Get products by page number
* @param {INT} pageNumber - pass the pagenumber to retrieved paginated results
Expand All @@ -60,7 +114,7 @@ const getProducts = async (config, pageNumber) => {
description,
shortDescription,
} = item.productView;
const { url: imageUrl } = item.product.image ?? {};
const { url: imageUrl } = item.productView.images?.[0] ?? { url: '' };

let baseImageUrl = imageUrl;
if (baseImageUrl.startsWith('//')) {
Expand Down Expand Up @@ -96,6 +150,9 @@ const getProducts = async (config, pageNumber) => {
const totalPages = response.productSearch.page_info.total_pages;
const currentPage = response.productSearch.page_info.current_page;
console.log(`Retrieved page ${currentPage} of ${totalPages} pages`);

await addVariantsToProducts(products, config);

if (currentPage !== totalPages) {
return [...products, ...(await getProducts(config, currentPage + 1))];
}
Expand All @@ -104,6 +161,29 @@ const getProducts = async (config, pageNumber) => {
return [];
};

async function addVariantsToProducts(products, config) {
const query = `
query Q {
${products.map((product, i) => {
return `
item_${i}: variants(sku: "${product.productView.sku}") {
...ProductVariant
}
`
}).join('\n')}
}${variantsFragment}`;

const response = await performCatalogServiceQuery(config, query, null);

if (!response) {
throw new Error('Could not fetch variants');
}

products.forEach((product, i) => {
product.variants = response[`item_${i}`];
});
}

(async () => {
const config = {};
try {
Expand All @@ -130,9 +210,10 @@ const getProducts = async (config, pageNumber) => {
'og:url',
'og:image',
'og:image:secure_url',
'json-ld',
],
];
products.forEach(({ productView: metaData }) => {
products.forEach(({ productView: metaData, variants }) => {
data.push(
[
metaData.path, // URL
Expand All @@ -145,6 +226,7 @@ const getProducts = async (config, pageNumber) => {
`${basePath}${metaData.path}`, // og:url
metaData['og:image'], // og:image
metaData['og:image:secure_url'], // og:image:secure_url
getJsonLd(metaData, variants), // json-ld
],
);
});
Expand Down
Loading

0 comments on commit f5cd140

Please sign in to comment.