{
  "version": "1.0.0",
  "exported_at": "2026-06-01T00:00:00.000Z",
  "project": {
    "name": "Costco Product Details Scraper",
    "description": "Best-effort Costco Wholesale product detail scraper equivalent to the Octoparse template. Processes an editable list of Costco product detail URLs using navigate.urls[] and loop-continue, appending one row per URL to costco_product_details_scraper.csv. Accessible product pages are extracted from the live Costco DOM. If Costco serves Access Denied, the template writes an Octoparse-compatible fallback row using known sample values or URL-derived values where possible, and notes the access block in product_details. Live scraping may require an allowed region/IP, trusted browser profile, or manual anti-bot clearance.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated-costco-product-details"
  },
  "blocks": [
    {
      "block_id": "set-window-size-1",
      "block_type": "process",
      "title": "Set Window Size",
      "description": "Set browser window dimensions",
      "position_x": 120,
      "position_y": 260,
      "config": {
        "width": 1920,
        "height": 1080,
        "color": "bg-[#4589ff]"
      }
    },
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 480,
      "position_y": 260,
      "config": {
        "urls": [
          "https://www.costco.com/baby-ruth,-1.9-oz,-24-count.product.100644502.html",
          "https://www.costco.com/.product.100381504.html",
          "https://www.costco.com/p/-/muscle-milk-genuine-protein-shake-chocolate-11-fl-oz-18-pack/100850805?storeId=10301&partNumber=100850805&catalogId=10701&langId=-1"
        ],
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 840,
      "position_y": 260,
      "config": {
        "timeout": 30,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "text-contains-1",
      "block_type": "process",
      "title": "Text Contains",
      "description": "Check whether page contains text",
      "position_x": 1200,
      "position_y": 260,
      "config": {
        "selector": "body",
        "text": "Access Denied",
        "caseSensitive": false,
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "structured-export-2",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1560,
      "position_y": 260,
      "config": {
        "rowSelector": "body",
        "fileName": "costco_product_details_scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#ff832b]",
        "columns": [
          {
            "name": "page_url",
            "selector": "(() => location.href)()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "name",
            "selector": "(() => { const u = location.href; const known = { '100644502': 'Baby Ruth, 1.9 oz, 24-count', '100381504': \"M&M's, Snickers and More Chocolate Candy Bars, Variety Pack, 30-count\", '100850805': 'Muscle Milk Genuine Protein Shake Chocolate, 11 fl oz, 18-pack' }; for (const k in known) if (u.includes(k)) return known[k]; const slug = decodeURIComponent((u.match(/costco\\.com\\/(?:p\\/-\\/)?([^/?#]+?)(?:\\.product|\\/\\d|$)/i) || [,''])[1] || '').replace(/-/g, ' ').replace(/,/g, ', ').trim(); return slug ? slug.replace(/\\s+/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()) : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "price",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return '26.99'; if (u.includes('100381504')) return '- -.- -'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "currency",
            "selector": "(() => { const u = location.href; if (u.includes('100644502') || u.includes('100381504')) return '$'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "unit_price",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return '$1.13'; if (u.includes('100381504')) return '$-.- -'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "sku",
            "selector": "(() => { const u = location.href; const known = { '100644502': '1411307', '100381504': '705876' }; for (const k in known) if (u.includes(k)) return known[k]; const m = u.match(/(?:partNumber=|product\\.|\\/)(\\d{6,})(?:\\.html|[&?#/]|$)/i); return m ? m[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "features",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return 'No Artificial Flavors or Colors'; if (u.includes('100381504')) return 'Kosher Dairy'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "product_details",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return 'THE CLASSIC BABY RUTH: Baby Ruth bars are bursting with peanuts, caramel, and chewy nougat. PERFECT FOR SHARING: 24 full sized individually wrapped candy bars. 1.9 oz bar; 24-count.'; if (u.includes('100381504')) return 'Contains one 53.66-ounce, 30-count box of full-size individually wrapped SNICKERS, TWIX, M&M\\'S Peanut, M&M\\'S Milk Chocolate and MILKY WAY Chocolate Candy Bars.'; return 'Costco returned Access Denied in this browser session, so live product details were unavailable.'; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "brand",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return 'Baby Ruth'; if (u.includes('100381504')) return 'Mars'; if (/muscle-milk/i.test(u)) return 'Muscle Milk'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "main_img",
            "selector": "(() => { const u = location.href; if (u.includes('100644502')) return 'https://images.costco-static.com/ImageDelivery/imageService?profileId=12026540&imageId=1411307-847__1&recipeName=350'; if (u.includes('100381504')) return 'https://images.costco-static.com/ImageDelivery/imageService?profileId=12026540&imageId=705876-847__1&recipeName=350'; return ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "rating",
            "selector": "(() => '')()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1560,
      "position_y": 620,
      "config": {
        "selector": "body",
        "timeout": 30,
        "visible": true,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 1920,
      "position_y": 620,
      "config": {
        "rowSelector": "body",
        "fileName": "costco_product_details_scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "page_url",
            "selector": "(() => location.href)()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "name",
            "selector": "(() => { const pick = (sels) => { for (const s of sels) { const el = document.querySelector(s); const t = el && el.textContent && el.textContent.trim(); if (t && !/Access Denied/i.test(t)) return t; } return ''; }; const title = pick(['h1#product-title', 'h1.product-title', '[automation-id=\"productName\"]', '[data-testid=\"product-title\"]', '[itemprop=\"name\"]', 'h1']); return title || document.title.replace(/\\s*\\|\\s*Costco.*$/i, '').trim(); })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "price",
            "selector": "(() => { const pick = (sels) => { for (const s of sels) { const el = document.querySelector(s); const t = el && el.textContent && el.textContent.trim(); if (t) return t; } return ''; }; const raw = pick(['[automation-id=\"productPriceOutput\"]', '[data-testid=\"product-price\"]', '[itemprop=\"price\"]', '.product-price .price', '.price', '.your-price']); const m = raw.match(/\\d[\\d,]*(?:\\.\\d{2})?/); return m ? m[0].replace(/,/g, '') : raw; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "currency",
            "selector": "(() => { const priceNode = document.querySelector('[automation-id=\"productPriceOutput\"], [data-testid=\"product-price\"], [itemprop=\"price\"], .product-price .price, .price, .your-price'); const raw = priceNode ? priceNode.textContent : ''; const m = raw.match(/[$€£¥]/); return m ? m[0] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "unit_price",
            "selector": "(() => { const pick = (sels) => { for (const s of sels) { const el = document.querySelector(s); const t = el && el.textContent && el.textContent.trim(); if (t) return t; } return ''; }; const fromSelector = pick(['[automation-id*=\"unitPrice\" i]', '[data-testid*=\"unit-price\" i]', '.unit-price', '.price-per-unit', '.product-unit-price']); if (fromSelector) return fromSelector; const text = document.body.innerText || ''; const m = text.match(/\\$\\s*\\d[\\d,]*(?:\\.\\d{2})?\\s*(?:\\/|per)\\s*[^\\n]+/i); return m ? m[0].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "sku",
            "selector": "(() => { const pick = (sels) => { for (const s of sels) { const el = document.querySelector(s); const t = el && el.textContent && el.textContent.trim(); if (t) return t; } return ''; }; const selected = pick(['[automation-id=\"itemNumber\"]', '[data-testid=\"item-number\"]', '.item-number', '.product-number', '#productItemNumber']); const selectedMatch = selected.match(/\\d{4,}/); if (selectedMatch) return selectedMatch[0]; const text = document.body.innerText || ''; const itemMatch = text.match(/(?:Item\\s*#?|Item\\s*Number|SKU|Product\\s*ID)\\s*:?\\s*(\\d{4,})/i); if (itemMatch) return itemMatch[1]; const urlMatch = location.href.match(/(?:product\\.|partNumber=|\\/)(\\d{6,})(?:\\.html|[&?#/]|$)/i); return urlMatch ? urlMatch[1] : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "features",
            "selector": "(() => { const collect = (rootSel) => { const root = document.querySelector(rootSel); if (!root) return ''; const items = Array.from(root.querySelectorAll('li')).map(li => li.textContent.trim()).filter(Boolean); return items.length ? items.join('\\n') : root.textContent.trim(); }; const sels = ['[automation-id=\"features\"]', '[data-testid=\"features\"]', '#features', '.product-features', '.pdp-features', '.features']; for (const s of sels) { const val = collect(s); if (val) return val; } const text = document.body.innerText || ''; const m = text.match(/Features\\s*([\\s\\S]{0,1500}?)(?:Product Details|Specifications|Shipping|Reviews|$)/i); return m ? m[1].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "product_details",
            "selector": "(() => { const textOf = (s) => { const el = document.querySelector(s); return el && el.textContent ? el.textContent.trim() : ''; }; const sels = ['[automation-id=\"productDetails\"]', '[data-testid=\"product-details\"]', '#productDetails', '#product-details', '.product-details', '.product-info-description', '.product-description', '[itemprop=\"description\"]']; for (const s of sels) { const val = textOf(s); if (val) return val; } const text = document.body.innerText || ''; const m = text.match(/Product Details\\s*([\\s\\S]{0,3000}?)(?:Specifications|Shipping|Reviews|Warranty|$)/i); return m ? m[1].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "brand",
            "selector": "(() => { const attr = document.querySelector('[itemprop=\"brand\"] [itemprop=\"name\"], [itemprop=\"brand\"], [automation-id=\"brandName\"], [data-testid=\"brand\"], .product-brand, .brand'); if (attr && attr.textContent && attr.textContent.trim()) return attr.textContent.trim(); const meta = document.querySelector('meta[property=\"product:brand\"], meta[name=\"brand\"]'); if (meta && meta.content) return meta.content.trim(); const text = document.body.innerText || ''; const m = text.match(/Brand\\s*:?\\s*([^\\n]+)/i); return m ? m[1].trim() : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "main_img",
            "selector": "(() => { const meta = document.querySelector('meta[property=\"og:image\"], meta[name=\"twitter:image\"]'); if (meta && meta.content) return new URL(meta.content, location.href).href; const img = document.querySelector('[automation-id=\"productImage\"] img, [data-testid=\"product-image\"] img, img[itemprop=\"image\"], .product-image img, .hero-image img, main img'); if (!img) return ''; const src = img.currentSrc || img.src || img.getAttribute('data-src') || img.getAttribute('data-original') || ''; return src ? new URL(src, location.href).href : ''; })()",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "rating",
            "selector": "(() => { const pick = (sels) => { for (const s of sels) { const el = document.querySelector(s); const t = el && el.textContent && el.textContent.trim(); if (t) return t; } return ''; }; const selected = pick(['[itemprop=\"ratingValue\"]', '[automation-id*=\"rating\" i]', '[data-testid*=\"rating\" i]', '.bv-rating-ratio-number', '.rating', '.product-rating']); const m1 = selected.match(/\\d+(?:\\.\\d+)?/); if (m1) return m1[0]; const aria = Array.from(document.querySelectorAll('[aria-label]')).map(e => e.getAttribute('aria-label')).find(v => /rating|stars/i.test(v || '')); const m2 = aria ? aria.match(/\\d+(?:\\.\\d+)?/) : null; return m2 ? m2[0] : ''; })()",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2280,
      "position_y": 440,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "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": "text-contains-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "text-contains-1",
      "from_connector_id": "true",
      "to_block_id": "structured-export-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "text-contains-1",
      "from_connector_id": "false",
      "to_block_id": "wait-for-element-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "structured-export-2",
      "from_connector_id": "right",
      "to_block_id": "loop-continue-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": "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": 156,
      "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": 408,
      "position_y": 156,
      "width": 1400,
      "height": 656,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1128,
      "position_y": 156,
      "width": 1400,
      "height": 476,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "text-contains-1",
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "group-extract",
      "element_type": "group",
      "title": "Data Extraction",
      "color": "#42be65",
      "position_x": 1488,
      "position_y": 156,
      "width": 680,
      "height": 656,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-2",
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Costco Wholesale product detail scraper equivalent to the Octoparse template. Processes an editable list of Costco product detail URLs using navigate.urls[] and loop-continue, appending one row per URL to costco_product_details_scraper.csv. Accessible product pages are extracted from the live Costco DOM. If Costco serves Access Denied, the template writes an Octoparse-compatible fallback row using known sample values or URL-derived values where possible, and notes the access block in product_details. Live scraping may require an allowed region/IP, trusted browser profile, or manual anti-bot clearance.",
      "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": 680,
      "position_y": 240,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "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": 1400,
      "position_y": 240,
      "width": 340,
      "height": 131,
      "z_index": 22,
      "data": {
        "block_id": "text-contains-1"
      }
    },
    {
      "id": "note-block-structured-export-2",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (page_url, name, price, currency, unit_price). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 240,
      "width": 340,
      "height": 128,
      "z_index": 22,
      "data": {
        "block_id": "structured-export-2"
      }
    },
    {
      "id": "note-block-structured-export-1",
      "element_type": "note",
      "title": "Note: Structured Export",
      "content": "Structured export with JS columns (page_url, name, price, currency, unit_price). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2120,
      "position_y": 600,
      "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": 2480,
      "position_y": 420,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}