Purpose
Enumerate the full Dedeman.ro product catalog and identify which products are currently on discount (carry a "Super Preț" tag). Returns: (1) a list of canonical product URLs and SKUs across the whole store, and (2) per-category lists of discounted products with regular price, discounted price, and computed savings. Read-only — never adds to cart, never checks out.
When to Use
- "Give me everything Dedeman sells in category X, and which of those are reduced right now."
- Daily monitoring of new "Super Preț" promotions (homepage + per-category).
- Bulk catalog snapshots for price-tracking dashboards.
- Pre-research before a customer asks "is there a sale on garden chairs?"
Workflow
Dedeman.ro runs on Magento 2. There is no public JSON API (/xapiv2 and /api/ return 403 Acces restrictionat). The optimal pattern is hybrid:
- Catalog enumeration → parse the public XML sitemaps. No auth needed — but each product file is ~46 MB rendered, far over the ~200k-char result cap, so extract
<loc>tags in-page with anevaluateand return only a projection (a URL slice), never the raw XML. - Discount detection → there is no
?promo=1filter, but the listing-page sort dropdown exposes?order=discount(label: "Oferte speciale"), which puts every product carrying aclass="special-price"block at the top of the result set. Paginate until the per-page Super-Preț count drops below the page size; that's the end of the discounted tier.
1. Session model
The browserless_agent session persists across calls, keyed by proxy/profile — keep a whole multi-step flow (nav → read grid → next page) inside a single call's commands array so cookies persist and you don't accidentally drop the session config on a follow-up. Pass proxy: { proxy: "residential" } on every call (Romanian eCommerce; US IPs occasionally hit a cookie/region prompt or a Fastly cache miss path). The site is not Akamai-protected at the page level and most pages render cleanly, but the API endpoint is restricted and large sitemaps must be projected in-page rather than shipped whole.
2. Enumerate the full catalog from the sitemap
Get the small (~3 KB) sitemap index — a browserless_agent goto then read the <loc> list from the DOM:
{ "method": "goto", "params": { "url": "https://www.dedeman.ro/media/sitemap/sitemap.xml", "waitUntil": "load", "timeout": 45000 } }
{ "method": "evaluate", "params": { "content": "(()=>JSON.stringify([...document.body.innerText.matchAll(/<loc>([^<]+)<\\/loc>/g)].map(m=>m[1])))()" } }The index lists ~25 child sitemaps:
sitemap-categories0.xml— category landing URLs (/ro/<slug>/c/<id>)sitemap-products0.xml…sitemap-products24.xml— ~7,000-8,000 product URLs each, ~200k products totalsitemap-cms0.xml— informational pages
Each sitemap-productsN.xml renders to ~46 MB of DOM — far over the ~200k-char result cap, so parse and slice in-page (return a projection, not the raw XML):
{ "method": "goto", "params": { "url": "https://www.dedeman.ro/media/sitemap/sitemap-products0.xml", "waitUntil": "load", "timeout": 45000 } }
{ "method": "evaluate", "params": { "content": "(()=>{const u=[...document.body.innerText.matchAll(/<loc>([^<]+)<\\/loc>/g)].map(m=>m[1]);return JSON.stringify({total:u.length,urls:u.slice(0,2000)});})()" } }Bump the slice offset across calls (or filter to the SKUs you need) so each result stays under the size cap. Canonical product URL shape: https://www.dedeman.ro/ro/<kebab-title>/p/<sku> — the trailing 6- to 7-digit SKU after /p/ is the stable Magento product ID.
3. List discounted products in a category
Category URL shape: https://www.dedeman.ro/ro/<slug>/c/<categoryId> (e.g. /ro/scaune/c/94, /ro/gradina/c/17). To get only the discounted tier, sort by order=discount and bump the page size:
{ "method": "goto", "params": { "url": "https://www.dedeman.ro/ro/scaune/c/94?order=discount&per_page=96&page=1", "waitUntil": "load", "timeout": 45000 } }Then parse the grid with an evaluate (see step 4). per_page accepts 24 | 36 | 48 | 60 | 72 | 96 — use 96 to cut paginations by ~25%. The dropdown's underlying sort options are visible in the page HTML as order=availability (default), order=price-asc, order=price-desc, order=date, order=discount.
4. Parse the product grid
Each grid card is a <div class="product-item-info" id="product-item-info_<internalId>"> block. Inside:
- Canonical URL + SKU:
<a class="product-item-link" href="https://www.dedeman.ro/ro/<slug>/p/<sku>">(the same anchor wraps the title text). - Title: text content of the
.product-item-linkanchor. - Old price (when discounted):
<span class="old-price">⊃<span data-price-amount="<float>" data-price-type="oldPrice">. - Discounted price:
<span class="special-price">⊃<span class="price-label">Super Preț</span>+<span data-price-amount="<float>" data-price-type="finalPrice">. - Regular price (non-discount): a single
<span class="price-container price-final_price tax weee">withdata-price-type="finalPrice"and noold-price/special-pricesiblings.
A product is on discount iff the card contains both class="old-price" and class="special-price" (equivalently: contains the string "Super Preț"). There is only one discount label across the whole site — Super Preț. No other badge variants exist.
Run this parse inside an evaluate (bind const html = document.body.innerHTML; first) and return JSON.stringify(products) — never ship the raw grid HTML back:
const re =
/<div class="product-item-info" id="product-item-info_(\d+)"[\s\S]*?<\/li>/g;
let m,
products = [];
while ((m = re.exec(html))) {
const block = m[0];
const url = (block.match(
/href="(https:\/\/www\.dedeman\.ro\/ro\/[^"]+\/p\/\d+)"/,
) || [])[1];
if (!url) continue;
const sku = url.match(/\/p\/(\d+)/)[1];
const title = (block.match(/class="product-item-link"[^>]*>([^<]+)</) || [])[1];
const oldPrice = parseFloat(
(block.match(/old-price[\s\S]*?data-price-amount="([\d.]+)"/) || [])[1] || "NaN",
);
const finalPrice = parseFloat(
(block.match(/special-price[\s\S]*?data-price-amount="([\d.]+)"/) || [])[1] ||
"NaN",
);
const onDiscount = block.includes("Super Preț");
products.push({
sku,
url,
title,
on_discount: onDiscount,
regular_price: isNaN(oldPrice) ? null : oldPrice,
discounted_price: isNaN(finalPrice) ? null : finalPrice,
});
}5. Paginate until the Super-Preț tier ends
order=discount is a sort, not a filter — discounted products are at the top, non-discounted continue afterwards. Count Super-Preț occurrences per page; when that count drops below per_page, that's the boundary page (discounted tier ends mid-page). When the count is 0, you're past the discount tier.
# pseudocode (loop pages inside one browserless_agent commands array: goto ?page=N then evaluate)
N=1
while true:
goto ?page=N ; evaluate the grid
super_count = count "Super Preț" in html
if super_count == 0: break # discounts exhausted
if super_count < per_page: # boundary page — capture only the first super_count cards
capture and break
N += 1Concrete example (category c/94 "Scaune", 2,104 total products, per_page=72): pages 1-4 each carry 72/72 Super-Preț cards, page 5 carries 17/72, page 6+ carry 0 → ~305 discounted products total (~14.5% of the category). With per_page=96, pages 1-3 carry 96/96, page 4 is the boundary.
6. Optional: catalog flyers and the homepage carousel
https://catalog.dedeman.ro/oferte-catalog/is a separate subdomain hosting Dedeman's printed flyer/catalog (e.g.catalog-mai-2026,catalog-iunie-2025) as flipbook-style HTML — useful when you want only the editorially curated headline promotions instead of every Super-Preț tag.- Homepage
## Oferte de sezoncarousel (https://www.dedeman.ro/ro) — a 14-product curated rotation with the samespecial-price/old-pricemarkup, parseable with the same regex as step 4. Cheap quick-look at "what's promoted right now."
7. Search-page variant
The same order=discount works on full-text search: https://www.dedeman.ro/ro/catalogsearch/result/v2?q=<term>&order=discount&per_page=96 returns the same grid markup and discount sorting. Useful for "what's on sale that matches <keyword>" without category-walking.
Site-Specific Gotchas
/xapiv2and/api/are blocked with403 Acces restrictionat("Access restricted") — these are real internal endpoints (referenced inrobots.txt) but they're locked to the official mobile app. Do not waste time probing them; they return a static "Acces restrictionat" HTML page even with stealth + residential proxy./ro/ofertereturns 404 — there is no single "all discounts" landing page. The discount surface is per-category (or per-search) via?order=discount, plus the homepage carousel +catalog.dedeman.roflyers.- Discount is a sort, not a filter.
?order=discountputs all Super-Preț products first, then non-discounted follow. Don't naively scrape all pages — countSuper Prețtags per page and stop when the count drops belowper_page. - Only one discount label exists:
Super Preț. There is no "Top Preț", "Reducere", "Lichidare", or rotating-promo label. If a card has<span class="price-label">Super Preț</span>it's on sale; otherwise it isn't. - Detect a discount via markup, not text. The reliable signal is the presence of both
class="old-price"andclass="special-price"in the sameproduct-item-info_<id>block — the textual label "Super Preț" can be hidden by CSS in some carousel variants but the markup is always present. data-price-amountis the source of truth for prices. The visible price text uses thousand-separator dots and Romanian decimal commas in some places; thedata-price-amountattribute is always a plain JS-parseable float ("879.00","2499.01"). Use it instead of regex on the displayed string.per_page=96is the maximum. Allowed values:24 | 36 | 48 | 60 | 72 | 96. Default is72. Asking for anything else silently falls back to72. Always passper_page=96to minimize pagination requests.- Sitemap product files are huge (~46 MB of DOM). Returning one whole blows the ~200k-char result cap.
gotothe sitemap URL then regex<loc>tags inside anevaluateand return only a slice — the browser parses the XML and exposes<loc>as regular text nodes. - No bare-
fetchegress inbrowserless_function. Abrowserless_functionruns in a page context: a rawfetch(url)has no network until youpage.goto(origin)first. For this catalog,browserless_agentgoto+evaluateis the cleaner path anyway. - Category landing pages vs. leaf categories. Top-level pages like
/ro/gradina/c/17show a subcategory navigation hub (no product grid). Leaf categories like/ro/scaune/c/94are the ones with the product grid +?order=discountsort. The full leaf-category list lives insitemap-categories0.xml— walk it and try?order=discount&page=1; if the response hasproduct-item-info_matches, it's a leaf. - Pagination caps. Category total counts are visible in the
(NNNN produse)breadcrumb-area text. Use that + your chosenper_pageto bound the loop, but you generally do not need to walk past the discount tier — stop on the first page withSuper Prețcount == 0. - Total category count is the whole category, not the discounted subset. "2104 produse" on
/ro/scaune/c/94?order=discountis the full chair catalog, not the discount count. The site does not publish a per-category discount count anywhere — you derive it from the pagination walk in step 5. - Homepage carousel duplicates entries: the
Oferte de sezoncarousel renders the same 14 products three times in sequence (Magento's carousel HTML clones items to enable infinite scroll). Dedupe by SKU when extracting from the homepage. - No price localization / store-level toggles affect the listing prices. The "Verifică disponibilitatea în magazin" widget only filters availability, not price — the
data-price-amountvalue is the national list price. - US-IP cookie banner. First-visit from a non-RO IP shows a cookie banner that overlays the page. It shows up in extracted text but the grid below is still in the DOM and parseable via
evaluateondocument.body.innerHTML. The banner does not block scraping.
Expected Output
The skill produces two JSON shapes (one per sub-task).
1. Full catalog enumeration:
{
"total_products": 198421,
"sitemaps_walked": 25,
"categories_total": 1832,
"sample_products": [
{
"sku": "5006949",
"url": "https://www.dedeman.ro/ro/placa-gips-carton-tip-f-protectie-foc-rigips-rf-12-5-x-1200-x-2000-mm/p/5006949"
},
{
"sku": "1012083",
"url": "https://www.dedeman.ro/ro/stergator-parbriz-auto-vivauto-flat-flexibil-18-inch-45-cm-1-bucata/p/1012083"
}
]
}2. Discount review for one category:
{
"category": {
"id": "94",
"slug": "scaune",
"url": "https://www.dedeman.ro/ro/scaune/c/94",
"total_products": 2104
},
"discounted_count": 305,
"discount_share": 0.145,
"discounted_products": [
{
"sku": "8052204",
"title": "Scaun birou directorial cu suport lombar si tetiera Grandio Mob LA-829AH, mesh, negru",
"url": "https://www.dedeman.ro/ro/scaun-birou-directorial-cu-suport-lombar-si-tetiera-grandio-mob-la-829ah-mesh-negru/p/8052204",
"regular_price": 879.0,
"discounted_price": 549.0,
"currency": "RON",
"unit": "bucată",
"savings_abs": 330.0,
"savings_pct": 0.376,
"label": "Super Preț"
},
{
"sku": "8065739",
"title": "Scaun gaming cu perna lombara si perna cervicala Bellem, rotativ, imitatie piele, negru + portocaliu",
"url": "https://www.dedeman.ro/ro/scaun-gaming-cu-perna-lombara-si-perna-cervicala-bellem-rotativ-imitatie-piele-negru-portocaliu/p/8065739",
"regular_price": 899.0,
"discounted_price": 649.0,
"currency": "RON",
"unit": "bucată",
"savings_abs": 250.0,
"savings_pct": 0.278,
"label": "Super Preț"
}
]
}3. Discount-search variant (/catalogsearch/result/v2?q=...&order=discount):
{
"query": "lampa",
"total_results": 802,
"discounted_count": 71,
"discounted_products": [/* same shape as above */]
}4. Quick "headline promotions" output (homepage carousel only):
{
"source": "homepage_carousel",
"section": "Oferte de sezon",
"products": [
{
"sku": "7028126",
"title": "Pergola metalica pentru gradina, arcada trandafiri cataratori / suport flori, 140 x 35 x 250 cm",
"regular_price": 199.9,
"discounted_price": 181.9,
"url": "https://www.dedeman.ro/ro/pergola-metalica-pentru-gradina-arcada-trandafiri-cataratori/-suport-flori-140-x-35-x-250-cm/p/7028126"
}
]
}All prices are in Romanian Lei (RON). The unit field is what the site shows after the slash (/bucată, /set, /m², /saco, etc.) — extract it from the <span class="price-wrapper price-sale-unit">/<unit></span> element in the same price-box.