{
  "version": "1.0.0",
  "exported_at": "2026-05-31T00:00:00.000Z",
  "project": {
    "name": "Twitter Scraper by hashtag",
    "description": "Best-effort X/Twitter hashtag scraper equivalent to the Octoparse Twitter Scraper by hashtag. Because the attached live analysis shows X hashtag search redirects unauthenticated sessions to login/onboarding, this template uses the public tweet detail URLs visible in the Octoparse #fridayfeeling preview as a multi-URL navigation loop. It extracts Category, Keyword, Web_Page_URL, Tweet_Website, Author_Name, Author_Web_Page_URL, Tweet_Timestamp, Tweet_Content, Tweet_Image_URL, Tweet_Video_URL, Tweet_AD, Reply, Repost, Like, and View. For full live hashtag search coverage, run UScraper with an authenticated X browser profile and replace the URL list with discovered status URLs or a logged-in search workflow. Pagination/navigation strategy: navigate.urls[] loops through all known tweet URLs and appends each extracted tweet to one CSV.",
    "color": "bg-[#4589ff]",
    "template_id": "ai-generated"
  },
  "blocks": [
    {
      "block_id": "navigate-1",
      "block_type": "process",
      "title": "Navigate",
      "description": "Go to a URL",
      "position_x": 120,
      "position_y": 220,
      "config": {
        "urls": [
          "https://x.com/Printtapp/status/1768257041947062311",
          "https://x.com/sharonmawbry/status/1766053090631839819",
          "https://x.com/CarlyanneMcCon1/status/1766031108536197166",
          "https://x.com/folu_og43/status/1766146340239610182",
          "https://x.com/Kulbirs_ingh/status/1766111000456007971",
          "https://x.com/premium/status/1765056361673654355",
          "https://x.com/LazyCatsX/status/1766179248539111557",
          "https://x.com/karma_shopping/status/1757323221781655642"
        ],
        "color": "bg-[#4589ff]",
        "tags": [
          "x",
          "twitter",
          "hashtag",
          "multi-url"
        ]
      }
    },
    {
      "block_id": "wait-for-page-load-1",
      "block_type": "process",
      "title": "Wait for Page Load",
      "description": "Wait for page to finish loading",
      "position_x": 456,
      "position_y": 220,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 792,
      "position_y": 220,
      "config": {
        "duration": 4,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "element-exists-1",
      "block_type": "process",
      "title": "Element Exists",
      "description": "Check if element exists",
      "position_x": 1128,
      "position_y": 220,
      "config": {
        "selector": "article[data-testid=\"tweet\"], [data-testid=\"tweetText\"]",
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1464,
      "position_y": 520,
      "config": {
        "jsCode": "return (() => { const KEYWORD = '#fridayfeeling'; const CATEGORY = 'TopPost'; const SEARCH_URL = 'https://x.com/search?q=%23fridayfeeling&src=typed_query&f=top'; const abs = href => { try { return href ? new URL(href, location.origin).href : ''; } catch (e) { return href || ''; } }; const clean = value => (value || '').replace(/\\s+/g, ' ').trim(); const normalizeStatusUrl = href => { const full = abs(href || location.href); const match = full.match(/https?:\\/\\/(?:twitter\\.com|x\\.com)\\/([^\\/?#]+)\\/status\\/(\\d+)/i); return match ? `https://x.com/${match[1]}/status/${match[2]}` : full; }; const ensureContainer = () => { let container = document.querySelector('#uscraper-twitter-rows'); if (!container) { container = document.createElement('div'); container.id = 'uscraper-twitter-rows'; container.style.display = 'none'; document.body.appendChild(container); } container.innerHTML = ''; return container; }; const metricFromText = (text, names) => { const normalized = clean(text); for (const name of names) { const re = new RegExp('([0-9][0-9.,]*\\\\s*[KMB]?)\\\\s+' + name + 's?', 'i'); const match = normalized.match(re); if (match) return clean(match[1]); } return ''; }; const getMetric = (article, names) => { const labels = Array.from(article.querySelectorAll('[aria-label]')).map(el => el.getAttribute('aria-label') || '').filter(Boolean); const combined = labels.join(' | '); const fromLabels = metricFromText(combined, names); if (fromLabels) return fromLabels; const fromText = metricFromText(article.innerText, names); if (fromText) return fromText; return ''; }; const article = document.querySelector('article[data-testid=\"tweet\"]') || (document.querySelector('[data-testid=\"tweetText\"]') ? document.querySelector('[data-testid=\"tweetText\"]').closest('article') : null); if (!article) return { collected: 0, reason: 'No tweet article found', url: location.href }; const statusCandidates = Array.from(document.querySelectorAll('a[href*=\"/status/\"]')).map(a => abs(a.getAttribute('href'))).filter(href => /\\/status\\/\\d+/.test(href || '')); const canonicalFromLocation = /\\/status\\/\\d+/.test(location.href) ? location.href : ''; const nonMediaStatus = statusCandidates.find(href => /\\/status\\/\\d+(?:$|[?#])/.test(href) && !/\\/photo\\/|\\/video\\//.test(href)); const tweetUrl = normalizeStatusUrl(canonicalFromLocation || nonMediaStatus || statusCandidates[0] || location.href); const userNameBlock = article.querySelector('[data-testid=\"User-Name\"]') || document.querySelector('[data-testid=\"User-Name\"]'); const userLink = userNameBlock ? Array.from(userNameBlock.querySelectorAll('a[href^=\"/\"]')).find(a => !/\\/status\\//.test(a.getAttribute('href') || '') && !/\\/photo/.test(a.getAttribute('href') || '')) : null; const authorUrl = abs(userLink && userLink.getAttribute('href')).replace('https://twitter.com/', 'https://x.com/'); const spans = userNameBlock ? Array.from(userNameBlock.querySelectorAll('span')).map(s => clean(s.innerText)).filter(Boolean) : []; const authorName = spans.find(s => !s.startsWith('@') && s !== '·') || ''; const timeEl = article.querySelector('time') || document.querySelector('time'); const timestamp = (timeEl && (timeEl.getAttribute('datetime') || clean(timeEl.innerText))) || ''; const contentEl = article.querySelector('[data-testid=\"tweetText\"]') || document.querySelector('[data-testid=\"tweetText\"]'); const content = clean(contentEl ? contentEl.innerText : ''); const imageUrls = Array.from(article.querySelectorAll('img[src*=\"pbs.twimg.com/media\"], img[src*=\"twimg.com/media\"]')).map(img => img.src).filter(Boolean).filter((src, i, arr) => arr.indexOf(src) === i).join(' | '); const videoUrls = Array.from(article.querySelectorAll('video')).map(video => video.currentSrc || video.src || video.getAttribute('poster') || '').filter(Boolean).filter((src, i, arr) => arr.indexOf(src) === i).join(' | '); const allText = clean(article.innerText); const isAd = /\\bPromoted\\b|\\bAd\\b/.test(allText) ? 'True' : 'False'; const viewLink = Array.from(document.querySelectorAll('a[href$=\"/analytics\"], a[href*=\"/analytics\"]')).find(a => /View/i.test(clean(a.innerText))); const view = viewLink ? clean(viewLink.innerText).replace(/\\s*Views?\\s*/i, '') : getMetric(article, ['view', 'views']); const data = { category: CATEGORY, keyword: KEYWORD, web_page_url: SEARCH_URL, tweet_website: tweetUrl, author_name: authorName, author_web_page_url: authorUrl, tweet_timestamp: timestamp, tweet_content: content, tweet_image_url: imageUrls, tweet_video_url: videoUrls, tweet_ad: isAd, reply: getMetric(article, ['reply', 'replies']), repost: getMetric(article, ['repost', 'reposts', 'retweet', 'retweets']), like: getMetric(article, ['like', 'likes']), view: view }; const container = ensureContainer(); const row = document.createElement('div'); row.className = 'uscraper-twitter-row'; Object.entries(data).forEach(([key, value]) => row.setAttribute('data-' + key.replace(/_/g, '-'), value || '')); container.appendChild(row); return { collected: 1, tweet: tweetUrl, author: authorName }; })();",
        "waitForCompletion": true,
        "timeout": 30,
        "color": "bg-[#a56eff]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1800,
      "position_y": 520,
      "config": {
        "selector": "#uscraper-twitter-rows .uscraper-twitter-row",
        "timeout": 10,
        "visible": false,
        "color": "bg-[#42be65]"
      }
    },
    {
      "block_id": "structured-export-1",
      "block_type": "process",
      "title": "Structured Export",
      "description": "Export data with custom columns",
      "position_x": 2136,
      "position_y": 520,
      "config": {
        "rowSelector": "#uscraper-twitter-rows .uscraper-twitter-row",
        "fileName": "twitter-scraper-by-hashtag.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "category",
            "selector": "",
            "attribute": "data-category"
          },
          {
            "name": "keyword",
            "selector": "",
            "attribute": "data-keyword"
          },
          {
            "name": "web_page_url",
            "selector": "",
            "attribute": "data-web-page-url"
          },
          {
            "name": "tweet_website",
            "selector": "",
            "attribute": "data-tweet-website"
          },
          {
            "name": "author_name",
            "selector": "",
            "attribute": "data-author-name"
          },
          {
            "name": "author_web_page_url",
            "selector": "",
            "attribute": "data-author-web-page-url"
          },
          {
            "name": "tweet_timestamp",
            "selector": "",
            "attribute": "data-tweet-timestamp"
          },
          {
            "name": "tweet_content",
            "selector": "",
            "attribute": "data-tweet-content"
          },
          {
            "name": "tweet_image_url",
            "selector": "",
            "attribute": "data-tweet-image-url"
          },
          {
            "name": "tweet_video_url",
            "selector": "",
            "attribute": "data-tweet-video-url"
          },
          {
            "name": "tweet_ad",
            "selector": "",
            "attribute": "data-tweet-ad"
          },
          {
            "name": "reply",
            "selector": "",
            "attribute": "data-reply"
          },
          {
            "name": "repost",
            "selector": "",
            "attribute": "data-repost"
          },
          {
            "name": "like",
            "selector": "",
            "attribute": "data-like"
          },
          {
            "name": "view",
            "selector": "",
            "attribute": "data-view"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2472,
      "position_y": 520,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    },
    {
      "block_id": "loop-continue-2",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 1128,
      "position_y": 520,
      "config": {
        "color": "bg-[#8d8d8d]"
      }
    }
  ],
  "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": "element-exists-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "true",
      "to_block_id": "inject-javascript-1",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "element-exists-1",
      "from_connector_id": "false",
      "to_block_id": "loop-continue-2",
      "to_connector_id": "left"
    },
    {
      "from_block_id": "inject-javascript-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": "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": 2000,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-pagination",
      "element_type": "group",
      "title": "Pagination Loop",
      "color": "#ff832b",
      "position_x": 1056,
      "position_y": 116,
      "width": 1664,
      "height": 596,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "element-exists-1",
          "loop-continue-1",
          "loop-continue-2"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1392,
      "position_y": 416,
      "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": 2064,
      "position_y": 416,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "structured-export-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort X/Twitter hashtag scraper equivalent to the Octoparse Twitter Scraper by hashtag. Because the attached live analysis shows X hashtag search redirects unauthenticated sessions to login/onboarding, this template uses the public tweet detail URLs visible in the Octoparse #fridayfeeling preview as a multi-URL navigation loop. It extracts Category, Keyword, Web_Page_URL, Tweet_Website, Author_Name, Author_Web_Page_URL, Tweet_Timestamp, Tweet_Content, Tweet_Image_URL, Tweet_Video_URL, Tweet_AD, Reply, Repost, Like, and View. For full live hashtag search coverage, run UScraper with an authenticated X browser profile and replace the URL list with discovered status URLs or a logged-in search workflow. Pagination/navigation strategy: navigate.urls[] loops through all known tweet URLs and appends each extracted tweet to one CSV.",
      "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 8 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-element-exists-1",
      "element_type": "note",
      "title": "Note: Element Exists",
      "content": "Condition block: checks `article[data-testid=\"tweet\"], [data-testid=\"tweetText\"]`. 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": 148,
      "z_index": 22,
      "data": {
        "block_id": "element-exists-1"
      }
    },
    {
      "id": "note-block-inject-javascript-1",
      "element_type": "note",
      "title": "Note: Inject JavaScript",
      "content": "Runs custom JavaScript in the page: `return (() => { const KEYWORD = '#fridayfeeling'; const CATEGORY = 'TopPost'; const SEARCH_URL = 'ht...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1664,
      "position_y": 500,
      "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": "Extracts rows matching `#uscraper-twitter-rows .uscraper-twitter-row`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2336,
      "position_y": 500,
      "width": 340,
      "height": 119,
      "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": 2672,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    },
    {
      "id": "note-block-loop-continue-2",
      "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": 1328,
      "position_y": 500,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-2"
      }
    }
  ]
}