{
  "version": "1.0.0",
  "exported_at": "2026-06-03T00:00:00.000Z",
  "project": {
    "name": "Hoteles Listing Scraper",
    "description": "Extracts hotel detail data from es.hoteles.com property URLs with check-in/check-out dates. Navigation strategy: multi-URL input list using navigate.urls[] and loop-continue, appending each hotel detail page into one CSV. The target pages are not paginated detail pages; add more hotel detail URLs to the Navigate block to scrape additional properties.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated-hoteles-listing-scraper"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to each Hoteles.com hotel detail URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://es.hoteles.com/ho124222/melia-madrid-princesa-madrid-espana/?chkin=2024-09-04&chkout=2024-09-06&x_pwa=1&rfrr=HSR&pwa_ts=1724726896511&referrerUrl=aHR0cHM6Ly9lcy5ob3RlbGVzLmNvbS9Ib3RlbC1TZWFyY2g%3D&useRewards=true&rm1=a2%C2%AEionId=166&destination=Espa%C3%B1a&destType=MARKET&neighborhoodId=179720&latLong=40.416705%2C-3.703582&rank=1&amenities=POOL&sort=RECOMMENDED&top_dp=450&top_cur=EUR&selectedRoomType=7154&selectedRatePlan=211166346&expediaPropertyId=11885&propertyName=Melia+Madrid+Princesa",
          "https://es.hoteles.com/ho564016/hard-rock-hotel-tenerife-adeje-espana/?chkin=2024-09-04&chkout=2024-09-06&x_pwa=1&rfrr=HSR&pwa_ts=1724726896516&referrerUrl=aHR0cHM6Ly9lcy5ob3RlbGVzLmNvbS9Ib3RlbC1TZWFyY2g%3D&useRewards=true&rm1=a2%C2%AEionId=166&destination=Espa%C3%B1a&destType=MARKET&neighborhoodId=6047465&latLong=40.416705%2C-3.703582&rank=2&amenities=POOL&sort=RECOMMENDED&top_dp=488&top_cur=EUR&selectedRoomType=201375004&selectedRatePlan=380119246&expediaPropertyId=13089814&propertyName=Hard+Rock+Hotel+Tenerife",
          "https://es.hoteles.com/ho283868/acqua-salou-espana/?chkin=2024-09-04&chkout=2024-09-06&x_pwa=1&rfrr=HSR&pwa_ts=1724726896511&referrerUrl=aHR0cHM6Ly9lcy5ob3RlbGVzLmNvbS9Ib3RlbC1TZWFyY2g%3D&useRewards=true&rm1=a2%C2%AEionId=166&destination=Espa%C3%B1a&destType=MARKET&neighborhoodId=553248633937780731&latLong=40.416705%2C-3.703582&amenities=POOL&sort=RECOMMENDED&top_dp=329&top_cur=EUR&selectedRoomType=324279361&selectedRatePlan=394360770&expediaPropertyId=1874130&propertyName=Golden+Acqua+Salou"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "hoteles",
          "multi-url",
          "hotel-details"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for the hotel detail page to finish loading",
      "position_x": 480,
      "position_y": 220,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Dismiss or remove cookie consent overlays if present",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "jsCode": "(() => {\n  const clickCandidates = [\n    '#onetrust-accept-btn-handler',\n    'button[id*=\"accept\" i]',\n    'button[aria-label*=\"Aceptar\" i]',\n    'button:has-text(\"Aceptar\")'\n  ];\n  for (const selector of clickCandidates) {\n    try {\n      const el = document.querySelector(selector);\n      if (el && typeof el.click === 'function') {\n        el.click();\n        break;\n      }\n    } catch (e) {}\n  }\n  const removable = ['#onetrust-banner-sdk', '.onetrust-pc-dark-filter', '[class*=\"cookie\" i]', '[class*=\"consent\" i]'];\n  for (const selector of removable) {\n    try {\n      document.querySelectorAll(selector).forEach(el => {\n        const style = window.getComputedStyle(el);\n        const rect = el.getBoundingClientRect();\n        if (selector === '#onetrust-banner-sdk' || rect.height > 80 || style.position === 'fixed') {\n          el.remove();\n        }\n      });\n    } catch (e) {}\n  }\n  return true;\n})()",
        "waitForCompletion": true,
        "timeout": 10
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until the hotel title is visible",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "selector": "h1",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "inject-javascript-2",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Scroll page to trigger lazy-loaded hotel details and room pricing",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "jsCode": "async () => {\n  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n  const steps = 6;\n  for (let i = 1; i <= steps; i++) {\n    window.scrollTo(0, Math.floor(document.body.scrollHeight * i / steps));\n    await delay(450);\n  }\n  window.scrollTo(0, 0);\n  await delay(600);\n  return true;\n}",
        "waitForCompletion": true,
        "timeout": 8
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Allow dynamic content to settle after scrolling",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Extract one hotel record from the current detail page",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "rowSelector": "body",
        "fileName": "hoteles-listados-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "nombre",
            "selector": "const h1 = ROW.querySelector('h1'); return h1 ? h1.textContent.trim() : (document.title || '').replace(/\\s*-\\s*Hoteles.*$/i, '').trim();",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "URL",
            "selector": "return window.location.href;",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "n_estrellas",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const m = text.match(/(\\d+(?:[,.]\\d+)?)\\s+de\\s+5/i); return m ? m[1].replace('.', ',') + ' de 5' : '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "direccion",
            "selector": "const text = document.body.innerText.split('\\n').map(s => s.trim()).filter(Boolean); const bad = /^(Resumen|Información|Servicios|Políticas|Comentarios|Selecciona|Habitación|Galería|Iniciar sesión|Comprar viaje|Hoteles|Coches|Ofertas)$/i; const h1 = (document.querySelector('h1')?.textContent || '').trim(); const candidates = text.filter(s => s !== h1 && !bad.test(s) && s.length > 2 && s.length < 90); const cityLike = candidates.find(s => /Madrid|Adeje|Salou|España|Spain|Tenerife|Barcelona|Valencia|Sevilla|Málaga/i.test(s)); return cityLike || '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Servicios_populares",
            "selector": "const lines = document.body.innerText.split('\\n').map(s => s.trim()).filter(Boolean); const serviceWords = /(Piscina|Desayuno incluido|Wifi|Wi-Fi|Spa|Gimnasio|Parking|Aparcamiento|Restaurante|Bar|Aire acondicionado|Mascotas|Cocina|Lavandería)/i; const seen = new Set(); const vals = []; for (const line of lines) { if (serviceWords.test(line) && line.length <= 60 && !seen.has(line.toLowerCase())) { seen.add(line.toLowerCase()); vals.push(line); } } return vals.slice(0, 8).join(' | ');",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "descripción",
            "selector": "const lines = document.body.innerText.split('\\n').map(s => s.trim()).filter(Boolean); const headings = Array.from(document.querySelectorAll('h2,h3')).map(h => h.textContent.trim()).filter(Boolean); const preferredHeading = headings.find(s => !/Habitación|Galería|Comentarios|Acerca|Echa|Selecciona|Modificar|Alojamientos/i.test(s) && s.length > 15); const longLine = lines.find(s => s.length > 80 && !/cookies|Iniciar sesión|Hoteles\\.com|Expedia|precio|noche/i.test(s)); return preferredHeading || longLine || '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Reembolsa_Sello",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const vals = []; if (/Completamente reembolsable/i.test(text)) vals.push('Completamente reembolsable'); if (/Acumula sellos/i.test(text)) vals.push('Acumula sellos'); return vals.join(' | ');",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "puntuacion",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const m = text.match(/(\\d+(?:[,.]\\d+)?)\\s+de\\s+10/i); return m ? m[1].replace('.', ',') + ' de 10' : '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "n_comentario",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const patterns = [/([\\d\\.]+)\\s+comentarios/i, /([\\d\\.]+)\\s+opiniones/i, /Ver\\s+los\\s+([\\d\\.]+)\\s+comentarios/i]; for (const p of patterns) { const m = text.match(p); if (m) return m[1]; } const rating = text.match(/\\d+(?:[,.]\\d+)?\\s+de\\s+10\\s+([\\d\\.]{2,6})\\b/i); return rating ? rating[1] : '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Precio",
            "selector": "const params = new URLSearchParams(location.search); const top = params.get('top_dp'); const cur = params.get('top_cur') || 'EUR'; if (top) return top + ' ' + (cur === 'EUR' ? '€' : cur); const text = document.body.innerText.replace(/\\s+/g, ' '); const prices = Array.from(text.matchAll(/(\\d{2,5})\\s*€/g)).map(m => m[1] + ' €'); return prices[0] || '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Precio_Original",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const prices = Array.from(text.matchAll(/(\\d{2,5})\\s*€/g)).map(m => m[1]); if (prices.length >= 2) { const nums = prices.map(Number).filter(n => !isNaN(n)); const max = Math.max(...nums); const min = Math.min(...nums); return max > min ? max + ' €' : ''; } return '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Precio_unitario",
            "selector": "const text = document.body.innerText.replace(/\\s+/g, ' '); const m = text.match(/(\\d{2,5}\\s*€\\s*por\\s*noche)/i); if (m) return m[1]; const params = new URLSearchParams(location.search); const top = Number(params.get('top_dp') || ''); const chkin = params.get('chkin'); const chkout = params.get('chkout'); if (top && chkin && chkout) { const nights = Math.max(1, Math.round((new Date(chkout) - new Date(chkin)) / 86400000)); return Math.round(top / nights) + ' € por noche'; } return '';",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Fecha",
            "selector": "const params = new URLSearchParams(location.search); const chkin = params.get('chkin'); const chkout = params.get('chkout'); function fmt(v){ if(!v) return ''; const d = new Date(v + 'T00:00:00'); if(isNaN(d)) return v; return d.toLocaleDateString('es-ES', { day: 'numeric', month: 'short' }).replace('.', ''); } return chkin && chkout ? `${fmt(chkin)} - ${fmt(chkout)}` : '';",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue to the next hotel URL in the input list",
      "position_x": 2640,
      "position_y": 220,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "navigate-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "inject-javascript-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-2",
      "from_connector_id": "right",
      "to_block_id": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-1",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 48,
      "position_y": 116,
      "width": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 768,
      "position_y": 116,
      "width": 1040,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1",
          "inject-javascript-2"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2208,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 2568,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Extracts hotel detail data from es.hoteles.com property URLs with check-in/check-out dates. Navigation strategy: multi-URL input list using navigate.urls[] and loop-continue, appending each hotel detail page into one CSV. The target pages are not paginated detail pages; add more hotel detail URLs to the Navigate block to scrape additional properties.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-navigate-1",
      "element_type": "note",
      "title": "Note: Navigate",
      "content": "Multi-URL loop over 3 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 320,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => {\n  const clickCandidates = [\n    '#onetrust-accept-btn-handler',\n    'button[id*=\"accept\" i]...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1040,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-inject-javascript-2",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `async () => {\n  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n  const steps =...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 200,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-2"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (nombre, URL, n_estrellas, direccion, Servicios_populares). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 200,
      "width": 340,
      "height": 132,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-loop-continue-1",
      "element_type": "note",
      "title": "Note: Loop Continue",
      "content": "Loop Continue advances a multi-URL or multi-text loop. Place at the end of the loop body with a clear back-edge to the loop start.",
      "color": "#ee5396",
      "position_x": 2840,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}