fix: classify Amazon sponsored brand and video search ads

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
syrup 2026-09-04 09:35:25 +08:00
parent 72b0c348a9
commit 7d446d8f54
3 changed files with 904 additions and 21 deletions

View File

@ -216,6 +216,721 @@ async function main() {
assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay'); assert.equal(groupedAdsByAsin.get('B000000014')?.adType, 'sponsoredDisplay');
assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar'); assert.equal(groupedAdsByAsin.get('B000000014')?.placement, 'sidebar');
const liveSponsoredBrandsDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
<div class="sb-headline">Arlo Security for any Home</div>
<span>Sponsored</span>
<div data-asin="B08AR2S6MC">
<span>Video Doorbell</span>
<a href="/sspa/click?ie=UTF8&url=%2Fdp%2FB08AR2S6MC">
<img data-src="https://example.com/arlo-1.jpg" src="https://example.com/arlo-1.jpg" />
</a>
</div>
<div data-asin="B0ARLO0002">
<span>Outdoor Camera</span>
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0002">
<img src="https://example.com/arlo-2.jpg" />
</a>
</div>
<div data-asin="B0ARLO0003">
<span>Bundle</span>
<a href="/sspa/click?url=%2Fdp%2FB0ARLO0003">
<img src="https://example.com/arlo-3.jpg" />
</a>
</div>
</div>
<article data-component-type="s-search-result" data-asin="B09RING001">
<span>Sponsored</span>
<h2>Ring Spotlight Cam Plus, Battery</h2>
<a href="/sspa/click?url=%2Fdp%2FB09RING001"><img class="s-image" src="https://example.com/ring.jpg" /></a>
</article>
<article data-component-type="s-search-result" data-asin="B09ORG0001">
<h2>Organic camera</h2>
<a href="/dp/B09ORG0001"><img class="s-image" src="https://example.com/org.jpg" /></a>
</article>
<div class="sbv-container AdHolder">
<span>Sponsored</span>
<video></video>
<div class="sbv-copy">PROTECT YOUR EQUIPMENT &amp; FACILITIES</div>
<div data-asin="B0VOSKER01" class="sbv-product">
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER01"><img src="https://example.com/vosker.jpg" /></a>
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
</div>
</div>
<section class="brands-related-to-your-search AdHolder">
<h2>Brands related to your search</h2>
<span>Sponsored</span>
<div class="brand-card" data-asin="B0BRAND001">
<img src="https://example.com/vosker-brand.jpg" alt="VOSKER" />
<a href="/sspa/click?url=%2Fdp%2FB0BRAND001">VOSKER</a>
</div>
<div class="brand-card">
<img src="https://example.com/reolink-brand.jpg" alt="Reolink" />
<a href="/sspa/click?url=%2Fgp%2Fproduct%2FB0BRAND002">Reolink</a>
</div>
</section>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: liveSponsoredBrandsDom.window.document,
Node: liveSponsoredBrandsDom.window.Node,
Element: liveSponsoredBrandsDom.window.Element,
HTMLElement: liveSponsoredBrandsDom.window.HTMLElement,
HTMLAnchorElement: liveSponsoredBrandsDom.window.HTMLAnchorElement,
HTMLImageElement: liveSponsoredBrandsDom.window.HTMLImageElement,
});
const liveSponsoredBrands = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const liveByAsin = new Map(liveSponsoredBrands.map((item) => [item.asin, item]));
assert.equal(liveByAsin.get('B08AR2S6MC')?.adType, 'sponsoredBrands');
assert.equal(liveByAsin.get('B0ARLO0002')?.adType, 'sponsoredBrands');
assert.equal(liveByAsin.get('B0ARLO0003')?.adType, 'sponsoredBrands');
assert.equal(liveByAsin.get('B08AR2S6MC')?.placement, 'topOfSearch');
assert.equal(liveByAsin.get('B09RING001')?.adType, 'sponsoredProducts');
assert.equal(liveByAsin.get('B09ORG0001')?.adType, 'organic');
assert.equal(liveByAsin.get('B0VOSKER01')?.adType, 'sponsoredBrands');
assert.equal(liveByAsin.get('B0VOSKER01')?.adFormat, 'video');
assert.equal(liveByAsin.get('B0VOSKER01')?.placement, 'bottomOfSearch');
assert.equal(liveByAsin.get('B0BRAND001')?.adType, 'sponsoredBrands');
assert.equal(liveByAsin.get('B0BRAND002')?.adType, 'sponsoredBrands');
const lazyImageDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<div class="s-widget AdHolder">
<div class="sb-headline">Arlo Security for any Home</div>
<span>Sponsored</span>
<div data-asin="B08AR2LAZY">
<h3>Video Doorbell</h3>
<a href="/sspa/click?url=%2Fdp%2FB08AR2LAZY">
<img data-src="https://example.com/arlo-lazy.jpg" />
</a>
</div>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: lazyImageDom.window.document,
Node: lazyImageDom.window.Node,
Element: lazyImageDom.window.Element,
HTMLElement: lazyImageDom.window.HTMLElement,
HTMLAnchorElement: lazyImageDom.window.HTMLAnchorElement,
HTMLImageElement: lazyImageDom.window.HTMLImageElement,
});
const lazyImageItems = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
assert.equal(lazyImageItems.length, 1);
assert.equal(lazyImageItems[0]?.asin, 'B08AR2LAZY');
assert.equal(lazyImageItems[0]?.adType, 'sponsoredBrands');
assert.equal(lazyImageItems[0]?.imageSrc, 'https://example.com/arlo-lazy.jpg');
const classificationBoundaryDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
<div class="sb-headline">Arlo Security for any Home</div>
<span>Sponsored</span>
<div data-asin="B08ARLOSB1">
<span>Video Doorbell</span>
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB1">
<img src="https://example.com/arlo-banner-1.jpg" />
</a>
</div>
<div data-asin="B08ARLOSB2">
<span>Outdoor Camera</span>
<a href="/sspa/click?url=%2Fdp%2FB08ARLOSB2">
<img src="https://example.com/arlo-banner-2.jpg" />
</a>
</div>
</div>
<div class="s-widget AdHolder" data-cel-widget="search-grid-wrap">
<article data-component-type="s-search-result" data-asin="B0ARLOGRID" class="AdHolder">
<span>Sponsored</span>
<h2>Arlo Video Doorbell Wired</h2>
<a href="/sspa/click?url=%2Fdp%2FB0ARLOGRID">
<img class="s-image" src="https://example.com/arlo-grid.jpg" />
</a>
</article>
<article data-component-type="s-search-result" data-asin="B09RINGSP1" class="AdHolder">
<span>Sponsored</span>
<h2>Ring Spotlight Cam Plus, Battery</h2>
<a href="/sspa/click?url=%2Fdp%2FB09RINGSP1">
<img class="s-image" src="https://example.com/ring-grid.jpg" />
</a>
</article>
<article data-component-type="s-search-result" data-asin="B09ORGKEEP">
<h2>Organic camera</h2>
<a href="/dp/B09ORGKEEP"><img class="s-image" src="https://example.com/org-keep.jpg" /></a>
</article>
</div>
<article data-component-type="s-search-result" data-asin="B0TRACKSP1" class="AdHolder">
<span>Sponsored</span>
<h2>Arlo Go 2 LTE Security Camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0TRACKSP1">
<img class="s-image" src="https://example.com/arlo-track.jpg" />
</a>
<a href="/gp/product/B0TRACKXX2">related tracking</a>
</article>
<article data-component-type="s-search-result" data-asin="B0DEMOSP01" class="AdHolder">
<span>Sponsored</span>
<h2>Ring Indoor Cam with demo</h2>
<video></video>
<a href="/dp/B0DEMOSP01">
<img class="s-image" src="https://example.com/ring-demo.jpg" />
</a>
</article>
<div class="sbv-container AdHolder">
<span>Sponsored</span>
<video></video>
<div class="sbv-copy">PROTECT YOUR EQUIPMENT &amp; FACILITIES</div>
<div data-asin="B0VOSKER02" class="sbv-product">
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VOSKER02">
<img src="https://example.com/vosker-2.jpg" />
</a>
</div>
</div>
<article data-component-type="s-search-result" data-asin="B0NOADVID1">
<h2>Organic with product video</h2>
<video></video>
<a href="/dp/B0NOADVID1">
<img class="s-image" src="https://example.com/organic-video.jpg" />
</a>
</article>
<section class="brands-related-to-your-search AdHolder">
<h2>Brands related to your search</h2>
<span>Sponsored</span>
<div class="brand-card" data-asin="B0BRANDWL1">
<img src="https://example.com/vosker-wall.jpg" alt="VOSKER" />
<a href="/sspa/click?url=%2Fdp%2FB0BRANDWL1">VOSKER</a>
</div>
</section>
</div>
<aside id="rightResultsATF" data-component-type="sponsored-display">
<span>Sponsored</span>
<article data-asin="B0SIDEBAR1">
<h3>Sidebar display product</h3>
<a href="/dp/B0SIDEBAR1"><img src="https://example.com/sidebar.jpg" /></a>
</article>
</aside>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: classificationBoundaryDom.window.document,
Node: classificationBoundaryDom.window.Node,
Element: classificationBoundaryDom.window.Element,
HTMLElement: classificationBoundaryDom.window.HTMLElement,
HTMLAnchorElement: classificationBoundaryDom.window.HTMLAnchorElement,
HTMLImageElement: classificationBoundaryDom.window.HTMLImageElement,
});
const classificationBoundary = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const boundaryByAsin = new Map(classificationBoundary.map((item) => [item.asin, item]));
assert.equal(boundaryByAsin.get('B08ARLOSB1')?.adType, 'sponsoredBrands');
assert.equal(boundaryByAsin.get('B08ARLOSB2')?.adType, 'sponsoredBrands');
assert.equal(boundaryByAsin.get('B0ARLOGRID')?.adType, 'sponsoredProducts');
assert.equal(boundaryByAsin.get('B09RINGSP1')?.adType, 'sponsoredProducts');
assert.equal(boundaryByAsin.get('B09ORGKEEP')?.adType, 'organic');
assert.equal(boundaryByAsin.get('B0TRACKSP1')?.adType, 'sponsoredProducts');
assert.equal(boundaryByAsin.has('B0TRACKXX2'), false);
assert.equal(boundaryByAsin.get('B0DEMOSP01')?.adType, 'sponsoredProducts');
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adType, 'sponsoredBrands');
assert.equal(boundaryByAsin.get('B0VOSKER02')?.adFormat, 'video');
assert.equal(boundaryByAsin.get('B0NOADVID1')?.adType, 'organic');
assert.equal(boundaryByAsin.get('B0BRANDWL1')?.adType, 'sponsoredBrands');
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.adType, 'sponsoredDisplay');
assert.equal(boundaryByAsin.get('B0SIDEBAR1')?.placement, 'sidebar');
const thematicLoomDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B0ORGLOOM1">
<h2>Organic camera</h2>
<a href="/dp/B0ORGLOOM1"><img class="s-image" src="https://example.com/org-loom.jpg" /></a>
</article>
<div
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-trending-hardlines-bottom-na-page2plus-t1"
cel_widget_id="MAIN-FEATURED_ASINS_LIST-22"
>
<div class="a-section s-include-content-margin">
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Trending now</h2>
<span
data-action="multi-ad-feedback-form-trigger"
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-trending-hardlines","feedbackType":"sponsoredProductsLoom"}}'
>
<a aria-label="View Sponsored information or leave ad feedback" class="s-widget-sponsored-label-text">Sponsored</a>
</span>
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
<ol class="a-carousel">
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0B2MPYRKP" class="s-result-item">
<div class="puis-card-container">
<h2>iulock Keypad Smart Door Lock</h2>
<a href="/sspa/click?url=%2Fdp%2FB0B2MPYRKP&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/iulock.jpg" />
</a>
</div>
</div>
</li>
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0F8HS3RRT" class="s-result-item">
<div class="puis-card-container">
<h2>Richelock fingerprint lock</h2>
<a href="/sspa/click?url=%2Fdp%2FB0F8HS3RRT&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/richelock.jpg" />
</a>
</div>
</div>
</li>
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0GQX6GHK2" class="s-result-item">
<div class="puis-card-container">
<h2>Swann MaxRanger4K</h2>
<a href="/sspa/click?url=%2Fdp%2FB0GQX6GHK2&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/swann.jpg" />
</a>
</div>
</div>
</li>
<li class="a-carousel-card" aria-hidden="false" style="visibility: hidden;">
<div data-asin="B0CN95HR9V" class="s-result-item">
<div class="puis-card-container">
<h2>Solar Fence Lights</h2>
<a href="/sspa/click?url=%2Fdp%2FB0CN95HR9V&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/solar.jpg" />
</a>
</div>
</div>
</li>
<li class="a-carousel-card" aria-hidden="true" style="visibility: hidden;">
<div data-asin="B07M68S376" class="s-result-item">
<div class="puis-card-container">
<h2>SECRUI Wireless Doorbell</h2>
<a href="/sspa/click?url=%2Fdp%2FB07M68S376&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/secrui.jpg" />
</a>
</div>
</div>
</li>
</ol>
</span>
</div>
</div>
<div class="sbv-container AdHolder">
<span>Sponsored</span>
<video></video>
<div class="sbv-copy">PROTECT YOUR EQUIPMENT &amp; FACILITIES</div>
<div data-asin="B0VOSKERLM" class="sbv-product">
<h2>Vosker VKX Solar Powered camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERLM"><img src="https://example.com/vosker-loom.jpg" /></a>
</div>
</div>
<div class="s-widget AdHolder" data-cel-widget="CardInstanceArlo">
<div class="sb-headline">Arlo Security for any Home</div>
<span>Sponsored</span>
<div data-asin="B08ARLOLM1">
<span>Video Doorbell</span>
<a href="/sspa/click?url=%2Fdp%2FB08ARLOLM1">
<img src="https://example.com/arlo-loom.jpg" />
</a>
</div>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: thematicLoomDom.window.document,
Node: thematicLoomDom.window.Node,
Element: thematicLoomDom.window.Element,
HTMLElement: thematicLoomDom.window.HTMLElement,
HTMLAnchorElement: thematicLoomDom.window.HTMLAnchorElement,
HTMLImageElement: thematicLoomDom.window.HTMLImageElement,
});
const thematicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const thematicByAsin = new Map(thematicLoom.map((item) => [item.asin, item]));
assert.equal(thematicByAsin.get('B0ORGLOOM1')?.adType, 'organic');
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adType, 'sponsoredProducts');
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adType, 'sponsoredProducts');
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adType, 'sponsoredProducts');
assert.equal(
thematicByAsin.get('B0B2MPYRKP')?.placementId,
thematicByAsin.get('B0F8HS3RRT')?.placementId,
);
assert.equal(thematicByAsin.get('B0B2MPYRKP')?.adProductIndex, 1);
assert.equal(thematicByAsin.get('B0F8HS3RRT')?.adProductIndex, 2);
assert.equal(thematicByAsin.get('B0GQX6GHK2')?.adProductIndex, 3);
assert.equal(thematicByAsin.has('B0CN95HR9V'), false);
assert.equal(thematicByAsin.has('B07M68S376'), false);
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adType, 'sponsoredBrands');
assert.equal(thematicByAsin.get('B0VOSKERLM')?.adFormat, 'video');
assert.equal(thematicByAsin.get('B08ARLOLM1')?.adType, 'sponsoredBrands');
const organicLoomDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B0ORGCFV01">
<h2>Organic camera</h2>
<a href="/dp/B0ORGCFV01"><img class="s-image" src="https://example.com/org-cfv.jpg" /></a>
</article>
<div
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-frequently-viewed-bottom-na"
cel_widget_id="MAIN-FEATURED_ASINS_LIST-23"
>
<div class="a-section s-include-content-margin">
<h2 id="loom-desktop-bottom-slot_featuredasins-heading">Customers frequently viewed</h2>
<span
data-action="multi-ad-feedback-form-trigger"
data-multi-ad-feedback-form-trigger='{"adPlacementMetaData":{"slotName":"sp_search_thematic_btf-frequently-viewed","feedbackType":"sponsoredProductsLoom"}}'
></span>
<span
aria-label="Sponsored"
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
>Sponsored</span>
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
<ol class="a-carousel">
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0CFV00001" class="s-result-item">
<div class="puis-card-container">
<h2>ANNKE security camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0CFV00001&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/annke.jpg" />
</a>
</div>
</div>
</li>
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0CFV00002" class="s-result-item">
<div class="puis-card-container">
<h2>Night Owl camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0CFV00002&amp;widgetName=sp_search_thematic_btf">
<img class="s-image" src="https://example.com/nightowl.jpg" />
</a>
</div>
</div>
</li>
</ol>
</span>
</div>
</div>
<div
class="s-widget-container celwidget slot=MAIN template=FEATURED_ASINS_LIST widget=loom-desktop-bottom-slot_sptw-recently-bought-bottom-na"
cel_widget_id="MAIN-FEATURED_ASINS_LIST-24"
>
<h2>Recently bought and rated</h2>
<span data-component-type="s-searchgrid-carousel" class="s-searchgrid-carousel">
<ol class="a-carousel">
<li class="a-carousel-card" aria-hidden="false">
<div data-asin="B0RBR00001" class="s-result-item">
<div class="puis-card-container">
<h2>Rated doorbell</h2>
<a href="/dp/B0RBR00001">
<img class="s-image" src="https://example.com/rated.jpg" />
</a>
</div>
</div>
</li>
</ol>
</span>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: organicLoomDom.window.document,
Node: organicLoomDom.window.Node,
Element: organicLoomDom.window.Element,
HTMLElement: organicLoomDom.window.HTMLElement,
HTMLAnchorElement: organicLoomDom.window.HTMLAnchorElement,
HTMLImageElement: organicLoomDom.window.HTMLImageElement,
});
const organicLoom = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const organicLoomByAsin = new Map(organicLoom.map((item) => [item.asin, item]));
assert.equal(organicLoomByAsin.get('B0ORGCFV01')?.adType, 'organic');
assert.equal(organicLoomByAsin.get('B0CFV00001')?.adType, 'organic');
assert.equal(organicLoomByAsin.get('B0CFV00002')?.adType, 'organic');
assert.equal(organicLoomByAsin.get('B0RBR00001')?.adType, 'organic');
assert.equal(
organicLoom.some((item) => item.adType === 'sponsoredProducts'),
false,
);
const organicVideoStripDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B0ORGVID01">
<h2>Organic camera</h2>
<a href="/dp/B0ORGVID01"><img class="s-image" src="https://example.com/org-vid.jpg" /></a>
</article>
<div class="sbv-container AdHolder">
<video></video>
<a href="/stores/chamberlain"><span class="brand-logo">Chamberlain</span></a>
<div class="sb-headline">Chamberlain myQ</div>
<div class="sbv-copy">Control your garage from anywhere</div>
<div data-asin="B0NOSBV001" class="sbv-product">
<h2>Chamberlain garage opener</h2>
<a href="/dp/B0NOSBV001"><img src="https://example.com/chamberlain-nosponsored.jpg" /></a>
</div>
</div>
<div class="sbv-container AdHolder">
<span
aria-label="Sponsored"
style="position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0, 0, 0, 0)"
>Sponsored</span>
<video></video>
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
<div data-asin="B0HIDDENSB" class="sbv-product">
<h2>Vosker solar camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0HIDDENSB"><img src="https://example.com/vosker-hidden.jpg" /></a>
</div>
</div>
<div class="sbv-container AdHolder">
<span>Sponsored</span>
<video></video>
<a href="/stores/vosker"><span class="brand-logo">Vosker</span></a>
<div class="sbv-copy">PROTECT YOUR EQUIPMENT</div>
<div data-asin="B0YESBV001" class="sbv-product">
<h2>Vosker with visible sponsored</h2>
<a href="/sspa/click?url=%2Fdp%2FB0YESBV001"><img src="https://example.com/vosker-yes.jpg" /></a>
</div>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: organicVideoStripDom.window.document,
Node: organicVideoStripDom.window.Node,
Element: organicVideoStripDom.window.Element,
HTMLElement: organicVideoStripDom.window.HTMLElement,
HTMLAnchorElement: organicVideoStripDom.window.HTMLAnchorElement,
HTMLImageElement: organicVideoStripDom.window.HTMLImageElement,
});
const organicVideoStrip = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const videoStripByAsin = new Map(organicVideoStrip.map((item) => [item.asin, item]));
assert.equal(videoStripByAsin.get('B0ORGVID01')?.adType, 'organic');
assert.equal(videoStripByAsin.get('B0NOSBV001')?.adType, 'organic');
assert.equal(videoStripByAsin.get('B0HIDDENSB')?.adType, 'organic');
assert.equal(videoStripByAsin.get('B0YESBV001')?.adType, 'sponsoredBrands');
assert.equal(videoStripByAsin.get('B0YESBV001')?.adFormat, 'video');
assert.deepEqual(
organicVideoStrip.filter((item) => item.adType === 'sponsoredBrands').map((item) => item.asin),
['B0YESBV001'],
);
const liveBrandWallDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B0ORGGRID1">
<h2>Organic camera</h2>
<a href="/dp/B0ORGGRID1"><img class="s-image" src="https://example.com/org-grid.jpg" /></a>
</article>
<div
class="s-result-item s-widget AdHolder s-flex-full-width"
data-asin=""
cel_widget_id="multi-brand-creative-desktop_loom-desktop-brand-footer-slot_46"
>
<span aria-label="Brands related to your search" role="heading">Brands related to your search</span>
<span aria-label="Leave feedback on Sponsored ad" tabindex="0" role="button">Sponsored</span>
<div class="sbx-desktop" data-aid="301209426575402" data-slot="desktop-hsa-3psl">
<a href="/stores/page/VOSKER?lp_asins=B0D5Z3V464%2CB0FY2QCXRJ%2CB0FY2MRK56&amp;lp_query=home%20security&amp;lp_slot=desktop-hsa-3psl">
<img
alt="Vosker"
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
data-src="https://example.com/vosker-wall.jpg"
/>
</a>
</div>
<div class="sbx-desktop" data-aid="301252396855702" data-slot="desktop-hsa-3psl">
<a href="/stores/page/REOLINK?lp_asins=B0C2PY2KTJ%2CB0FM45MSS8&amp;lp_slot=desktop-hsa-3psl">
<img
alt="REOLINK"
src="https://images-na.ssl-images-amazon.com/images/G/01/x-locale/common/grey-pixel.gif"
data-src="https://example.com/reolink-wall.jpg"
/>
</a>
</div>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: liveBrandWallDom.window.document,
Node: liveBrandWallDom.window.Node,
Element: liveBrandWallDom.window.Element,
HTMLElement: liveBrandWallDom.window.HTMLElement,
HTMLAnchorElement: liveBrandWallDom.window.HTMLAnchorElement,
HTMLImageElement: liveBrandWallDom.window.HTMLImageElement,
});
const liveBrandWall = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const liveBrandWallByAsin = new Map(liveBrandWall.map((item) => [item.asin, item]));
assert.equal(liveBrandWallByAsin.get('B0ORGGRID1')?.adType, 'organic');
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.adType, 'sponsoredBrands');
assert.equal(liveBrandWallByAsin.get('B0C2PY2KTJ')?.adType, 'sponsoredBrands');
assert.equal(liveBrandWallByAsin.get('B0D5Z3V464')?.title, 'Vosker');
assert.equal(
liveBrandWallByAsin.get('B0D5Z3V464')?.imageSrc,
'https://example.com/vosker-wall.jpg',
);
assert.equal(
liveBrandWallByAsin.get('B0C2PY2KTJ')?.imageSrc,
'https://example.com/reolink-wall.jpg',
);
assert.equal(liveBrandWallByAsin.has('B0FY2QCXRJ'), false);
const influencerPicksDom = new JSDOM(
`
<main id="search">
<div class="s-main-slot">
<article data-component-type="s-search-result" data-asin="B0ORGINF01">
<h2>Organic camera</h2>
<a href="/dp/B0ORGINF01"><img class="s-image" src="https://example.com/org-inf.jpg" /></a>
</article>
<section class="s-widget-container celwidget">
<h2>Picks from Amazon Influencers</h2>
<span>Sponsored</span>
<span>From Amazon Influencer storefronts</span>
<div data-asin="B0ANNKE001" class="puis-card-container">
<h2>ANNKE 4K PoE Security Camera System</h2>
<a href="/sspa/click?url=%2Fdp%2FB0ANNKE001"><img src="https://example.com/annke.jpg" /></a>
</div>
<div data-asin="B0NIGHTO01" class="puis-card-container">
<h2>Night Owl DVR</h2>
<a href="/shop/nightowlstorefront"><span>storefront</span></a>
<a href="/sspa/click?url=%2Fdp%2FB0NIGHTO01"><img src="https://example.com/nightowl.jpg" /></a>
</div>
<div data-asin="B0SOLAR001" class="puis-card-container">
<h2>Solar Street Lights</h2>
<a href="/sspa/click?url=%2Fdp%2FB0SOLAR001"><img src="https://example.com/solar.jpg" /></a>
</div>
</section>
<div class="s-widget AdHolder">
<div class="creative-collage">
<img src="https://example.com/jobsite.jpg" alt="jobsite" />
<img src="https://example.com/camera-closeup.jpg" alt="camera" />
<button aria-label="Pause"></button>
<img src="https://example.com/packaging.jpg" alt="packaging" />
</div>
<div data-asin="B0VOSKERVK">
<span>Sponsored</span>
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERVK">
<img src="https://example.com/vosker-vkx.jpg" />
</a>
<span class="a-price"><span class="a-offscreen">$379.99</span></span>
</div>
</div>
<div class="sbv-container AdHolder">
<div class="sbv-creative">
<img src="https://example.com/jobsite2.jpg" alt="PROTECT YOUR INVESTMENT" />
<img src="https://example.com/closeup2.jpg" alt="CELLULAR SECURITY CAMERA" />
<button class="sbv-pause" aria-label="Pause"></button>
</div>
<div data-asin="B0VOSKERSB" class="sbv-product">
<span>Sponsored</span>
<h2>Vosker VKX Solar Powered camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VOSKERSB">
<img src="https://example.com/vosker-sb.jpg" />
</a>
</div>
</div>
<article data-component-type="s-search-result" data-asin="B0VIEWVOSK">
<span>Sponsored</span>
<span class="previously-viewed-badge">Previously viewed</span>
<h2>Vosker VKX Solar Powered 4G-LTE Outdoor Cellular Security Camera</h2>
<a href="/sspa/click?url=%2Fdp%2FB0VIEWVOSK">
<img class="s-image" src="https://example.com/vosker-grid.jpg" />
</a>
<span class="a-price"><span class="a-offscreen">$149.99</span></span>
</article>
<article data-component-type="s-search-result" data-asin="B0OWLLABS1">
<span>Sponsored</span>
<h2>Owl Labs Meeting Owl 3</h2>
<a href="/sspa/click?url=%2Fdp%2FB0OWLLABS1">
<img class="s-image" src="https://example.com/owl.jpg" />
</a>
<span class="a-price"><span class="a-offscreen">$1,099.00</span></span>
</article>
<div class="s-related-searches AdHolder">
<h2>Related searches</h2>
<a href="/s?k=home+security+camera">home security camera</a>
<a href="/s?k=outdoor+camera">outdoor camera</a>
</div>
</div>
</main>
`,
{ url: 'https://www.amazon.com/s?k=home+security' },
);
Object.assign(globalThis, {
document: influencerPicksDom.window.document,
Node: influencerPicksDom.window.Node,
Element: influencerPicksDom.window.Element,
HTMLElement: influencerPicksDom.window.HTMLElement,
HTMLAnchorElement: influencerPicksDom.window.HTMLAnchorElement,
HTMLImageElement: influencerPicksDom.window.HTMLImageElement,
});
const influencerPicks = await collectAmazonSearchPageData({ pattern: 'pattern-2' });
const influencerByAsin = new Map(influencerPicks.map((item) => [item.asin, item]));
assert.equal(influencerByAsin.get('B0ORGINF01')?.adType, 'organic');
assert.equal(influencerByAsin.get('B0ANNKE001')?.adType, 'sponsoredProducts');
assert.equal(influencerByAsin.get('B0NIGHTO01')?.adType, 'sponsoredProducts');
assert.equal(influencerByAsin.get('B0SOLAR001')?.adType, 'sponsoredProducts');
assert.equal(
influencerByAsin.get('B0ANNKE001')?.placementId,
influencerByAsin.get('B0NIGHTO01')?.placementId,
);
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adType, 'sponsoredBrands');
assert.equal(influencerByAsin.get('B0VOSKERVK')?.adFormat, 'standard');
assert.equal(influencerByAsin.get('B0VOSKERSB')?.adType, 'sponsoredBrands');
assert.equal(influencerByAsin.get('B0VIEWVOSK')?.adType, 'sponsoredProducts');
assert.equal(influencerByAsin.get('B0OWLLABS1')?.adType, 'sponsoredProducts');
assert.equal(
influencerPicks.some((item) => /related searches/i.test(item.title)),
false,
);
console.log('Amazon search parser fixtures passed.'); console.log('Amazon search parser fixtures passed.');
} }

