{
  "version": "1.0.0",
  "exported_at": "2026-05-31T00:00:00.000Z",
  "project": {
    "name": "YouTube Community Scraper",
    "description": "Best-effort YouTube Community scraper for public channel Community URLs and direct /post/ URLs. Pagination/navigation strategy: uses navigate.urls[] for multiple input URLs and appends one extracted record per loaded URL; the JavaScript block creates a guaranteed extraction row from visible YouTube post DOM, metadata, and page text. YouTube may hide Community feeds, require login, or show bot checks; in those cases the row records the input URL and available blocker/status text rather than silently exporting nothing.",
    "color": "bg-[#ff0000]",
    "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": 120,
      "position_y": 240,
      "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": 240,
      "config": {
        "urls": [
          "https://www.youtube.com/post/UgkxEe7iQ7rQFSrORs8Kj2Mi_zl10KXfRPsj",
          "https://www.youtube.com/@NatGeo/community",
          "https://www.youtube.com/@BusinessInsider/community",
          "https://www.youtube.com/@TSN_Sports/community"
        ],
        "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": 240,
      "config": {
        "timeout": 45,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "sleep-1",
      "block_type": "process",
      "title": "Sleep",
      "description": "Wait for specified time",
      "position_x": 1200,
      "position_y": 240,
      "config": {
        "duration": 4,
        "color": "bg-[#08bdba]"
      }
    },
    {
      "block_id": "inject-javascript-1",
      "block_type": "process",
      "title": "Inject JavaScript",
      "description": "Execute custom JavaScript",
      "position_x": 1560,
      "position_y": 240,
      "config": {
        "jsCode": "(() => {\n  const clean = (s) => String(s || '').replace(/\\s+/g, ' ').trim();\n  const abs = (u) => { try { return u ? new URL(u, location.origin).href.split('&')[0] : ''; } catch (e) { return u || ''; } };\n  const meta = (sel) => clean((document.querySelector(sel) || {}).content || '');\n  const countOnly = (s) => {\n    const t = clean(s);\n    const m = t.match(/[0-9]+(?:[,.][0-9]+)*(?:\\s*[KMB])?/i);\n    return m ? clean(m[0]) : '';\n  };\n  const first = (sels, root = document) => {\n    for (const s of sels) {\n      const el = root.querySelector(s);\n      if (el) return el;\n    }\n    return null;\n  };\n  const firstText = (sels, root = document) => {\n    const el = first(sels, root);\n    return clean(el && (el.getAttribute('title') || el.getAttribute('aria-label') || el.textContent));\n  };\n  const findCommentCount = (root) => {\n    const scoped = firstText(['#reply-button-end #count', '#comment-button #count'], root);\n    if (countOnly(scoped)) return countOnly(scoped);\n    const candidates = Array.from(document.querySelectorAll('h2, yt-formatted-string, span, div'));\n    for (const n of candidates) {\n      const t = clean(n.textContent);\n      const m = t.match(/^([0-9]+(?:[,.][0-9]+)*(?:\\s*[KMB])?)\\s+Comments?$/i);\n      if (m) return clean(m[1]);\n    }\n    const aria = Array.from(root.querySelectorAll('[aria-label*=\"comment\" i], [title*=\"comment\" i]'))\n      .map(n => clean(n.getAttribute('aria-label') || n.getAttribute('title') || n.textContent))\n      .find(t => /comments?/i.test(t) && countOnly(t));\n    return countOnly(aria);\n  };\n  const findLikeCount = (root) => {\n    const direct = firstText(['#vote-count-middle', '#vote-count-left', '[id*=\"vote-count\"]'], root);\n    if (countOnly(direct)) return countOnly(direct);\n    const aria = Array.from(root.querySelectorAll('[aria-label*=\"like\" i], [title*=\"like\" i]'))\n      .map(n => clean(n.getAttribute('aria-label') || n.getAttribute('title') || n.textContent))\n      .find(t => /likes?/i.test(t) && countOnly(t));\n    return countOnly(aria);\n  };\n  const findSubscribers = () => {\n    const selectors = ['#subscriber-count', '#owner-sub-count', 'ytd-video-owner-renderer #owner-sub-count'];\n    for (const sel of selectors) {\n      const t = firstText([sel]);\n      if (/subscribers?/i.test(t)) return t;\n    }\n    return '';\n  };\n  const bodyText = clean(document.body ? document.body.innerText || document.body.textContent : '');\n  const postRoot = first(['ytd-backstage-post-renderer', 'ytd-browse', 'ytd-app', 'body']) || document.body;\n  const authorEl = first(['#author-text', '#author-name', 'ytd-channel-name a', 'a[href^=\"/@\"]', 'a[href*=\"youtube.com/@\"]'], postRoot) || first(['a[href^=\"/@\"]', 'a[href*=\"youtube.com/@\"]']);\n  const authorHref = clean(authorEl && (authorEl.href || authorEl.getAttribute('href')));\n  const handleMatch = (authorHref || location.href).match(/(?:youtube\\.com\\/)?(@[^\\/?#]+)/);\n  const account = handleMatch ? handleMatch[1] : '';\n  let channelName = clean(authorEl && authorEl.textContent);\n  if (!channelName || channelName.startsWith('@')) channelName = meta('meta[property=\"og:title\"]') || meta('meta[name=\"title\"]') || document.title;\n  channelName = clean(channelName.replace(/^Post from\\s+/i, '').replace(/ - YouTube$/i, ''));\n  if ((!channelName || channelName === 'YouTube') && account) channelName = account;\n  const descMeta = meta('meta[property=\"og:description\"]') || meta('meta[name=\"description\"]');\n  const postTextEl = first(['#content-text', 'yt-formatted-string#content-text', 'yt-attributed-string#content-text', 'ytd-expander #content-text'], postRoot);\n  let postText = clean(postTextEl && postTextEl.textContent);\n  if (!postText && location.href.includes('/post/')) postText = descMeta;\n  if (!postText && /This Community isn't available/i.test(bodyText)) postText = \"This Community isn't available\";\n  if (!postText && /Sign in to confirm/i.test(bodyText)) postText = \"Sign in to confirm you're not a bot\";\n  const postLink = first(['a[href*=\"/post/\"]'], postRoot);\n  const postUrl = postLink ? abs(postLink.getAttribute('href')) : (location.href.includes('/post/') ? location.href.split('?')[0] : '');\n  const postDate = firstText(['#published-time-text a', 'a[href*=\"/post/\"]'], postRoot) || ((bodyText.match(/\\b\\d+\\s+(?:second|minute|hour|day|week|month|year)s?\\s+ago\\b/i) || [''])[0]);\n  const subscribers = findSubscribers();\n  const likeCount = findLikeCount(postRoot);\n  const commentCount = findCommentCount(postRoot);\n  const domLinks = postTextEl ? Array.from(postTextEl.querySelectorAll('a[href]')).map(a => abs(a.getAttribute('href'))).filter(h => h && !h.includes('/post/') && !h.includes('/@') && !h.includes('/channel/')) : [];\n  const textLinks = (postText.match(/https?:\\/\\/\\S+/g) || []).map(x => x.replace(/[),.;]+$/, ''));\n  const hashtags = Array.from(new Set((postText.match(/#[\\p{L}\\p{N}_]+/gu) || []))).join(' ');\n  const videoTitle = firstText(['ytd-video-renderer #video-title', 'ytd-video-renderer a#video-title', 'h3 a[href*=\"watch\"]', 'a[href*=\"/watch?v=\"][title]'], postRoot);\n  const videoDesc = firstText(['ytd-video-renderer #description-text', 'ytd-video-renderer .metadata-snippet-text', 'ytd-video-renderer #description'], postRoot);\n  const videoEl = first(['ytd-video-renderer a#video-title[href*=\"watch\"]', 'ytd-video-renderer ytd-thumbnail a[href*=\"watch\"]', 'a[href*=\"/watch?v=\"]'], postRoot);\n  const videoLink = videoEl ? abs(videoEl.getAttribute('href')) : '';\n  const pollRoot = first(['ytd-backstage-poll-renderer'], postRoot);\n  const pollText = clean(pollRoot && pollRoot.textContent);\n  const pollOptions = pollRoot ? Array.from(new Set(Array.from(pollRoot.querySelectorAll('#choice-text, yt-formatted-string, ytd-backstage-poll-choice-renderer')).map(n => clean(n.textContent)).filter(Boolean))).join(' | ') : '';\n  const pollVoteCount = (pollText.match(/[\\d,.]+\\s*[KMB]?\\s+votes?/i) || [''])[0];\n  document.querySelectorAll('.uscraper-community-post').forEach(n => n.remove());\n  const host = document.createElement('div');\n  host.id = 'uscraper-community-posts';\n  host.setAttribute('data-uscraper-created', 'true');\n  host.style.cssText = 'position:absolute;left:-99999px;top:0;width:1px;height:1px;overflow:hidden;';\n  const row = document.createElement('div');\n  row.className = 'uscraper-community-post';\n  const data = {\n    channel_name: channelName,\n    account: account,\n    subscribers: subscribers,\n    bio: location.href.includes('/community') && !/This Community isn't available/i.test(bodyText) ? descMeta : '',\n    community_url: account ? 'https://www.youtube.com/' + account + '/community' : (location.href.includes('/community') ? location.href.split('?')[0] : ''),\n    post_date: postDate,\n    post_text: postText,\n    post_url: postUrl,\n    like_count: likeCount,\n    comment_count: commentCount,\n    hashtags: hashtags,\n    links_in_post: Array.from(new Set(domLinks.concat(textLinks))).join(' '),\n    attached_video_title: videoTitle,\n    attached_video_description: videoDesc,\n    attached_video_link: videoLink,\n    poll_options: pollOptions,\n    poll_vote_count: pollVoteCount,\n    source_url: location.href.split('#')[0],\n    page_status: /Sign in to confirm/i.test(bodyText) ? 'bot_or_login_prompt' : (/This Community isn't available/i.test(bodyText) ? 'community_unavailable' : 'loaded')\n  };\n  for (const [k, v] of Object.entries(data)) row.setAttribute('data-' + k.replace(/_/g, '-'), v || '');\n  row.textContent = data.post_text || data.channel_name || data.source_url || 'youtube-row';\n  host.appendChild(row);\n  document.body.appendChild(host);\n  return document.querySelectorAll('.uscraper-community-post').length;\n})()",
        "waitForCompletion": true,
        "timeout": 30,
        "color": "bg-[#ff832b]"
      }
    },
    {
      "block_id": "wait-for-element-1",
      "block_type": "process",
      "title": "Wait for Element",
      "description": "Wait until element appears",
      "position_x": 1920,
      "position_y": 240,
      "config": {
        "selector": ".uscraper-community-post",
        "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": 2280,
      "position_y": 240,
      "config": {
        "rowSelector": ".uscraper-community-post",
        "fileName": "youtube-community-scraper.csv",
        "saveLocation": "C:\\Users\\theskd\\Documents\\UScraper\\templates",
        "includeHeaders": true,
        "fileMode": "append",
        "color": "bg-[#42be65]",
        "columns": [
          {
            "name": "channel_name",
            "selector": "",
            "attribute": "data-channel-name"
          },
          {
            "name": "account",
            "selector": "",
            "attribute": "data-account"
          },
          {
            "name": "subscribers",
            "selector": "",
            "attribute": "data-subscribers"
          },
          {
            "name": "bio",
            "selector": "",
            "attribute": "data-bio"
          },
          {
            "name": "community_url",
            "selector": "",
            "attribute": "data-community-url"
          },
          {
            "name": "post_date",
            "selector": "",
            "attribute": "data-post-date"
          },
          {
            "name": "post_text",
            "selector": "",
            "attribute": "data-post-text"
          },
          {
            "name": "post_url",
            "selector": "",
            "attribute": "data-post-url"
          },
          {
            "name": "like_count",
            "selector": "",
            "attribute": "data-like-count"
          },
          {
            "name": "comment_count",
            "selector": "",
            "attribute": "data-comment-count"
          },
          {
            "name": "hashtags",
            "selector": "",
            "attribute": "data-hashtags"
          },
          {
            "name": "links_in_post",
            "selector": "",
            "attribute": "data-links-in-post"
          },
          {
            "name": "attached_video_title",
            "selector": "",
            "attribute": "data-attached-video-title"
          },
          {
            "name": "attached_video_description",
            "selector": "",
            "attribute": "data-attached-video-description"
          },
          {
            "name": "attached_video_link",
            "selector": "",
            "attribute": "data-attached-video-link"
          },
          {
            "name": "poll_options",
            "selector": "",
            "attribute": "data-poll-options"
          },
          {
            "name": "poll_vote_count",
            "selector": "",
            "attribute": "data-poll-vote-count"
          },
          {
            "name": "source_url",
            "selector": "",
            "attribute": "data-source-url"
          },
          {
            "name": "page_status",
            "selector": "",
            "attribute": "data-page-status"
          }
        ]
      }
    },
    {
      "block_id": "loop-continue-1",
      "block_type": "process",
      "title": "Loop Continue",
      "description": "Continue multi-input loop",
      "position_x": 2640,
      "position_y": 240,
      "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": "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": "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-entry",
      "element_type": "group",
      "title": "Entry & Setup",
      "color": "#4589ff",
      "position_x": 48,
      "position_y": 136,
      "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": 136,
      "width": 1760,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "navigate-1",
          "wait-for-page-load-1",
          "sleep-1",
          "wait-for-element-1"
        ]
      }
    },
    {
      "id": "group-interaction",
      "element_type": "group",
      "title": "Interaction",
      "color": "#a56eff",
      "position_x": 1488,
      "position_y": 136,
      "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": 136,
      "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": 136,
      "width": 380,
      "height": 296,
      "z_index": 20,
      "data": {
        "memberBlockIds": [
          "loop-continue-1"
        ]
      }
    },
    {
      "id": "note-overview",
      "element_type": "note",
      "title": "Overview",
      "content": "Best-effort YouTube Community scraper for public channel Community URLs and direct /post/ URLs. Pagination/navigation strategy: uses navigate.urls[] for multiple input URLs and appends one extracted record per loaded URL; the JavaScript block creates a guaranteed extraction row from visible YouTube post DOM, metadata, and page text. YouTube may hide Community feeds, require login, or show bot checks; in those cases the row records the input URL and available blocker/status text rather than silently exporting nothing.",
      "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: `(() => {\n  const clean = (s) => String(s || '').replace(/\\s+/g, ' ').trim();\n  const abs = (u) => { ...` Verify in browser if results are empty.",
      "color": "#ee5396",
      "position_x": 1760,
      "position_y": 220,
      "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-community-post`. Confirm row count > 0 before running at scale.",
      "color": "#ee5396",
      "position_x": 2480,
      "position_y": 220,
      "width": 340,
      "height": 112,
      "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": 220,
      "width": 340,
      "height": 123,
      "z_index": 22,
      "data": {
        "block_id": "loop-continue-1"
      }
    }
  ]
}