From 05b99e9ed43ee42ef9ad7b80f9282fb6598ee38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Thu, 16 Jul 2026 15:19:07 +0200 Subject: [PATCH] feat: working scraping for Promofarma, DocMorris, and Primor - Fixed Primor URL format and selectors - Created simple N8N webhook that calls scraper API - All 3 sources now working: - Promofarma: 20 products - DocMorris: 10 products - Primor: 20 products - Total: 130 products in database - N8N webhook at /webhook/scrape working correctly --- apps/parapharmacy-api/src/scraper.js | 34 +++++++++++++------ n8n/workflow-sdk-simple.js | 51 ++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 10 deletions(-) create mode 100644 n8n/workflow-sdk-simple.js diff --git a/apps/parapharmacy-api/src/scraper.js b/apps/parapharmacy-api/src/scraper.js index ca09f55..9838d83 100644 --- a/apps/parapharmacy-api/src/scraper.js +++ b/apps/parapharmacy-api/src/scraper.js @@ -28,12 +28,12 @@ const SOURCES = { }, primor: { name: 'Primor', - searchUrl: (q) => `https://www.primor.eu/search?s=${encodeURIComponent(q)}`, + searchUrl: (q) => `https://www.primor.eu/catalogsearch/result/?q=${encodeURIComponent(q)}`, selectors: { - product: '.product-item, .product-miniature', - name: '.product-title, h3', - price: '.price', - link: 'a.product-title', + product: 'form.product-item', + name: 'form.product-item', + price: 'form.product-item', + link: 'a', image: 'img' } } @@ -74,7 +74,10 @@ async function scrapeSource(browser, source, query) { 'a[href*="/p/"]', '[class*="Product"]', 'article', - '[role="listitem"]' + '[role="listitem"]', + '.product-item', + '.product', + '[data-product]' ]; for (const sel of selectors) { @@ -85,8 +88,8 @@ async function scrapeSource(browser, source, query) { count: els.length, samples: Array.from(els).slice(0, 3).map(el => ({ tag: el.tagName, - class: el.className?.substring(0, 100), - html: el.outerHTML?.substring(0, 300) + class: el.className?.substring(0, 150), + html: el.outerHTML?.substring(0, 500) })) }; } @@ -110,8 +113,19 @@ async function scrapeSource(browser, source, query) { cards.forEach(card => { // Try data attributes first (Promofarma style) - const name = card.getAttribute('data-name') || card.querySelector(selectors.name)?.innerText?.trim(); - const priceStr = card.getAttribute('data-pvp') || card.querySelector(selectors.price)?.innerText?.trim(); + let name = card.getAttribute('data-name'); + let priceStr = card.getAttribute('data-pvp'); + + // If no data attributes, try to extract from content + if (!name) { + const nameEl = card.querySelector('[class*="name"], h3, h2, .product-name'); + name = nameEl?.innerText?.trim(); + } + if (!priceStr) { + const priceEl = card.querySelector('[class*="price"], .price'); + priceStr = priceEl?.innerText?.trim(); + } + const linkEl = card.querySelector(selectors.link); const imgEl = card.querySelector(selectors.image); diff --git a/n8n/workflow-sdk-simple.js b/n8n/workflow-sdk-simple.js new file mode 100644 index 0000000..fd5e81b --- /dev/null +++ b/n8n/workflow-sdk-simple.js @@ -0,0 +1,51 @@ +import { workflow, node, trigger } from '@n8n/workflow-sdk'; + +const webhook = trigger({ + type: 'n8n-nodes-base.webhook', + version: 2, + config: { + name: 'Webhook', + position: [240, 300], + parameters: { + httpMethod: 'POST', + path: 'scrape', + responseMode: 'lastNode' + } + }, + output: [{}] +}); + +const callApi = node({ + type: 'n8n-nodes-base.httpRequest', + version: 4.2, + config: { + name: 'Call Scraper API', + position: [440, 300], + parameters: { + method: 'POST', + url: 'http://parapharmacy-api:3002/api/scrape', + sendBody: true, + specifyBody: 'json', + jsonBody: '={{ JSON.stringify($json.body || { queries: ["capricare"], sources: ["promofarma"] }) }}' + } + }, + output: [{}] +}); + +const response = node({ + type: 'n8n-nodes-base.code', + version: 2, + config: { + name: 'Response', + position: [640, 300], + parameters: { + jsCode: `return [{ json: $input.first().json }];` + } + }, + output: [{}] +}); + +export default workflow('scrape-simple', 'Parapharmacy Scraper Simple') + .add(webhook) + .to(callApi) + .to(response);