{
  "version": "1.0.0",
  "exported_at": "2026-06-01T00:00:00.000Z",
  "project": {
    "name": "Airbnb Hotel Details Scraper",
    "description": "Best-effort Airbnb accommodation scraper for Japan keyword search. Extracts publicly visible listing details such as listing title/name, displayed property type/location, price, rating, canonical listing URL, listing image URL, room/property summary, and visible badges. Uses click-next pagination to scrape all available Airbnb result pages until no enabled Next button remains. The row selector is restricted to Airbnb itemListElement wrappers to avoid duplicate rows. Airbnb may block scraping with CAPTCHA/login/bot checks, and exact street addresses are usually not exposed in search results.",
    "color": "bg-[#ff5a5f]",
    "template_id": "ai-generated-airbnb-hotel-details"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 100,
      "position_y": 220,
      "config": {
        "url": "https://www.airbnb.com/s/Japan/homes?query=Japan&adults=1",
        "color": "bg-[#ff5a5f]",
        "tags": [
          "airbnb",
          "search",
          "japan"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 460,
      "position_y": 220,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 820,
      "position_y": 220,
      "config": {
        "jsCode": "(() => { const acceptedTexts = ['Accept all', 'Accept', 'I agree', 'OK', 'Got it', 'Allow all']; const buttons = Array.from(document.querySelectorAll('button')); const button = buttons.find((btn) => { const text = (btn.textContent || '').trim(); return acceptedTexts.some((accepted) => text.toLowerCase().includes(accepted.toLowerCase())); }); if (button) { button.click(); return 'Clicked cookie/consent button: ' + button.textContent.trim(); } return 'No cookie/consent button found'; })();",
        "waitForCompletion": true,
        "timeout": 10
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1180,
      "position_y": 220,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1540,
      "position_y": 220,
      "config": {
        "selector": "div[itemprop=\"itemListElement\"]",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1900,
      "position_y": 220,
      "config": {
        "rowSelector": "div[itemprop=\"itemListElement\"]",
        "fileName": "airbnb-jp-hotel-details-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "listing_title",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const explicitName = clean((ROW.querySelector('[data-testid=\"listing-card-name\"]') || ROW.querySelector('[aria-labelledby] [id^=\"title_\"]'))?.textContent); if (explicitName) return explicitName; const loc = clean((ROW.querySelector('[data-testid=\"listing-card-title\"]') || ROW.querySelector('[id^=\"title_\"]') || ROW.querySelector('meta[itemprop=\"name\"]'))?.textContent || ROW.querySelector('meta[itemprop=\"name\"]')?.getAttribute('content')); let text = clean(ROW.innerText || ROW.textContent); text = text.replace(/^(Top guest favorite|Guest favorite|Superhost|Rare find)+/i, '').trim(); if (loc && text.startsWith(loc)) text = text.slice(loc.length).trim(); const stop = text.search(/\\d+\\s*(bedrooms?|beds?|futons?|baths?|guests?)|\\b(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\b|[$€£¥]|Free cancellation|Book early|\\d\\.\\d/i); const parsed = clean(stop > 0 ? text.slice(0, stop) : text); return parsed || loc; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "property_type_location",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); return clean((ROW.querySelector('[data-testid=\"listing-card-title\"]') || ROW.querySelector('[id^=\"title_\"]') || ROW.querySelector('meta[itemprop=\"name\"]'))?.textContent || ROW.querySelector('meta[itemprop=\"name\"]')?.getAttribute('content') || ''); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "price",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const priceNode = ROW.querySelector('[data-testid=\"price-availability-row\"]'); const text = clean((priceNode && priceNode.textContent) || ROW.innerText || ROW.textContent); const prices = Array.from(text.matchAll(/[$€£¥]\\s?[\\d,]+/g)).map(m => m[0].replace(/\\s+/g, '')); const uniquePrices = [...new Set(prices)]; const nights = text.match(/for\\s+\\d+\\s+nights?/i); if (uniquePrices.length) return clean(uniquePrices.slice(0, 2).join(' ') + (nights ? ' ' + nights[0] : '')); return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "rating",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const text = clean(ROW.innerText || ROW.textContent); let m = text.match(/(\\d(?:\\.\\d{1,2})?)\\s*out of 5 average rating,?\\s*([\\d,]+)\\s*reviews?/i); if (m) return `${m[1]} (${m[2]} reviews)`; m = text.match(/(\\d(?:\\.\\d{1,2})?)\\s*\\(([\\d,]+)\\)/); if (m) return `${m[1]} (${m[2]} reviews)`; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "listing_url",
            "selector": "(() => { const a = ROW.querySelector('a[href*=\"/rooms/\"]'); if (a && a.href) { const u = new URL(a.href, location.origin); return u.origin + u.pathname; } const metaUrl = ROW.querySelector('meta[itemprop=\"url\"]')?.getAttribute('content') || ''; if (!metaUrl) return ''; const u = new URL(metaUrl, location.origin); return u.origin + u.pathname; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "image_url",
            "selector": "(() => { const imgs = Array.from(ROW.querySelectorAll('img')); const img = imgs.find(i => { const src = i.currentSrc || i.src || ''; return src && src.includes('muscache.com') && !src.includes('airbnb-platform-assets') && !src.includes('AirbnbPlatformAssets') && !src.includes('GuestFavorite'); }); return img ? (img.currentSrc || img.src || '') : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "room_or_property_summary",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const text = clean(ROW.innerText || ROW.textContent); const found = []; const re = /(\\d+\\s*(?:guests?|bedrooms?|beds?|futons?|baths?)|studio|private room|entire home|hotel room|shared room)/gi; let m; while ((m = re.exec(text)) !== null) { const val = clean(m[1]); if (val && !found.some(x => x.toLowerCase() === val.toLowerCase())) found.push(val); } return found.slice(0, 8).join('; '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "host_or_badge",
            "selector": "(() => { const clean = s => (s || '').replace(/\\s+/g, ' ').trim(); const text = clean(ROW.innerText || ROW.textContent); const badges = []; ['Guest favorite', 'Top guest favorite', 'Superhost', 'Rare find', 'Book early to save', 'Free cancellation'].forEach(b => { if (new RegExp(b, 'i').test(text) && !badges.includes(b)) badges.push(b); }); return badges.join('; '); })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 2260,
      "position_y": 220,
      "config": {
        "selector": "a[aria-label=\"Next\"]:not([aria-disabled=\"true\"]), button[aria-label=\"Next\"]:not(:disabled):not([aria-disabled=\"true\"])"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click on element",
      "position_x": 2260,
      "position_y": 540,
      "config": {
        "selector": "a[aria-label=\"Next\"]:not([aria-disabled=\"true\"]), button[aria-label=\"Next\"]:not(:disabled):not([aria-disabled=\"true\"])",
        "timeout": 15
      }
    },
    {
      "block_id": "wait-for-page-load-2",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 2620,
      "position_y": 540,
      "config": {
        "timeout": 45
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 2980,
      "position_y": 540,
      "config": {
        "duration": 4
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 3340,
      "position_y": 540,
      "config": {
        "selector": "div[itemprop=\"itemListElement\"]",
        "timeout": 45,
        "visible": true
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 2260,
      "position_y": 860,
      "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": "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": "structured-export-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-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": "end-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "click-1",
      "from_connector_id": "right",
      "to_block_id": "wait-for-page-load-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-page-load-2",
      "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": "wait-for-element-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "wait-for-element-2",
      "from_connector_id": "right",
      "to_block_id": "structured-export-1",
      "to_connector_id": "left"
    }
  ],
  "canvas_elements": [
    {
      "id": "group-load",
      "element_type": "group",
      "title": "Page Load",
      "color": "#08bdba",
      "position_x": 28,
      "position_y": 116,
      "width": 3560,
      "height": 616,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1",
          "wait-for-page-load-2",
          "sleep-2",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 748,
      "position_y": 116,
      "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": 1828,
      "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": 2188,
      "position_y": 116,
      "width": 380,
      "height": 616,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "click-1"
        ]
      }
    },
    {
      "id": "group-control",
      "element_type": "group",
      "title": "Control Flow",
      "color": "#8d8d8d",
      "position_x": 2188,
      "position_y": 756,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Airbnb accommodation scraper for Japan keyword search. Extracts publicly visible listing details such as listing title/name, displayed property type/location, price, rating, canonical listing URL, listing image URL, room/property summary, and visible badges. Uses click-next pagination to scrape all available Airbnb result pages until no enabled Next button remains. The row selector is restricted to Airbnb itemListElement wrappers to avoid duplicate rows. Airbnb may block scraping with CAPTCHA/login/bot checks, and exact street addresses are usually not exposed in search results.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `(() => { const acceptedTexts = ['Accept all', 'Accept', 'I agree', 'OK', 'Got it', 'Allow all']; con...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1020,
      "position_y": 200,
      "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 (listing_title, property_type_location, price, rating, listing_url). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2100,
      "position_y": 200,
      "width": 340,
      "height": 135,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-1"
      }
    },
    {
      "id": "note-block-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `a[aria-label=\"Next\"]:not([aria-disabled=\"true\"]), button[aria-label=\"Next\"]:not(:disabled):not([aria-disabled=\"true\"])`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 2460,
      "position_y": 200,
      "width": 340,
      "height": 169,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-click-1",
      "element_type": "note",
      "title": "Note: Click",
      "content": "Pagination click — add waits after this block; the page reloads asynchronously.",
      "color": "#ee5396",
      "position_x": 2460,
      "position_y": 520,
      "width": 316,
      "height": 106,
      "z_index": 22,
      "data": {
        "block_id": "click-1"
      }
    }
  ]
}