{
  "version": "1.0.0",
  "exported_at": "2026-05-31T23:45:00.000Z",
  "project": {
    "name": "Jalan Hotel Listings Scraper",
    "description": "Scrapes Jalan hotel listing pages for hotel name, detail URL, rating, review count, clean introduction text, access, lowest price, and the first three visible accommodation plans. Pagination is handled with a click-next loop using the detected 'a.next' link so all result pages are appended to one CSV. If Jalan presents CAPTCHA or bot protection, pause and solve manually in the browser.",
    "color": "bg-[#ff832b]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 100,
      "position_y": 200,
      "config": {
        "width": 1920,
        "height": 1080
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 460,
      "position_y": 200,
      "config": {
        "url": "https://www.jalan.net/010000/LRG_010200/?roomCount=1&adultNum=2&distCd=01&child5Num=&mealType=&photo=1&child4Num=&roomCrack=200000&dateUndecided=1&child3Num=&afCd=&child1Num=&child2Num=&childPriceFlg=0,0,0,0,0&rootCd=041",
        "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": 820,
      "position_y": 200,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until hotel result cards appear",
      "position_x": 1180,
      "position_y": 200,
      "config": {
        "selector": ".p-searchResultItem",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export Jalan hotel listings with plan details",
      "position_x": 1540,
      "position_y": 200,
      "config": {
        "rowSelector": ".p-searchResultItem",
        "fileName": "jalan_hotel_listings_scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "URL",
            "selector": "(() => window.location.href)()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "ホテル名",
            "selector": "(() => { const el = ROW.querySelector('.p-searchResultItem__facilityName, .p-yadoCassette__name, .p-yadoCassette__yadoName, .yado-name'); if (el && el.textContent.trim()) return el.textContent.replace(/\\s+/g, ' ').trim(); const links = Array.from(ROW.querySelectorAll('a[href]')); const a = links.find(a => /\\/yad\\d+\\//.test(a.href) && !a.closest('.p-searchResultItem__planNameCell') && a.textContent.trim()) || links.find(a => a.textContent.trim() && !a.closest('.p-searchResultItem__planNameCell')); return a ? a.textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "詳細ページURL",
            "selector": "(() => { const links = Array.from(ROW.querySelectorAll('a[href]')); const direct = links.find(a => /\\/yad\\d+\\//.test(a.href) && !a.closest('.p-searchResultItem__planNameCell')) || links.find(a => /\\/yad\\d+\\//.test(a.href)); if (direct) return direct.href; const idEl = ROW.querySelector('[data-yad-no], [data-yadno], [data-yad-id], input[name=\"yadNo\"], input[name=\"yadoNo\"], input[name=\"yadNoHidden\"]'); let id = ''; if (idEl) id = idEl.getAttribute('data-yad-no') || idEl.getAttribute('data-yadno') || idEl.getAttribute('data-yad-id') || idEl.getAttribute('value') || ''; if (!id) { const html = ROW.innerHTML; const m = html.match(/(?:yadNo|yadoNo|yadCd|yadoCd)[^0-9]{0,30}(\\d{6})/) || html.match(/yad(\\d{6})/) || html.match(/Y(\\d{6})/) || html.match(/yadoId[^0-9]{0,30}(\\d{6})/); id = m ? m[1] : ''; } return id ? `https://www.jalan.net/yad${id}/` : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "星評価",
            "selector": "(() => { const selectors = ['.p-searchResultItem__reviewPoint', '.p-searchResultItem__rating', '.p-yadoCassette__reviewPoint', '.reviewPoint']; for (const s of selectors) { const el = ROW.querySelector(s); if (el) { const m = el.textContent.match(/[0-5]\\.[0-9]/); if (m) return m[0]; } } const txt = ROW.innerText.replace(/\\s+/g, ' '); const m = txt.match(/\\b([0-5]\\.[0-9])\\s*([0-9,]+件)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "評価件数",
            "selector": "(() => { const selectors = ['.p-searchResultItem__reviewCount', '.p-yadoCassette__reviewCount', '.reviewCount']; for (const s of selectors) { const el = ROW.querySelector(s); if (el) { const m = el.textContent.match(/[0-9,]+件/); if (m) return m[0]; } } const txt = ROW.innerText.replace(/\\s+/g, ' '); const m = txt.match(/\\b[0-5]\\.[0-9]\\s*([0-9,]+件)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "紹介",
            "selector": "(() => { let txt = ROW.innerText.replace(/\\s+/g, ' ').trim(); let s = -1; const boilerplate = '表示しております。'; const b = txt.indexOf(boilerplate); if (b >= 0) { s = b + boilerplate.length; } else { const m = txt.match(/[0-5]\\.[0-9]\\s*[0-9,]+件\\s*(?:[^。]{0,80}高評価)?/); if (m) s = m.index + m[0].length; } let seg = s >= 0 ? txt.slice(s).trim() : txt; const endTokens = ['【アクセス】', ' 宿泊プラン ', ' 注目プラン ', ' 加算予定ポイント ', ' MAP ']; let end = seg.length; for (const token of endTokens) { const i = seg.indexOf(token); if (i >= 0 && i < end) end = i; } seg = seg.slice(0, end).trim(); seg = seg.replace(/^実際に宿泊した会員のクチコミ評価から.*?表示しております。\\s*/, '').trim(); seg = seg.replace(/^(?:\\d+(?:分|時間)前に予約されました\\s*)/, '').trim(); return seg.replace(/\\s+/g, ' ').trim(); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "アクセス",
            "selector": "(() => { const txt = ROW.innerText.replace(/\\s+/g, ' '); const m = txt.match(/【アクセス】\\s*(.*?)\\s*MAP/); return m ? m[1].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "最安料金_税込",
            "selector": "(() => { const txt = ROW.innerText.replace(/\\s+/g, ' '); const m = txt.match(/合計\\(税込\\)\\s*([0-9,]+円～)/); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "宿泊プラン1",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); return plans[0] ? plans[0].textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "タグ1",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[0] ? (plans[0].closest('tr') || plans[0].parentElement) : null; if (!row) return ''; const labels = Array.from(row.querySelectorAll('.p-searchResultItem__horizontalLabel, .p-searchResultItem__verticalLabel, .c-label--room, .p-searchResultItem__verticalLabel--meal')).map(e => e.textContent.replace(/\\s+/g, ' ').trim()).filter(Boolean); return Array.from(new Set(labels)).join(' / '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "料金1",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[0] ? (plans[0].closest('tr') || plans[0].parentElement) : null; const el = row ? row.querySelector('.p-searchResultItem__total') : null; return el ? el.textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "宿泊プラン2",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); return plans[1] ? plans[1].textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "タグ2",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[1] ? (plans[1].closest('tr') || plans[1].parentElement) : null; if (!row) return ''; const labels = Array.from(row.querySelectorAll('.p-searchResultItem__horizontalLabel, .p-searchResultItem__verticalLabel, .c-label--room, .p-searchResultItem__verticalLabel--meal')).map(e => e.textContent.replace(/\\s+/g, ' ').trim()).filter(Boolean); return Array.from(new Set(labels)).join(' / '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "料金2",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[1] ? (plans[1].closest('tr') || plans[1].parentElement) : null; const el = row ? row.querySelector('.p-searchResultItem__total') : null; return el ? el.textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "宿泊プラン3",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); return plans[2] ? plans[2].textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "タグ3",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[2] ? (plans[2].closest('tr') || plans[2].parentElement) : null; if (!row) return ''; const labels = Array.from(row.querySelectorAll('.p-searchResultItem__horizontalLabel, .p-searchResultItem__verticalLabel, .c-label--room, .p-searchResultItem__verticalLabel--meal')).map(e => e.textContent.replace(/\\s+/g, ' ').trim()).filter(Boolean); return Array.from(new Set(labels)).join(' / '); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "料金3",
            "selector": "(() => { const plans = Array.from(ROW.querySelectorAll('.p-searchResultItem__planName')); const row = plans[2] ? (plans[2].closest('tr') || plans[2].parentElement) : null; const el = row ? row.querySelector('.p-searchResultItem__total') : null; return el ? el.textContent.replace(/\\s+/g, ' ').trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if a next page link exists",
      "position_x": 1900,
      "position_y": 200,
      "config": {
        "selector": "a.next"
      }
    },
    {
      "block_id": "click-1",
      "block_type": "process",
      "title": "Click",
      "description": "Click the next page link",
      "position_x": 2260,
      "position_y": 520,
      "config": {
        "selector": "a.next",
        "timeout": 10
      }
    },
    {
      "block_id": "wait-for-page-load-2",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait after next page navigation",
      "position_x": 2620,
      "position_y": 520,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Allow Jalan dynamic content to settle",
      "position_x": 2980,
      "position_y": 520,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "wait-for-element-2",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait for hotel result cards on the next page",
      "position_x": 3340,
      "position_y": 520,
      "config": {
        "selector": ".p-searchResultItem",
        "timeout": 30,
        "visible": true
      }
    },
    {
      "block_id": "end-1",
      "block_type": "output",
      "title": "End",
      "description": "Terminate execution flow",
      "position_x": 1900,
      "position_y": 840,
      "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": "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-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-1",
      "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-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 28,
      "position_y": 96,
      "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": 388,
      "position_y": 96,
      "width": 3200,
      "height": 616,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1",
          "wait-for-page-load-2",
          "sleep-1",
          "wait-for-element-2"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 1468,
      "position_y": 96,
      "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": 1828,
      "position_y": 96,
      "width": 680,
      "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": 1828,
      "position_y": 736,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "end-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Scrapes Jalan hotel listing pages for hotel name, detail URL, rating, review count, clean introduction text, access, lowest price, and the first three visible accommodation plans. Pagination is handled with a click-next loop using the detected 'a.next' link so all result pages are appended to one CSV. If Jalan presents CAPTCHA or bot protection, pause and solve manually in the browser.",
      "color": "#f1c21b",
      "position_x": 80,
      "position_y": 20,
      "width": 480,
      "height": 160,
      "z_index": 22,
      "data": {}
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (URL, ホテル名, 詳細ページURL, 星評価, 評価件数). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 1740,
      "position_y": 180,
      "width": 340,
      "height": 124,
      "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.next`. True / False branches control which path runs next. Keep enough space between branches so both connector lines are visible.",
      "color": "#ee5396",
      "position_x": 2100,
      "position_y": 180,
      "width": 340,
      "height": 132,
      "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": 500,
      "width": 316,
      "height": 106,
      "z_index": 22,
      "data": {
        "block_id": "click-1"
      }
    }
  ]
}