{
  "version": "1.0.0",
  "exported_at": "2026-06-02T02:20:00.000Z",
  "project": {
    "name": "Lamudi Post Details Scraper",
    "description": "Scrapes Lamudi Mexico property detail pages by URL, extracting title, price, address, technical specifications, description, amenities, up to five image URLs, page URL, publication date, and scrape timestamp. Navigation uses a multi-URL loop: add all target Lamudi detail URLs to the navigate.urls array; each successful page is appended to one CSV. Lamudi may show an anti-bot page with the text 'Let's confirm you are human'; this template detects that challenge and skips extraction for blocked pages to avoid writing invalid rows.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://www.lamudi.com.mx/detalle/41032-73-c184fc1ac04f-8b77-4e258a79-bd73-46ca",
          "https://www.lamudi.com.mx/detalle/41032-73-e4e47f9a0eca-81d1-6557ce60-bf43-46d3"
        ],
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 456,
      "position_y": 220,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 792,
      "position_y": 220,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "text-contains-1",
      "block_type": "process",
      "title": "Text Contains",
      "description": "Check if page contains text",
      "position_x": 1128,
      "position_y": 220,
      "config": {
        "text": "Let's confirm you are human",
        "selector": "body",
        "caseSensitive": false
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Run custom JavaScript",
      "position_x": 1128,
      "position_y": 520,
      "config": {
        "jsCode": "window.scrollTo(0, document.body.scrollHeight);\nwindow.__US_LAMUDI_DETAIL__ = (() => {\n  const norm = (v) => (v == null ? '' : String(v)).replace(/\\s+/g, ' ').trim();\n  const bodyText = () => norm(document.body ? document.body.innerText : '');\n  const meta = (name) => norm(document.querySelector(`meta[property=\"${name}\"], meta[name=\"${name}\"]`)?.content || '');\n  const qText = (selectors) => {\n    for (const sel of selectors) {\n      const el = document.querySelector(sel);\n      const txt = norm(el?.innerText || el?.textContent || el?.content || '');\n      if (txt) return txt;\n    }\n    return '';\n  };\n  const parseJsonLd = () => {\n    const out = [];\n    document.querySelectorAll('script[type=\"application/ld+json\"]').forEach((s) => {\n      try {\n        const parsed = JSON.parse(s.textContent || '{}');\n        if (Array.isArray(parsed)) out.push(...parsed); else out.push(parsed);\n      } catch (_) {}\n    });\n    return out;\n  };\n  const findKey = (wanted) => {\n    const seen = new Set();\n    const walk = (x) => {\n      if (!x || seen.has(x)) return '';\n      if (typeof x === 'object') seen.add(x);\n      if (Array.isArray(x)) {\n        for (const item of x) {\n          const got = walk(item);\n          if (got) return got;\n        }\n        return '';\n      }\n      if (typeof x !== 'object') return '';\n      for (const [k, v] of Object.entries(x)) {\n        if (wanted.includes(String(k).toLowerCase())) {\n          if (typeof v === 'string' || typeof v === 'number') return norm(v);\n          if (v && typeof v === 'object') {\n            if (v.streetAddress || v.addressLocality || v.addressRegion || v.postalCode) {\n              return norm([v.streetAddress, v.addressLocality, v.addressRegion, v.postalCode].filter(Boolean).join(', '));\n            }\n            const compact = norm(Object.values(v).filter((z) => typeof z === 'string' || typeof z === 'number').join(', '));\n            if (compact) return compact;\n          }\n        }\n      }\n      for (const v of Object.values(x)) {\n        const got = walk(v);\n        if (got) return got;\n      }\n      return '';\n    };\n    for (const root of parseJsonLd()) {\n      const got = walk(root);\n      if (got) return got;\n    }\n    return '';\n  };\n  const allImages = () => {\n    const set = new Set();\n    const addUrl = (u) => {\n      u = norm(u);\n      if (!u) return;\n      if (u.startsWith('//')) u = location.protocol + u;\n      if (u.startsWith('/')) u = location.origin + u;\n      if (/lamudi|lifull|jpg|jpeg|png|webp/i.test(u)) set.add(u);\n    };\n    document.querySelectorAll('img').forEach((img) => {\n      addUrl(img.currentSrc || img.src || img.getAttribute('data-src') || img.getAttribute('data-lazy-src'));\n      const srcset = img.getAttribute('srcset') || img.getAttribute('data-srcset') || '';\n      srcset.split(',').forEach((part) => addUrl(part.trim().split(/\\s+/)[0]));\n    });\n    document.querySelectorAll('source').forEach((source) => {\n      const srcset = source.getAttribute('srcset') || '';\n      srcset.split(',').forEach((part) => addUrl(part.trim().split(/\\s+/)[0]));\n    });\n    document.querySelectorAll('*').forEach((el) => {\n      const bg = getComputedStyle(el).backgroundImage || '';\n      const matches = bg.match(/url\\([\"']?([^\"')]+)[\"']?\\)/g) || [];\n      matches.forEach((m) => addUrl(m.replace(/^url\\([\"']?/, '').replace(/[\"']?\\)$/, '')));\n    });\n    return Array.from(set).filter((u) => !/logo|icon|sprite/i.test(u));\n  };\n  const spec = (patterns) => {\n    const txt = bodyText();\n    for (const p of patterns) {\n      const m = txt.match(p);\n      if (m) return norm(m[0]);\n    }\n    return '';\n  };\n  const amenities = () => {\n    const keywords = /garaje|cocina|agua|gas|electricidad|internet|muebles|dep[oó]sito|televisi[oó]n|armario|servicio|alberca|jard[ií]n|seguridad|terraza|balc[oó]n|elevador|mascotas|gimnasio/i;\n    const vals = [];\n    document.querySelectorAll('li, span, p, div, button').forEach((el) => {\n      const t = norm(el.innerText || el.textContent || '');\n      if (t && t.length <= 60 && keywords.test(t) && !vals.includes(t)) vals.push(t);\n    });\n    return vals.slice(0, 40).join('\\n');\n  };\n  const description = () => {\n    const bySelector = qText(['[data-testid*=\"description\" i]', '[class*=\"description\" i]', '[id*=\"description\" i]', 'section[aria-label*=\"description\" i]']);\n    if (bySelector && bySelector.length > 40) return bySelector;\n    const json = findKey(['description']);\n    if (json && json.length > 40) return json;\n    return meta('description');\n  };\n  const price = () => {\n    const bySelector = qText(['[data-testid*=\"price\" i]', '[class*=\"price\" i]', '[id*=\"price\" i]']);\n    if (bySelector && /\\$/.test(bySelector)) return bySelector;\n    const txt = bodyText();\n    const m = txt.match(/\\$\\s*[\\d.,]+\\s*(?:MXN)?(?:\\s*Gastos de comunidad\\s*\\$?\\s*[\\d.,]+\\s*MXN)?/i);\n    return m ? norm(m[0]) : findKey(['price']);\n  };\n  const address = () => {\n    const bySelector = qText(['[data-testid*=\"address\" i]', '[class*=\"address\" i]', '[id*=\"address\" i]', '[data-testid*=\"location\" i]', '[class*=\"location\" i]']);\n    if (bySelector && bySelector.length > 5) return bySelector;\n    return findKey(['address', 'streetaddress', 'addresslocality', 'addressregion']) || meta('og:street-address');\n  };\n  const title = () => qText(['h1', '[data-testid*=\"title\" i]', '[class*=\"title\" i]']) || findKey(['name', 'headline', 'title']) || meta('og:title') || document.title;\n  const published = () => qText(['time[datetime]', 'time', '[data-testid*=\"date\" i]', '[class*=\"date\" i]']) || document.querySelector('time')?.getAttribute('datetime') || findKey(['datepublished', 'dateposted', 'uploaddate']);\n  return {\n    get: (field) => {\n      const imgs = allImages();\n      const map = {\n        titulo: title(),\n        precio: price(),\n        direccion: address(),\n        recamaras: spec([/\\d+\\s*rec[aá]maras?/i, /\\d+\\s*habitaciones?/i]),\n        banos: spec([/\\d+\\s*bañ(?:o|os)/i, /\\d+\\s*ban(?:o|os)/i]),\n        construidos: spec([/\\d+[\\d.,]*\\s*m²/i, /\\d+[\\d.,]*\\s*m2/i, /\\d+[\\d.,]*\\s*metros cuadrados/i]),\n        descripcion: description(),\n        amenidades: amenities(),\n        imagen1_url: imgs[0] || '',\n        imagen2_url: imgs[1] || '',\n        imagen3_url: imgs[2] || '',\n        imagen4_url: imgs[3] || '',\n        imagen5_url: imgs[4] || '',\n        pagina_url: location.href,\n        fecha_publicacion: published(),\n        hora_actual: new Date().toISOString()\n      };\n      return map[field] || '';\n    }\n  };\n})();",
        "waitForCompletion": true,
        "timeout": 10
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1632,
      "position_y": 800,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1968,
      "position_y": 800,
      "config": {
        "rowSelector": "body",
        "fileName": "lamudi-detalles-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "titulo",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('titulo') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "precio",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('precio') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "direccion",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('direccion') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "recamaras",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('recamaras') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "banos",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('banos') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "construidos",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('construidos') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "descripcion",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('descripcion') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "amenidades",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('amenidades') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "imagen1_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('imagen1_url') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "imagen2_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('imagen2_url') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "imagen3_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('imagen3_url') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "imagen4_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('imagen4_url') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "imagen5_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('imagen5_url') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "pagina_url",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('pagina_url') : location.href",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "fecha_publicacion",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('fecha_publicacion') : ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "hora_actual",
            "selector": "window.__US_LAMUDI_DETAIL__ ? window.__US_LAMUDI_DETAIL__.get('hora_actual') : new Date().toISOString()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 1464,
      "position_y": 520,
      "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": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "text-contains-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "text-contains-1",
      "from_connector_id": "true",
      "to_block_id": "loop-continue-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "text-contains-1",
      "from_connector_id": "false",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-1",
      "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": 1832,
      "height": 876,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "sleep-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1056,
      "position_y": 116,
      "width": 656,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "text-contains-1",
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1056,
      "position_y": 416,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "inject-javascript-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 1896,
      "position_y": 696,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Lamudi Mexico property detail pages by URL, extracting title, price, address, technical specifications, description, amenities, up to five image URLs, page URL, publication date, and scrape timestamp. Navigation uses a multi-URL loop: add all target Lamudi detail URLs to the navigate.urls array; each successful page is appended to one CSV. Lamudi may show an anti-bot page with the text 'Let's confirm you are human'; this template detects that challenge and skips extraction for blocked pages to avoid writing invalid rows.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-text-contains-1",
      "element_type": "note",
      "title": "Note: Text Contains",
      "content": "Condition block: checks `body`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 1328,
      "position_y": 200,
      "width": 340,
      "height": 131,
      "z_index": 22,
      "data": {
        "block_id": "text-contains-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `window.scrollTo(0, document.body.scrollHeight);\nwindow.__US_LAMUDI_DETAIL__ = (() => {\n  const norm ...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1328,
      "position_y": 500,
      "width": 340,
      "height": 140,
      "z_index": 22,
      "data": {
        "block_id": "inject-javascript-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (titulo, precio, direccion, recamaras, banos). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2168,
      "position_y": 780,
      "width": 340,
      "height": 128,
      "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": 1664,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}