Purpose
Collect structured product data from one or more Media Expert category listing URLs, including product links, names, catalogue codes, prices, promotions, ratings, availability indicators, images, hero benefits, and dynamically expandable attributes.
When to Use
Use when the caller provides a Media Expert category or listing URL such as {category-url} or a collection of {category-urls}. This recipe is intended for full-list extraction rather than a single product page.
Workflow
- Navigate directly to each supplied
{category-url}; do not visit the homepage or use the search box. - Wait for
div.offers-list. Accept the OneTrust banner with#onetrust-accept-btn-handlerwhen present and close the promotional popup#snrs-closewhen visible. If launching a browser directly, use a stealth-capable Chromium context because the site may challenge automated browsers. - On the loaded listing page, run this single
evaluate()extractor. It expands product attributes, repeatedly scrolls to trigger lazy loading, deduplicates cards by catalogue code or product link, and returns all records:
(async () => {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const clean = (v) => (v || "").replace(/\\s+/g, " ").trim();
const abs = (href) => (href ? new URL(href, location.origin).href : "");
const root = () => document.querySelector("div.offers-list");
const cards = () =>
root() ? [...root().children].filter((e) => e.querySelector("span.id")) : [];
const expand = (card) => {
const controls = [...card.querySelectorAll('button, [role="button"]')];
const button = controls.find((b) => {
const t = clean(b.textContent).toLowerCase();
const a = clean(b.getAttribute("aria-label")).toLowerCase();
const cy = clean(b.getAttribute("data-cy")).toLowerCase();
const ac = clean(b.getAttribute("aria-controls")).toLowerCase();
return (
cy.includes("seemoreattrlink") ||
t.includes("pokaż więcej") ||
a.includes("pokaż więcej") ||
ac === "attributessection" ||
clean(b.className).toLowerCase().includes("toggle-attr")
);
});
if (button && !clean(button.textContent).toLowerCase().includes("pokaż mniej")) {
button.scrollIntoView({ block: "center" });
button.click();
}
};
if (!root()) return { url: location.href, products: [] };
let unchanged = 0,
previousCount = 0,
previousHeight = 0;
while (unchanged < 2) {
const current = cards();
current.forEach(expand);
await sleep(180);
const count = cards().length;
const height = document.body.scrollHeight;
window.scrollBy(0, Math.max(500, Math.round(innerHeight * 1.7)));
await sleep(350);
unchanged =
count === previousCount && height === previousHeight ? unchanged + 1 : 0;
previousCount = count;
previousHeight = height;
}
window.scrollTo(0, 0);
await sleep(150);
const text = (el, sel) => clean(el.querySelector(sel)?.textContent);
const attr = (el, sel, name) => el.querySelector(sel)?.getAttribute(name) || "";
const allText = (el, sel) =>
[...el.querySelectorAll(sel)].map((x) => clean(x.textContent));
const seen = new Set();
const products = [];
for (const card of cards()) {
const code = text(card, "span.id").replace(/^Kod:\\s*/i, "");
const href = abs(attr(card, "h3 a", "href"));
const key = code || href;
if (!key || seen.has(key)) continue;
seen.add(key);
const additional = allText(card, "div.additional-prices div.price");
const scoreText = text(card, "div.row span.screen-reader-text");
const attrNames = allText(card, "table.list.attributes th .attribute-name");
const attrValues = allText(card, "table.list.attributes span.attribute-value");
const attributes = {};
for (let i = 0; i < Math.min(attrNames.length, attrValues.length); i++) {
if (attrNames[i] && attrValues[i]) attributes[attrNames[i]] = attrValues[i];
}
products.push({
url: href,
name: text(card, "h3 a"),
code,
price: text(card, "div.is-big span.whole").replace(/\\s+/g, ""),
price_description: text(card, "div.price-with-code-emblem div.description"),
price_before: (additional[0] || "").split(",")[0].replace(/\\s+/g, ""),
price_lowest: (additional[1] || "").split(",")[0].replace(/\\s+/g, ""),
price_installment: (
text(card, "div.emblem-text strong").split("zł")[0] || "0"
).replace(/\\s+/g, ""),
score_text: scoreText,
promotions: [
...card.querySelectorAll(
"div.emblems-wrapper.is-desktop div.emblems-desktop div a",
),
]
.map((x) => x.getAttribute("aria-label") || "")
.filter(Boolean),
energy_class:
text(card, "div.icon-energy-class.icon-size-xxl span") ||
attr(card, "div.is-desktop.with-energy-class img", "alt")
.replace("Klasa energetyczna ", "")
.trim(),
image: attr(card, "div.column-left div.picture-image img", "src"),
hero1: text(card, "div.product-attributes-usp-list span:nth-child(1)"),
hero2: text(card, "div.product-attributes-usp-list span:nth-child(2)"),
available_in_store: !!card.querySelector(
"div.available-in-store, div.available-in-store-label",
),
pos_delivery: !!card.querySelector("div.pos-delivery-label, div.ozg-label"),
calendar_delivery: !!card.querySelector("div.calendar-delivery-label"),
predicted_availability: text(card, "div.postscript.availability span.name"),
unavailable_offer: !!card.querySelector("div.offer-unavailable"),
above_emblem: text(
card,
"div.above-name-emblem div.emblem-tooltip div.above-emblem",
),
attributes,
});
}
return { url: location.href, products };
})();- If multiple URLs are supplied, repeat the direct navigation and extractor for each URL and concatenate the returned
productsarrays. Usecode, falling back tourl, as the cross-page deduplication key.
Site-Specific Gotchas
- Product cards are direct children of
div.offers-list, but not every child is a product; retain only elements containingspan.id. - Important attributes are initially collapsed. The Polish “Pokaż więcej” control may be identified by
data-cy="seeMoreAttrLink",aria-controls="attributesSection", or its text/class, and must be clicked before readingtable.list.attributes. - Scrolling is required to materialize lazy-loaded cards; stop only after repeated passes produce no new cards and no document-height growth.
- The page can contain a OneTrust consent banner and an
#snrs-closepromotional popup that obstruct interaction. - Selectors such as
div.offers-list,span.id, andtable.list.attributesare site-specific DOM selectors and should be revalidated if Media Expert changes its frontend. - Product ratings are exposed as Polish screen-reader prose in
div.row span.screen-reader-text; preservescore_textunless locale-specific parsing is explicitly requested.
Expected Output
Return an object per unique listed product with url, name, code, pricing fields, rating text, promotions, energy class, image, hero benefits, availability flags, predicted availability, unavailable-offer status, above-name emblem, and an attributes object keyed by the visible attribute names. For multiple listing URLs, return the deduplicated concatenation of all product objects.