View File

@ -48,7 +48,7 @@ export async function waitForAmazonSearchPageReady() {
// Visit the full result stream instead of jumping straight to pagination. Amazon loads // Visit the full result stream instead of jumping straight to pagination. Amazon loads
// recommendation and sponsored modules when their region approaches the viewport. // recommendation and sponsored modules when their region approaches the viewport.
for (const fraction of [0.2, 0.55, 0.92]) { for (const fraction of [0.15, 0.35, 0.55, 0.75, 0.92]) {
const documentHeight = Math.max( const documentHeight = Math.max(
document.documentElement.scrollHeight, document.documentElement.scrollHeight,
document.body?.scrollHeight || 0, document.body?.scrollHeight || 0,

View File

@ -92,14 +92,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
return decoded; return decoded;
}; };
const parseAsinFromText = (value: string) => {
const decoded = decodeRepeatedly(value);
const patterns = [
/\/(?:dp|gp\/product|gp\/aw\/d)\/([A-Z0-9]{10})(?:[/?]|$)/i,
/[?&#]asin=([A-Z0-9]{10})(?:[&]|$)/i,
/[?&#]lp_asins=([A-Z0-9]{10})/i,
/\/dp\/([A-Z0-9]{10})/i,
];
for (const pattern of patterns) {
const match = pattern.exec(decoded);
if (match) return match[1].toUpperCase();
}
return undefined;
};
const parseAsin = (node: HTMLElement, href?: string) => { const parseAsin = (node: HTMLElement, href?: string) => {
const dataAsin = node.closest<HTMLElement>('[data-asin]')?.dataset.asin || node.dataset.asin; const dataAsin = node.closest<HTMLElement>('[data-asin]')?.dataset.asin || node.dataset.asin;
if (dataAsin && /^[A-Z0-9]{10}$/.test(dataAsin)) return dataAsin; if (dataAsin && /^[A-Z0-9]{10}$/.test(dataAsin)) return dataAsin;
const candidates = [href || '', decodeRepeatedly(href || '')]; const candidates = [href || '', decodeRepeatedly(href || '')];
for (const candidate of candidates) { for (const candidate of candidates) {
const match = /\/(?:dp|gp\/product)\/([A-Z0-9]{10})(?:[/?]|$)/i.exec(candidate); const asin = parseAsinFromText(candidate);
if (match) return match[1].toUpperCase(); if (asin) return asin;
} }
return undefined; return undefined;
}; };
@ -125,9 +140,14 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const hasSponsoredDisclosure = (node: Element) => const hasSponsoredDisclosure = (node: Element) =>
sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure); sponsoredDisclosureCandidates(node).some(isSponsoredDisclosure);
const closestAsinHost = (node: Element) => {
const host = node.closest<HTMLElement>('[data-asin]');
return host && /^[A-Z0-9]{10}$/.test(host.dataset.asin || '') ? host : null;
};
const hasModuleSponsoredDisclosure = (node: Element) => const hasModuleSponsoredDisclosure = (node: Element) =>
sponsoredDisclosureCandidates(node).some( sponsoredDisclosureCandidates(node).some(
(candidate) => !candidate.closest('[data-asin]') && isSponsoredDisclosure(candidate), (candidate) => !closestAsinHost(candidate) && isSponsoredDisclosure(candidate),
); );
const metadata = (node: Element) => const metadata = (node: Element) =>
@ -152,6 +172,10 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const asin = candidate.dataset.asin; const asin = candidate.dataset.asin;
if (asin && /^[A-Z0-9]{10}$/.test(asin)) asins.add(asin); if (asin && /^[A-Z0-9]{10}$/.test(asin)) asins.add(asin);
}); });
node.querySelectorAll<HTMLAnchorElement>('a[href]').forEach((anchor) => {
const asin = parseAsinFromText(anchor.href);
if (asin) asins.add(asin);
});
return asins.size; return asins.size;
}; };
@ -161,17 +185,117 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
const hasVisibleElement = (node: Element, selector: string) => const hasVisibleElement = (node: Element, selector: string) =>
Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible); Array.from(node.querySelectorAll<HTMLElement>(selector)).some(isVisible);
const isSearchResultCard = (node: Element) =>
node.matches('[data-component-type="s-search-result"]');
const hasModuleHeadline = (node: Element) =>
Array.from(
node.querySelectorAll<HTMLElement>(
'h1, h2, h3, [class*="headline"], [class*="sb-headline"], [class*="sbv-"]',
),
).some(
(heading) =>
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0,
);
const isOutsideProductCard = (node: Element, root: Element) => {
if (node.closest('[data-component-type="s-search-result"]')) return false;
const asinHost = node.closest<HTMLElement>('[data-asin]');
return !asinHost || asinHost === root;
};
const hasModuleLevelVideo = (node: Element) =>
Array.from(node.querySelectorAll('video')).some((video) => isOutsideProductCard(video, node));
const hasModuleLevelBrandVisual = (node: Element) => {
if (hasModuleLevelVideo(node)) return true;
const playPause = Array.from(
node.querySelectorAll<HTMLElement>(
'[aria-label*="Pause" i], [aria-label*="Play" i], [title*="Pause" i], [title*="Play" i]',
),
).some((candidate) => isOutsideProductCard(candidate, node) && isVisible(candidate));
if (playPause) return true;
const outsideImages = Array.from(node.querySelectorAll<HTMLElement>('img')).filter(
(image) => isOutsideProductCard(image, node) && isVisible(image),
);
return outsideImages.length >= 2;
};
const hasInfluencerThematicCopy = (node: Element) =>
Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3, span, a, [class*="headline"]')).some(
(candidate) => {
if (closestAsinHost(candidate) || isVisuallyHiddenText(candidate)) return false;
return /picks from amazon influencers|influencer storefronts|amazon influencer/i.test(
text(candidate),
);
},
);
const hasThematicSpSignal = (node: Element) => {
const blob = [
metadata(node),
node.getAttribute('cel_widget_id') || '',
node.getAttribute('data-multi-ad-feedback-form-trigger') || '',
node.getAttribute('data-component-props') || '',
]
.join(' ')
.toLowerCase();
if (/brand-footer|multi-brand-creative|brands-related|desktop-hsa-3psl/.test(blob)) {
return false;
}
if (
/featured.?asins.?list|sponsoredproductsloom|sp_search_thematic|loom-desktop|influencer/.test(
blob,
)
) {
return true;
}
return hasInfluencerThematicCopy(node);
};
const isSponsoredProductsThematic = (node: Element) => {
if (hasThematicSpSignal(node)) return true;
if (
Array.from(
node.querySelectorAll(
'[cel_widget_id], [data-multi-ad-feedback-form-trigger], [id*="loom"], [class*="loom"]',
),
).some(hasThematicSpSignal)
) {
return true;
}
for (
let current = node.parentElement;
current && !isMainResultsBoundary(current);
current = current.parentElement
) {
if (hasThematicSpSignal(current)) return true;
}
return false;
};
const hasVisibleBrandCreative = (node: Element) => { const hasVisibleBrandCreative = (node: Element) => {
if (isSearchResultCard(node)) return false;
if (isSponsoredProductsThematic(node)) return false;
const visibleBrandDestination = hasVisibleElement( const visibleBrandDestination = hasVisibleElement(
node, node,
'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]', 'a[href*="/stores/"], a[href*="/brands/"], [class*="brand-logo"], [class*="brandLogo"]',
); );
const visibleModuleHeadline = Array.from(node.querySelectorAll<HTMLElement>('h1, h2, h3')).some( const explicitBrandModule = /sponsored.?brand|brand.?ad|brands-related|brand-card|\bsbv-/.test(
(heading) => metadata(node),
!heading.closest('[data-asin]') && isVisible(heading) && text(heading).length > 0, );
const brandsRelatedCopy = /brands related to your search/i.test(text(node));
const moduleSponsored = hasModuleSponsoredDisclosure(node) || hasSponsoredDisclosure(node);
const videoStrip = hasModuleLevelBrandVisual(node) && moduleSponsored;
const headlineUnit = hasModuleHeadline(node) && moduleSponsored;
return (
visibleBrandDestination ||
explicitBrandModule ||
brandsRelatedCopy ||
videoStrip ||
headlineUnit
); );
const explicitBrandModule = /sponsored.?brand|brand.?ad/.test(metadata(node));
return visibleBrandDestination || (explicitBrandModule && visibleModuleHeadline);
}; };
const findAdModule = (card: HTMLElement) => { const findAdModule = (card: HTMLElement) => {
@ -216,11 +340,18 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
adFormat: 'standard' as const, adFormat: 'standard' as const,
}; };
} }
if (isSearchResultCard(card)) {
return {
adType: 'sponsoredProducts' as const,
placement: 'middleOfSearch' as const,
adFormat: hasVisibleElement(card, 'video') ? ('video' as const) : ('standard' as const),
};
}
if (hasVisibleBrandCreative(module)) { if (hasVisibleBrandCreative(module)) {
return { return {
adType: 'sponsoredBrands' as const, adType: 'sponsoredBrands' as const,
placement: 'middleOfSearch' as const, placement: 'middleOfSearch' as const,
adFormat: module.querySelector('video') ? ('video' as const) : ('standard' as const), adFormat: hasModuleLevelVideo(module) ? ('video' as const) : ('standard' as const),
}; };
} }
return { return {
@ -230,6 +361,24 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
}; };
}; };
const hasCollectableProductLink = (node: Element) =>
Boolean(
node.querySelector(
'a[href*="/dp/"], a[href*="%2Fdp%2F"], a[href*="/gp/product"], a[href*="%2Fgp%2Fproduct%2F"], a[href*="sspa/click"], a[href*="lp_asins="]',
),
);
const addCandidate = (node: HTMLElement) => {
if (
isVisible(node) &&
(node.dataset.asin ||
hasCollectableProductLink(node) ||
parseAsin(node, node.querySelector<HTMLAnchorElement>('a[href]')?.href))
) {
candidateSet.add(node);
}
};
const candidateSet = new Set<HTMLElement>(); const candidateSet = new Set<HTMLElement>();
document document
.querySelectorAll<HTMLElement>( .querySelectorAll<HTMLElement>(
@ -238,13 +387,7 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
.forEach((node) => { .forEach((node) => {
const stableCard = const stableCard =
node.closest<HTMLElement>('[data-component-type="s-search-result"]') || node; node.closest<HTMLElement>('[data-component-type="s-search-result"]') || node;
if ( addCandidate(stableCard);
isVisible(stableCard) &&
(stableCard.dataset.asin ||
stableCard.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]'))
) {
candidateSet.add(stableCard);
}
}); });
document.querySelectorAll<HTMLElement>('[data-asin]').forEach((node) => { document.querySelectorAll<HTMLElement>('[data-asin]').forEach((node) => {
@ -268,13 +411,29 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
isVisible(sidebarAd) && isVisible(sidebarAd) &&
hasSponsoredDisclosure(sidebarAd) && hasSponsoredDisclosure(sidebarAd) &&
sidebarAd.querySelector('h2, h3') && sidebarAd.querySelector('h2, h3') &&
sidebarAd.querySelector('img[src]') && sidebarAd.querySelector('img[src], img[data-src]') &&
sidebarAd.querySelector('a[href*="/dp/"], a[href*="%2Fdp%2F"]') hasCollectableProductLink(sidebarAd)
) { ) {
candidateSet.add(sidebarAd); candidateSet.add(sidebarAd);
} }
}); });
document
.querySelectorAll<HTMLElement>(
'.AdHolder, [class*="brands-related"], [class*="brand-card"], [data-component-type="sponsored-brand"]',
)
.forEach((module) => {
if (!isVisible(module)) return;
const nestedCards = module.querySelectorAll<HTMLElement>(
'[data-asin], [class*="brand-card"], .sbv-product, [data-slot*="3psl"], [data-slot*="hsa"]',
);
if (nestedCards.length > 0) {
nestedCards.forEach(addCandidate);
return;
}
addCandidate(module);
});
const cards = Array.from(candidateSet) const cards = Array.from(candidateSet)
.filter( .filter(
(card, index, all) => (card, index, all) =>
@ -310,13 +469,22 @@ export async function collectAmazonSearchPageData(_: SearchPageCollectorPayload)
if (!asin) return items; if (!asin) return items;
const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3'); const titleNode = card.querySelector<HTMLElement>('h2[aria-label], h2, h3[aria-label], h3');
const imageNode = card.querySelector<HTMLImageElement>('img.s-image, img[src]'); const imageNode = card.querySelector<HTMLImageElement>(
'img.s-image, img[src], img[data-src], img[srcset]',
);
const title = const title =
titleNode?.getAttribute('aria-label')?.trim() || titleNode?.getAttribute('aria-label')?.trim() ||
text(titleNode) || text(titleNode) ||
imageNode?.alt?.trim() || imageNode?.alt?.trim() ||
asin; asin;
const imageSrc = imageNode?.src; const dataSrc = imageNode?.getAttribute('data-src') || imageNode?.dataset.src;
const srcsetUrl = (imageNode?.getAttribute('srcset') || '')
.split(',')[0]
?.trim()
.split(/\s+/)[0];
const rawSrc = imageNode?.src && !imageNode.src.startsWith('data:') ? imageNode.src : undefined;
const placeholderSrc = /grey-pixel|transparent-pixel|\/1x1(?:\.|$)/i.test(rawSrc || '');
const imageSrc = (placeholderSrc ? undefined : rawSrc) || dataSrc || srcsetUrl;
if (!imageSrc) return items; if (!imageSrc) return items;
const currentPriceNode = card.querySelector<HTMLElement>( const currentPriceNode = card.querySelector<HTMLElement>(