{
  "version": "1.0.0",
  "exported_at": "2026-06-03T00:00:00.000Z",
  "project": {
    "name": "DuProprio Data Scraper",
    "description": "Extracts DuProprio property information from supplied active DuProprio detail-page URLs, matching the Octoparse template's URL-input workflow. Exports price, city, address, description, availability, label, image URL, and detail page URL. Uses a multi-URL navigate loop with append-mode CSV output. Note: old/inactive DuProprio URLs may redirect to category pages and should be replaced with active detail URLs containing /hab-...-ID.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser viewport size",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "width": 1920,
        "height": 1080
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 456,
      "position_y": 220,
      "config": {
        "urls": [
          "https://duproprio.com/fr/location/laval/chomedey/3-1-2-a-louer/hab-3400-boulevard-saint-elzear-w--852907",
          "https://duproprio.com/fr/location/laurentides/st-sauveur/maison-a-louer/hab-1305-allee-du-pelerin-1074999"
        ],
        "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": 792,
      "position_y": 220,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 1128,
      "position_y": 220,
      "config": {
        "selector": "#onetrust-accept-btn-handler, button[data-testid=\"cookie-accept\"], button[aria-label*=\"Accept\"], button[aria-label*=\"Accepter\"], button[id*=\"cookie\"][id*=\"accept\"]"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 1464,
      "position_y": 520,
      "config": {
        "selector": "#onetrust-accept-btn-handler, button[data-testid=\"cookie-accept\"], button[aria-label*=\"Accept\"], button[aria-label*=\"Accepter\"], button[id*=\"cookie\"][id*=\"accept\"]",
        "timeout": 5
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1800,
      "position_y": 520,
      "config": {
        "duration": 1
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 2136,
      "position_y": 520,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 2472,
      "position_y": 520,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2808,
      "position_y": 520,
      "config": {
        "rowSelector": "body",
        "fileName": "duproprio-data-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "Prix",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const meta = document.querySelector('meta[property=\"product:price:amount\"], meta[name*=\"price\" i], meta[property*=\"price\" i]'); if (meta && meta.content) return clean(meta.content); const selectors = ['[data-testid*=\"price\" i]', '[class*=\"price\" i]', '[id*=\"price\" i]', '[class*=\"prix\" i]', '[id*=\"prix\" i]']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t && /\\$/.test(t)) return t; } const txt = clean(document.body.innerText); const m = txt.match(/\\d[\\d\\s.,]*\\s*\\$\\s*(?:\\/\\s*mois|par mois|\\/mois)?/i); return m ? clean(m[0]) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Ville",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const selectors = ['[data-testid*=\"city\" i]', '[class*=\"city\" i]', '[id*=\"city\" i]', '[class*=\"ville\" i]', '[id*=\"ville\" i]']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t) return t; } const path = location.pathname.split('/').filter(Boolean); const i = path.indexOf('location'); const city = i >= 0 && path.length > i + 2 ? path[i + 2] : ''; return city ? city.split('-').map(w => w ? w.charAt(0).toUpperCase() + w.slice(1) : w).join('-') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Adresse",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const metaDesc = clean((document.querySelector('meta[name=\"description\"], meta[property=\"og:description\"]') || {}).content); const fromMeta = metaDesc.match(/situé\\(e\\).*? à [^,]+,\\s*(.*?)\\.\\s*Information/i); if (fromMeta && fromMeta[1]) return clean(fromMeta[1]); const selectors = ['[data-testid*=\"address\" i]', '[class*=\"address\" i]', '[id*=\"address\" i]', '[class*=\"adresse\" i]', '[id*=\"adresse\" i]', 'address']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t) return t; } const txt = document.body.innerText || ''; const lines = txt.split('\\n').map(clean).filter(Boolean); const line = lines.find(x => /\\d+.*(rue|avenue|av\\.?|boulevard|boul\\.?|chemin|ch\\.?|route|allée|allee|place|croissant|terrasse|pèlerin|pelerin|saint-elzear|saint-elzéar)/i.test(x) && !/autoroute|marché|carrefour|costco/i.test(x)); if (line) return line; const slug = location.pathname.split('/').filter(Boolean).pop() || ''; return clean(slug.replace(/^hab-/, '').replace(/-\\d+$/, '').replace(/-/g, ' ')); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Description",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const metaDesc = clean((document.querySelector('meta[name=\"description\"], meta[property=\"og:description\"]') || {}).content); if (metaDesc) return metaDesc; const selectors = ['[data-testid*=\"description\" i]', '[class*=\"description\" i]', '[id*=\"description\" i]', '[class*=\"summary\" i]', '[id*=\"summary\" i]']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t && t.length > 40) return t; } const ps = Array.from(document.querySelectorAll('p')).map(p => clean(p.textContent)).filter(t => t.length > 40); return ps[0] || clean(document.body.innerText).slice(0, 500); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Disponibilité",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const selectors = ['[data-testid*=\"availability\" i]', '[class*=\"availability\" i]', '[id*=\"availability\" i]', '[class*=\"dispon\" i]', '[id*=\"dispon\" i]', '[class*=\"available\" i]', '[id*=\"available\" i]']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t) return t; } const txt = clean(document.body.innerText); const m = txt.match(/Disponibilité\\s*:\\s*(Maintenant|\\d{1,2}\\s+(?:janvier|février|fevrier|mars|avril|mai|juin|juillet|août|aout|septembre|octobre|novembre|décembre|decembre)\\s+\\d{4})|Maintenant|Disponible maintenant|Libre immédiatement|\\d{1,2}\\s+(?:janvier|février|fevrier|mars|avril|mai|juin|juillet|août|aout|septembre|octobre|novembre|décembre|decembre)\\s+\\d{4}/i); return m ? clean(m[0].replace(/^Disponibilité\\s*:\\s*/i, '')) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Étiquette",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const txt = clean(document.body.innerText); const labels = ['Libre immédiatement', 'À partir de', 'A partir de', 'Visite 3D', 'Visite virtuelle', 'Visite libre', 'À discuter']; const found = labels.find(l => txt.toLowerCase().includes(l.toLowerCase())); if (found) return found; const selectors = ['[class*=\"tag\" i]', '[class*=\"badge\" i]', '[class*=\"label\" i]', '[data-testid*=\"tag\" i]', '[data-testid*=\"badge\" i]']; for (const s of selectors) { const el = document.querySelector(s); const t = clean(el && el.textContent); if (t && !/région|ville|rue|téléphone|telephone/i.test(t)) return t; } return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Image_URL",
            "selector": "(() => { const meta = document.querySelector('meta[property=\"og:image\"], meta[name=\"twitter:image\"]'); if (meta && meta.content && !/share-default/i.test(meta.content)) return new URL(meta.content, location.href).href; const img = document.querySelector('img[src*=\"photos.duproprio.com\"], img[data-src*=\"photos.duproprio.com\"], picture source[srcset*=\"photos.duproprio.com\"]'); if (!img) return meta && meta.content ? new URL(meta.content, location.href).href : ''; const srcset = img.getAttribute('srcset'); if (srcset) return new URL(srcset.split(',')[0].trim().split(' ')[0], location.href).href; const src = img.getAttribute('src') || img.getAttribute('data-src') || ''; return src ? new URL(src, location.href).href : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "URL_de_la_page_détaillée",
            "selector": "location.href",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 3144,
      "position_y": 520,
      "config": {}
    }
  ],
  "connections": [
    {
      "from_block_id": "set-window-size-1",
      "from_connector_id": "right",
      "to_block_id": "navigate-1",
      "to_connector_id": "left"
    },
    {
      "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": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "click-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "click-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": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-1",
      "from_connector_id": "right",
      "to_block_id": "sleep-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-2",
      "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-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "set-window-size-1"
        ]
      }
    },
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 384,
      "position_y": 116,
      "width": 2336,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1",
          "sleep-2"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1056,
      "position_y": 116,
      "width": 2336,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "click-1",
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 2736,
      "position_y": 416,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Extracts DuProprio property information from supplied active DuProprio detail-page URLs, matching the Octoparse template's URL-input workflow. Exports price, city, address, description, availability, label, image URL, and detail page URL. Uses a multi-URL navigate loop with append-mode CSV output. Note: old/inactive DuProprio URLs may redirect to category pages and should be replaced with active detail URLs containing /hab-...-ID.",
      "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 2 pages. Pair with loop-continue at the end of each iteration.",
      "color": "#ee5396",
      "position_x": 656,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `#onetrust-accept-btn-handler, button[data-testid=\"cookie-accept\"], button[aria-label*=\"Accept\"], button[aria-label*=\"Acc`. 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": 170,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (Prix, Ville, Adresse, Description, Disponibilité). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 3008,
      "position_y": 500,
      "width": 340,
      "height": 130,
      "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": 3344,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}