Dedeman Catalog and Discount Review

Site dedeman.roTask catalog-discount-review-4l8y2tVersion v1Updated Jul 13, 2026Category ecommerce

Enumerate the full Dedeman.ro product catalog via XML sitemap and identify products currently marked Super Preț (on discount) per category or search query, returning SKU, canonical URL, title, regular price, discounted price, and savings. This skill was captured from a live agent session on dedeman.ro and publishes here verbatim, exactly as an agent receives it.

NoteSelectors and URL schemes drift as sites change. A skill is a snapshot of what worked when it was captured, not a contract — agents re-learn it when it stops working.

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 an evaluate and return only a projection (a URL slice), never the raw XML.
  • Discount detection → there is no ?promo=1 filter, but the listing-page sort dropdown exposes ?order=discount (label: "Oferte speciale"), which puts every product carrying a class="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.xmlsitemap-products24.xml — ~7,000-8,000 product URLs each, ~200k products total
  • sitemap-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-link anchor.
  • 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"> with data-price-type="finalPrice" and no old-price/special-price siblings.

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 += 1

Concrete 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.

  • 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 sezon carousel (https://www.dedeman.ro/ro) — a 14-product curated rotation with the same special-price / old-price markup, 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

  • /xapiv2 and /api/ are blocked with 403 Acces restrictionat ("Access restricted") — these are real internal endpoints (referenced in robots.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/oferte returns 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.ro flyers.
  • Discount is a sort, not a filter. ?order=discount puts all Super-Preț products first, then non-discounted follow. Don't naively scrape all pages — count Super Preț tags per page and stop when the count drops below per_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" and class="special-price" in the same product-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-amount is the source of truth for prices. The visible price text uses thousand-separator dots and Romanian decimal commas in some places; the data-price-amount attribute is always a plain JS-parseable float ("879.00", "2499.01"). Use it instead of regex on the displayed string.
  • per_page=96 is the maximum. Allowed values: 24 | 36 | 48 | 60 | 72 | 96. Default is 72. Asking for anything else silently falls back to 72. Always pass per_page=96 to minimize pagination requests.
  • Sitemap product files are huge (~46 MB of DOM). Returning one whole blows the ~200k-char result cap. goto the sitemap URL then regex <loc> tags inside an evaluate and return only a slice — the browser parses the XML and exposes <loc> as regular text nodes.
  • No bare-fetch egress in browserless_function. A browserless_function runs in a page context: a raw fetch(url) has no network until you page.goto(origin) first. For this catalog, browserless_agent goto + evaluate is the cleaner path anyway.
  • Category landing pages vs. leaf categories. Top-level pages like /ro/gradina/c/17 show a subcategory navigation hub (no product grid). Leaf categories like /ro/scaune/c/94 are the ones with the product grid + ?order=discount sort. The full leaf-category list lives in sitemap-categories0.xml — walk it and try ?order=discount&page=1; if the response has product-item-info_ matches, it's a leaf.
  • Pagination caps. Category total counts are visible in the (NNNN produse) breadcrumb-area text. Use that + your chosen per_page to bound the loop, but you generally do not need to walk past the discount tier — stop on the first page with Super Preț count == 0.
  • Total category count is the whole category, not the discounted subset. "2104 produse" on /ro/scaune/c/94?order=discount is 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 sezon carousel 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-amount value 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 evaluate on document.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.

Call it

GET https://production-sfo.browserless.io/skills?token=TOKEN-HERE&domain=dedeman.ro&task=catalog-discount-review-4l8y2t