Skip to content

Conversation

@oliverabrahams
Copy link
Contributor

@oliverabrahams oliverabrahams commented Oct 29, 2025

Co-authored-by: Charley_Campbell [email protected]

Co-authored-by: Emma Imber [email protected]

What does this change?

  • Adds ProductElement component and corresponding story
  • Updates schemas to add Product block element
  • Adds Product Element to renderElement code
  • Updates H2 enhancer to handle Product element headings
  • Adds Product element enhancer and uses it in `enhanceBlocks
  • Adds Product element types to content

Why?

We have added a new Product element in Composer, which needs to be handled in the rendering layer. The point of this element is to provide a way of structuring product data in Filter articles. This structured data will unlock more design opportunities on Filter pages. Some more details are here if you're interested.

image

Screenshots

Screenshots are taken using our test article in CAPI CODE

Wide breakpoint

Screenshot 2025-10-30 at 11 56 01

Mobile

Screenshot 2025-10-30 at 11 56 29 Screenshot 2025-10-30 at 11 56 45

… colours for the product cards. Add the Product Card image component. Add the correct sizing to the Picture component for the new Product Image.
@oliverabrahams oliverabrahams changed the base branch from main to mob/product-cards October 29, 2025 17:47
@github-actions
Copy link

github-actions bot commented Oct 30, 2025

# Conflicts:
#	dotcom-rendering/src/types/content.ts
@github-actions
Copy link

github-actions bot commented Oct 30, 2025

@oliverabrahams oliverabrahams marked this pull request as ready for review October 30, 2025 12:57
@github-actions
Copy link

Hello 👋! When you're ready to run Chromatic, please apply the run_chromatic label to this PR.

You will need to reapply the label each time you want to run Chromatic.

Click here to see the Chromatic project.

@oliverabrahams oliverabrahams requested a review from a team October 30, 2025 14:36
@oliverabrahams oliverabrahams marked this pull request as draft November 3, 2025 12:13
@oliverabrahams oliverabrahams marked this pull request as ready for review November 3, 2025 12:46
Base automatically changed from mob/product-cards to main November 3, 2025 16:20
Copy link
Contributor

@JamieB-gu JamieB-gu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Some comments and questions.

_type: 'model.dotcomrendering.pageElements.ProductBlockElement';
elementId: string;
brandName: string;
starRating: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be of type StarRating (defined in this file) or, if that doesn't work, number?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is type string because it can have a value of N/A, which is the undefined dropdown value. You can see the value set here

customAttributes: { name: string; value: string }[];
content: FEElement[];
h2Id?: string;
displayType: ProductDisplayType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small thing, but do we need the word "type" in this type?

for (const cta of ctas) {
const priceMatch = cta.price.match(/[\d,.]+/);
if (priceMatch) {
const priceNumber = parseFloat(priceMatch[0].replace(/,/g, ''));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when parseFloat returns NaN1?

For example, if the first CTA is parsed as NaN, then I believe lowestPrice will be set to NaN? From then on, all comparisons with NaN evaluate to false2, so the condition in the if will always evaluate to false, and the first CTA will be chosen as the lowest price, regardless of the values?

Footnotes

  1. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat#return_value

  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN#description

lowestPrice: getLowestPrice(element.productCtas),
});

const getLowestPrice = (ctas: ProductCta[]): string | undefined => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be useful to add a JSDoc comment to this function explaining how these regexes and replacements work?

product.displayType === 'ProductCardOnly' ||
product.displayType === 'InlineWithProductCard';
return (
<div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this div be a fragment?

Comment on lines +285 to +308
decorators: [
(Story) => (
<SectionComponent
shouldCenter={true}
showSideBorders={true}
centralBorder={'full'}
css={css`
strong {
font-weight: bold;
}
`}
>
<ArticleContainer
format={{
design: ArticleDesign.Review,
display: ArticleDisplay.Standard,
theme: Pillar.Lifestyle,
}}
>
<Story />
</ArticleContainer>
</SectionComponent>
),
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned in the ProductCard PR, we have a centreColumnDecorator which may be a bit simpler here?

Suggested change
decorators: [
(Story) => (
<SectionComponent
shouldCenter={true}
showSideBorders={true}
centralBorder={'full'}
css={css`
strong {
font-weight: bold;
}
`}
>
<ArticleContainer
format={{
design: ArticleDesign.Review,
display: ArticleDisplay.Standard,
theme: Pillar.Lifestyle,
}}
>
<Story />
</ArticleContainer>
</SectionComponent>
),
],
decorators: [centreColumnDecorator],

Comment on lines 22 to 32
const productImage: ProductImage = {
url: 'https://media.guimcode.co.uk/cb193848ed75d40103eceaf12b448de2330770dc/0_0_725_725/725.jpg',
caption: 'Filter-2 test image for live demo',
height: 1,
width: 1,
alt: 'Bosch Sky kettle',
credit: 'Photograph: Rachel Ogden/The Guardian',
displayCredit: false,
};

const product: ProductBlockElement = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider moving some of this data out into fixtures/manual? That may perhaps make the stories a bit easier to read, and it's possible some of it could be reused, as it looks like there's similar data used in other stories files and in the tests?

shouldShowLeftColCard;
const subheadingHtml = parseHtml(
`<h2 id="${product.h2Id ?? product.elementId}">${
product.primaryHeading ? `${product.primaryHeading}</br>` : ''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A minor detail, but <br> is a self-closing tag so should be <br /> rather than </br>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants