{
  "version": "1.0.0",
  "exported_at": "2026-06-03T18:50:00.000Z",
  "project": {
    "name": "Amazon Reviews Scraper Lite for US",
    "description": "Best-effort Amazon US reviews scraper equivalent to the Octoparse Lite template. It accepts editable Amazon product detail URLs, detects the ASIN, then attempts to collect review pages 1-10 using visible review snippets, direct /product-reviews pages, and Amazon's same-origin reviews-render AJAX endpoint. It extracts review content, review titles, ratings, dates, reviewer/avatar links, helpful counts, and product metadata. If Amazon blocks reviews, CAPTCHA appears, or review rows are unavailable, a diagnostic fallback row is exported instead of failing.",
    "color": "bg-[#ff9900]",
    "template_id": "ai-generated-amazon-reviews-lite-us"
  },
  "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.amazon.com/bnhjsdw-Elastic-Round-Tablecloth-Resistant/dp/B0GFLYYTRH/ref=cm_cr_arp_d_product_top?ie=UTF8",
          "https://www.amazon.com/Etekcity-Multifunction-Stainless-Batteries-Included/dp/B0113UZJE2/ref=zg_bs_c_kitchen_d_sccl_4/144-5220518-3162904?pd_rd_i=B0113UZJE2&th=1",
          "https://www.amazon.com/Owala-Insulated-Stainless-Steel-Push-Button-24-Ounce/dp/B085DTZQNZ/ref=zg_bs_c_kitchen_d_sccl_1/144-5220518-3162904?pd_rd_i=B085DTZQNZ&th=1"
        ],
        "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": 480,
      "position_y": 220,
      "config": {
        "timeout": 30
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 840,
      "position_y": 220,
      "config": {
        "duration": 2
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1200,
      "position_y": 220,
      "config": {
        "jsCode": "window.__uscraperAmazonReviewsDone = false;\n(async () => {\n  const clean = v => (v || '').replace(/\\s+/g, ' ').trim();\n  const q = (sel, root = document) => root.querySelector(sel);\n  const qa = (sel, root = document) => Array.from(root.querySelectorAll(sel));\n  const txt = (sel, root = document) => { const el = q(sel, root); return el ? clean(el.textContent) : ''; };\n  const absUrl = u => { try { return u ? new URL(u, location.origin).href : ''; } catch (_) { return u || ''; } };\n  const bestImg = (selectors, root) => {\n    for (const sel of selectors) {\n      const el = q(sel, root);\n      if (el) {\n        const u = el.getAttribute('data-src') || el.getAttribute('data-a-hires') || el.currentSrc || el.src || el.getAttribute('src') || '';\n        if (u) return absUrl(u);\n      }\n    }\n    return '';\n  };\n  const parseStar = raw => {\n    const m = clean(raw).match(/([0-9.]+)\\s*out of/i);\n    return m ? m[1] : clean(raw);\n  };\n  const parseDateIso = raw => {\n    const t = clean(raw);\n    const m = t.match(/on\\s+(.+)$/i);\n    if (!m) return t;\n    const d = new Date(m[1]);\n    return isNaN(d) ? clean(m[1]) : d.toISOString().slice(0, 10);\n  };\n  const parseLocation = raw => {\n    const m = clean(raw).match(/^Reviewed in\\s+(.+?)\\s+on\\s+/i);\n    return m ? clean(m[1]) : '';\n  };\n  const parseHelpful = raw => {\n    const t = clean(raw);\n    if (/one person/i.test(t)) return '1';\n    const m = t.match(/([\\d,]+)\\s+people/i);\n    return m ? m[1].replace(/,/g, '') : '';\n  };\n  const nodeCleanText = el => {\n    if (!el) return '';\n    const clone = el.cloneNode(true);\n    clone.querySelectorAll('script, style, noscript, svg, i.a-icon, .a-icon-alt').forEach(n => n.remove());\n    return clean(clone.textContent);\n  };\n  const parseTitle = row => {\n    let value = '';\n    const candidates = [\n      '[data-hook=\"review-title\"] span:not(.a-icon-alt)',\n      '[data-hook=\"review-title\"]',\n      'a.review-title span:not(.a-icon-alt)',\n      'a.review-title',\n      '.review-title span:not(.a-icon-alt)',\n      '.review-title'\n    ];\n    for (const sel of candidates) {\n      const values = qa(sel, row).map(nodeCleanText).filter(Boolean).filter(v => !/^\\d+(\\.\\d+)?$/.test(v));\n      values.sort((a, b) => b.length - a.length);\n      if (values[0]) { value = values[0]; break; }\n    }\n    value = value.replace(/^[0-9.]+\\s*out of\\s*5\\s*stars\\s*/i, '');\n    value = value.replace(/^Customer Review\\s*/i, '');\n    return clean(value);\n  };\n  const parseBody = row => {\n    const selectors = [\n      '[data-hook=\"review-body\"] span',\n      '[data-hook=\"review-body\"]',\n      'span[data-hook=\"review-body\"]',\n      '[data-hook=\"review-collapsed\"] span',\n      '[data-hook=\"review-collapsed\"]',\n      '.review-text-content span',\n      '.review-text-content',\n      '.review-text span',\n      '.review-text',\n      '.a-expander-content.reviewText',\n      '.cr-original-review-content',\n      '.cr-translated-review-content'\n    ];\n    const values = [];\n    for (const sel of selectors) qa(sel, row).forEach(el => { const v = nodeCleanText(el); if (v) values.push(v); });\n    values.sort((a, b) => b.length - a.length);\n    if (values[0]) return clean(values[0]);\n    let fallback = nodeCleanText(row);\n    const removeBits = [\n      txt('.a-profile-name', row),\n      parseTitle(row),\n      txt('[data-hook=\"format-strip\"], .review-format-strip', row),\n      txt('[data-hook=\"review-date\"], .review-date', row),\n      txt('[data-hook=\"review-star-rating\"] .a-icon-alt, [data-hook=\"cmps-review-star-rating\"] .a-icon-alt, .review-rating .a-icon-alt', row),\n      txt('[data-hook=\"helpful-vote-statement\"], .cr-vote-text', row)\n    ].filter(Boolean);\n    for (const bit of removeBits) fallback = fallback.replace(bit, ' ');\n    fallback = fallback.replace(/Verified Purchase|Read more|Report|Helpful|Translate review to English/gi, ' ');\n    fallback = clean(fallback);\n    return fallback.length > 20 ? fallback : '';\n  };\n  const getAsin = () => {\n    const parts = location.pathname.split('/').filter(Boolean);\n    for (let i = 0; i < parts.length; i++) {\n      const p = parts[i].toLowerCase();\n      if ((p === 'dp' || p === 'product-reviews') && parts[i + 1]) return parts[i + 1].toUpperCase();\n      if (p === 'gp' && parts[i + 1] && parts[i + 1].toLowerCase() === 'product' && parts[i + 2]) return parts[i + 2].toUpperCase();\n    }\n    const m = location.href.match(/[?&](?:pd_rd_i|asin)=([A-Z0-9]{10})/i);\n    return m ? m[1].toUpperCase() : '';\n  };\n  const asin = getAsin();\n  const productTitle = clean(txt('#productTitle') || txt('[data-hook=\"product-link\"]') || document.title.replace(/^Amazon\\.com:\\s*/i, '').split(':')[0]);\n  const productPrice = clean(txt('.a-price .a-offscreen') || txt('[data-hook=\"product-price\"]'));\n  const productStars = parseStar(txt('#acrPopover .a-icon-alt') || txt('[data-hook=\"rating-out-of-text\"]') || txt('[data-hook=\"average-star-rating\"] .a-icon-alt'));\n  const ratingCountRaw = clean(txt('#acrCustomerReviewText') || txt('[data-hook=\"total-review-count\"]') || txt('[data-hook=\"cr-filter-info-review-rating-count\"]'));\n  const ratingCountMatch = ratingCountRaw.match(/([\\d,]+)/);\n  const ratingCount = ratingCountMatch ? ratingCountMatch[1] : '';\n  let container = q('#uscraper-review-rows');\n  if (container) container.remove();\n  container = document.createElement('div');\n  container.id = 'uscraper-review-rows';\n  container.style.display = 'none';\n  document.body.appendChild(container);\n  const rowsByKey = new Map();\n  const ingestRow = (row, source, pageNumber) => {\n    const reviewDateRaw = txt('[data-hook=\"review-date\"], .review-date', row);\n    const reviewTitle = parseTitle(row);\n    const reviewContent = parseBody(row);\n    const reviewRating = parseStar(txt('[data-hook=\"review-star-rating\"] .a-icon-alt, [data-hook=\"cmps-review-star-rating\"] .a-icon-alt, i[data-hook*=\"review-star-rating\"] span, .review-rating .a-icon-alt', row));\n    const productAttributes = txt('[data-hook=\"format-strip\"], .review-format-strip', row);\n    const reviewerName = txt('.a-profile-name', row);\n    const reviewImages = qa('[data-hook=\"review-image-tile\"] img, .review-image-tile-section img, img.review-image-tile', row)\n      .map(img => absUrl(img.getAttribute('data-src') || img.getAttribute('data-a-hires') || img.currentSrc || img.src || img.getAttribute('src') || ''))\n      .filter(Boolean)\n      .join(' | ');\n    const data = {\n      productPrice,\n      productTitle,\n      productLink: asin ? `${location.origin}/dp/${asin}` : location.href,\n      asin,\n      reviewerName,\n      reviewTitle,\n      reviewContent,\n      productAttributes,\n      reviewDate: parseDateIso(reviewDateRaw),\n      ratingCount,\n      reviewCount: ratingCount,\n      dataSource: 'Amazon',\n      productStars,\n      reviewLocation: parseLocation(reviewDateRaw),\n      reviewRating,\n      helpfulCount: parseHelpful(txt('[data-hook=\"helpful-vote-statement\"], .cr-vote-text', row)),\n      reviewerAvatarLink: bestImg(['.a-profile-avatar img', 'img.a-profile-avatar'], row),\n      reviewImageLink: reviewImages,\n      source,\n      pageNumber: String(pageNumber || '')\n    };\n    const key = [reviewerName, data.reviewDate, reviewRating, productAttributes, reviewTitle || reviewContent.slice(0, 60)].join('|');\n    if (!key.replace(/\\|/g, '')) return;\n    const existing = rowsByKey.get(key);\n    if (!existing) rowsByKey.set(key, data);\n    else {\n      const oldScore = (existing.reviewTitle ? 1 : 0) + (existing.reviewContent ? 3 : 0) + existing.reviewContent.length / 1000;\n      const newScore = (data.reviewTitle ? 1 : 0) + (data.reviewContent ? 3 : 0) + data.reviewContent.length / 1000;\n      if (newScore > oldScore) rowsByKey.set(key, Object.assign(existing, data));\n    }\n  };\n  const reviewSelectors = 'div[data-hook=\"review\"], div[id^=\"customer_review-\"], div.a-section.review';\n  const ingestDocument = (doc, source, pageNumber) => {\n    qa(reviewSelectors, doc).forEach(row => ingestRow(row, source, pageNumber));\n  };\n  const fetchText = async (url, ajax = false) => {\n    const opts = { credentials: 'include' };\n    if (ajax) opts.headers = { 'x-requested-with': 'XMLHttpRequest' };\n    const res = await fetch(url, opts);\n    return await res.text();\n  };\n  const parseHtmlString = html => new DOMParser().parseFromString(html, 'text/html');\n  const collectAjaxHtml = raw => {\n    const fragments = [];\n    raw.split('\\n').forEach(line => {\n      const s = line.trim().replace(/^&&&/, '');\n      if (!s) return;\n      try {\n        const val = JSON.parse(s);\n        const walk = x => {\n          if (typeof x === 'string' && /data-hook=\\\\?\"review|customer_review-|review-body|review-title/i.test(x)) fragments.push(x);\n          else if (Array.isArray(x)) x.forEach(walk);\n          else if (x && typeof x === 'object') Object.values(x).forEach(walk);\n        };\n        walk(val);\n      } catch (_) {\n        if (/data-hook=\\\\?\"review|customer_review-|review-body|review-title/i.test(s)) fragments.push(s);\n      }\n    });\n    return fragments.join('\\n');\n  };\n  ingestDocument(document, 'visible_product_page', 'current');\n  if (asin) {\n    for (let page = 1; page <= 10; page++) {\n      try {\n        const directUrl = `${location.origin}/product-reviews/${asin}/?ie=UTF8&reviewerType=all_reviews&filterByStar=all_stars&sortBy=recent&pageNumber=${page}`;\n        const html = await fetchText(directUrl, false);\n        if (/captcha|robot check|enter the characters you see below/i.test(html)) break;\n        ingestDocument(parseHtmlString(html), 'direct_review_page', page);\n      } catch (_) {}\n      try {\n        const ajaxUrl = `${location.origin}/hz/reviews-render/ajax/reviews/get/ref=cm_cr_getr_d_paging_btm_next_${page}?ie=UTF8&reviewerType=all_reviews&filterByStar=all_stars&sortBy=recent&pageNumber=${page}&asin=${asin}`;\n        const raw = await fetchText(ajaxUrl, true);\n        if (/captcha|robot check|enter the characters you see below/i.test(raw)) break;\n        const html = collectAjaxHtml(raw);\n        if (html) ingestDocument(parseHtmlString(html), 'ajax_reviews_render', page);\n      } catch (_) {}\n      await new Promise(resolve => setTimeout(resolve, 400));\n    }\n  }\n  const set = (el, k, v) => el.setAttribute(k, v == null ? '' : String(v));\n  Array.from(rowsByKey.values()).forEach(data => {\n    const el = document.createElement('div');\n    el.className = 'uscraper-review-row';\n    set(el, 'data-product-price', data.productPrice);\n    set(el, 'data-product-title', data.productTitle);\n    set(el, 'data-product-link', data.productLink);\n    set(el, 'data-asin', data.asin);\n    set(el, 'data-reviewer-name', data.reviewerName);\n    set(el, 'data-review-title', data.reviewTitle);\n    set(el, 'data-review-content', data.reviewContent);\n    set(el, 'data-product-attributes', data.productAttributes);\n    set(el, 'data-review-date', data.reviewDate);\n    set(el, 'data-rating-count', data.ratingCount);\n    set(el, 'data-review-count', data.reviewCount);\n    set(el, 'data-data-source', data.dataSource);\n    set(el, 'data-product-stars', data.productStars);\n    set(el, 'data-review-location', data.reviewLocation);\n    set(el, 'data-review-rating', data.reviewRating);\n    set(el, 'data-helpful-count', data.helpfulCount);\n    set(el, 'data-reviewer-avatar-link', data.reviewerAvatarLink);\n    set(el, 'data-review-image-link', data.reviewImageLink);\n    set(el, 'data-source', data.source);\n    set(el, 'data-page-number', data.pageNumber);\n    container.appendChild(el);\n  });\n  if (!container.querySelector('.uscraper-review-row')) {\n    const fallback = document.createElement('div');\n    fallback.className = 'uscraper-review-row uscraper-fallback-row';\n    set(fallback, 'data-product-price', productPrice);\n    set(fallback, 'data-product-title', productTitle);\n    set(fallback, 'data-product-link', asin ? `${location.origin}/dp/${asin}` : location.href);\n    set(fallback, 'data-asin', asin);\n    set(fallback, 'data-review-title', 'NO_VISIBLE_REVIEWS_OR_AMAZON_BLOCKED');\n    set(fallback, 'data-review-content', 'Amazon did not expose review rows in this browser session. CAPTCHA, bot challenge, regional page, or unavailable review pages may be present.');\n    set(fallback, 'data-rating-count', ratingCount);\n    set(fallback, 'data-review-count', ratingCount);\n    set(fallback, 'data-data-source', 'Amazon');\n    set(fallback, 'data-product-stars', productStars);\n    container.appendChild(fallback);\n  }\n  window.__uscraperAmazonReviewsDone = true;\n})();",
        "waitForCompletion": true,
        "timeout": 60
      }
    },
    {
      "block_id": "sleep-2",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1560,
      "position_y": 220,
      "config": {
        "duration": 20
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 220,
      "config": {
        "selector": "#uscraper-review-rows .uscraper-review-row",
        "timeout": 30,
        "visible": false
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2280,
      "position_y": 220,
      "config": {
        "rowSelector": "#uscraper-review-rows .uscraper-review-row",
        "fileName": "amazon-reviews-scraper-lite-for-us.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "columns": [
          {
            "name": "Product_price",
            "selector": "ROW.getAttribute('data-product-price') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Product_title",
            "selector": "ROW.getAttribute('data-product-title') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Product_link",
            "selector": "ROW.getAttribute('data-product-link') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "ASIN",
            "selector": "ROW.getAttribute('data-asin') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Reviewer_name",
            "selector": "ROW.getAttribute('data-reviewer-name') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_title",
            "selector": "ROW.getAttribute('data-review-title') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_content",
            "selector": "ROW.getAttribute('data-review-content') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Product_attributes",
            "selector": "ROW.getAttribute('data-product-attributes') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review__date",
            "selector": "ROW.getAttribute('data-review-date') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Rating_count",
            "selector": "ROW.getAttribute('data-rating-count') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_Count",
            "selector": "ROW.getAttribute('data-review-count') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Data_source",
            "selector": "ROW.getAttribute('data-data-source') || 'Amazon'",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Product_stars",
            "selector": "ROW.getAttribute('data-product-stars') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_location",
            "selector": "ROW.getAttribute('data-review-location') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_rating",
            "selector": "ROW.getAttribute('data-review-rating') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Helpful_count",
            "selector": "ROW.getAttribute('data-helpful-count') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Reviewer_avatar_link",
            "selector": "ROW.getAttribute('data-reviewer-avatar-link') || ''",
            "attribute": "text",
            "isJs": true
          },
          {
            "name": "Review_image_link",
            "selector": "ROW.getAttribute('data-review-image-link') || ''",
            "attribute": "text",
            "isJs": true
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 220,
      "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": "sleep-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-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-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "sleep-2",
      "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": "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": 2120,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "sleep-2",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1128,
      "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": 2208,
      "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": 2568,
      "position_y": 116,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort Amazon US reviews scraper equivalent to the Octoparse Lite template. It accepts editable Amazon product detail URLs, detects the ASIN, then attempts to collect review pages 1-10 using visible review snippets, direct /product-reviews pages, and Amazon's same-origin reviews-render AJAX endpoint. It extracts review content, review titles, ratings, dates, reviewer/avatar links, helpful counts, and product metadata. If Amazon blocks reviews, CAPTCHA appears, or review rows are unavailable, a diagnostic fallback row is exported instead of failing.",
      "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": 320,
      "position_y": 200,
      "width": 328,
      "height": 107,
      "z_index": 22,
      "data": {
        "block_id": "navigate-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `window.__uscraperAmazonReviewsDone = false;\n(async () => {\n  const clean = v => (v || '').replace(/\\...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1400,
      "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 (Product_price, Product_title, Product_link, ASIN, Reviewer_name). These selectors are fragile — update if the site layout changes.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 200,
      "width": 340,
      "height": 135,
      "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": 2840,
      "position_y": 200,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}