{
  "id": "VKCnlRbfmd3GmvXZ",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "GitHub Trending Dev Tools",
  "tags": [
    {
      "id": "EwNZ2taImfwXNIV6",
      "name": "github",
      "createdAt": "2025-12-11T12:51:23.257Z",
      "updatedAt": "2025-12-11T12:51:23.257Z"
    },
    {
      "id": "851ehcNzkj5GLim3",
      "name": "scraping",
      "createdAt": "2025-12-11T12:52:06.933Z",
      "updatedAt": "2025-12-11T12:52:06.933Z"
    },
    {
      "id": "lZKSh2IoxHklnOUw",
      "name": "ScrapeOps",
      "createdAt": "2025-10-20T20:27:13.410Z",
      "updatedAt": "2025-10-20T20:27:13.410Z"
    },
    {
      "id": "VtV62IGgHutlusaN",
      "name": "devtools",
      "createdAt": "2025-12-11T12:52:38.192Z",
      "updatedAt": "2025-12-11T12:52:38.192Z"
    },
    {
      "id": "yh7XX4GObvd3np2S",
      "name": "google sheets",
      "createdAt": "2025-11-06T11:10:29.979Z",
      "updatedAt": "2025-11-06T11:10:29.979Z"
    },
    {
      "id": "5DcXvqnjGQotjqKa",
      "name": "trending",
      "createdAt": "2025-12-11T12:52:53.810Z",
      "updatedAt": "2025-12-11T12:52:53.810Z"
    }
  ],
  "nodes": [
    {
      "id": "f0f306fd-4fe7-46db-8761-7980eef55bdb",
      "name": "Cron (daily/weekly)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1104,
        -80
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ab4b695b-de79-4729-9463-8b59c047be2c",
      "name": "Build URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        -656,
        -80
      ],
      "parameters": {
        "jsCode": "// Build URLs (n8n Code node)\n\nconst input = items[0]?.json ?? {};\n\n// since: daily | weekly | monthly\nconst since = String(input.since || 'daily').toLowerCase().trim();\n\n// languages_csv example: \"any,JavaScript,TypeScript,Python,Go,Rust,C++,Java\"\nconst languagesCsv = String(input.languages_csv || 'any').trim();\n\n// Normalize \"since\"\nconst allowedSince = new Set(['daily', 'weekly', 'monthly']);\nconst sinceFinal = allowedSince.has(since) ? since : 'daily';\n\n// Parse languages\nlet langs = languagesCsv\n  .split(',')\n  .map(s => s.trim())\n  .filter(Boolean);\n\n// remove duplicates\nlangs = [...new Set(langs.map(l => l.toLowerCase()))];\n\n// map normalized language -> GitHub path slug\nconst langSlugMap = {\n  'any': null,\n  'javascript': 'javascript',\n  'typescript': 'typescript',\n  'python': 'python',\n  'go': 'go',\n  'rust': 'rust',\n  'c++': 'c%2B%2B',     // URL encoded\n  'cpp': 'c%2B%2B',     // allow cpp too\n  'java': 'java',\n};\n\nconst nowIso = new Date().toISOString();\n\n// week_id like 2025-W50\nfunction getWeekId(d = new Date()) {\n  const date = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()));\n  const dayNum = date.getUTCDay() || 7;\n  date.setUTCDate(date.getUTCDate() + 4 - dayNum);\n  const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));\n  const weekNo = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);\n  return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;\n}\n\nconst weekId = getWeekId(new Date());\n\n// Build URLs\nconst urls = [];\nfor (const l of langs) {\n  const slug = langSlugMap[l];\n  // skip unknown languages safely\n  if (slug === undefined) continue;\n\n  const base = slug ? `https://github.com/trending/${slug}` : `https://github.com/trending`;\n  const url = `${base}?since=${encodeURIComponent(sinceFinal)}`;\n\n  urls.push({\n    url,\n    source_url: url,\n    page_index: urls.length,\n    since: sinceFinal,\n    captured_at: nowIso,\n    week_id: weekId,\n    // keep your sheet config if you want:\n    sheet_id: input.sheet_id,\n    raw_sheet_name: input.raw_sheet_name || 'trending_raw',\n    brief_sheet_name: input.brief_sheet_name || 'weekly_brief',\n  });\n}\n\nreturn urls.map(x => ({ json: x }));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "fbb4a5fc-1222-48c0-a9dd-2efd06472d66",
      "name": "Split URLs (loop)",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -400,
        -80
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 2
    },
    {
      "id": "e00a10e3-0650-467f-bda5-1ebd855d1779",
      "name": "ScrapeOps: Fetch HTML",
      "type": "@scrapeops/n8n-nodes-scrapeops.ScrapeOps",
      "position": [
        -176,
        -80
      ],
      "parameters": {
        "url": "={{$json.url}}",
        "advancedOptions": {}
      },
      "credentials": {
        "scrapeOpsApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "2d61a915-3d69-4fff-a179-150362f26a3c",
      "name": "Polite Delay",
      "type": "n8n-nodes-base.wait",
      "position": [
        64,
        -80
      ],
      "parameters": {
        "unit": "seconds",
        "amount": 2
      },
      "typeVersion": 1
    },
    {
      "id": "3d681415-dd9a-454d-a108-dc1c1ec9cdb8",
      "name": "Extract Trending Repos",
      "type": "n8n-nodes-base.code",
      "position": [
        320,
        -80
      ],
      "parameters": {
        "jsCode": "const html = $json.data || $json.body || $json || '';\nconst upstream = $items('Split URLs (loop)')[0]?.json || {};\n\nconst articles = [...String(html).matchAll(/<article[\\s\\S]*?<\\/article>/g)];\nlet rank = 0;\n\nconst cleanText = (s) => String(s || '').replace(/<[^>]+>/g,' ').replace(/\\s+/g,' ').trim();\n\nfunction extractTightDescription(block) {\n  const preferred = block.match(/<p[^>]*class=\"[^\"]*\\bcol-9\\b[^\"]*\"[^>]*>([\\s\\S]*?)<\\/p>/i);\n  if (preferred) return cleanText(preferred[1]);\n\n  const afterTitle = block.match(/<h2[\\s\\S]*?<\\/h2>[\\s\\S]*?<p[^>]*>([\\s\\S]*?)<\\/p>/i);\n  if (afterTitle) return cleanText(afterTitle[1]);\n\n  const anyP = block.match(/<p[^>]*>([\\s\\S]*?)<\\/p>/i);\n  return anyP ? cleanText(anyP[1]) : '';\n}\n\nfunction extractCountFromAnchor(block, hrefSuffix) {\n  const aMatch = block.match(new RegExp(`(<a[^>]*href=\\\"\\\\/[^\"]+\\\\/${hrefSuffix}\\\"[^>]*>[\\\\s\\\\S]*?<\\\\/a>)`, 'i'));\n  if (!aMatch) return '';\n  const text = cleanText(aMatch[1]);\n  const numMatch = text.match(/([0-9][0-9.,]*\\s*[kKmM]?)/);\n  return numMatch ? numMatch[1].replace(/\\s+/g,'') : '';\n}\n\nconst extracted = [];\nfor (const m of articles) {\n  const block = m[0];\n\n  const h2RepoMatch = block.match(/<h2[^>]*>[\\s\\S]*?<a[^>]*href=\\\"\\/([^\\/\\s]+)\\/([^\\/\"]+)\\\"/i);\n  if (!h2RepoMatch) continue;\n\n  const owner = h2RepoMatch[1].trim();\n  const repo = h2RepoMatch[2].trim();\n  if (!owner || !repo) continue;\n  if (owner.toLowerCase() === 'sponsors') continue;\n\n  rank += 1;\n\n  const full_name = `${owner}/${repo}`;\n  const repo_url = `https://github.com/${full_name}`;\n  const description = extractTightDescription(block);\n\n  const langMatch = block.match(/itemprop=\\\"programmingLanguage\\\"[^>]*>([^<]+)/i);\n  const language = langMatch ? cleanText(langMatch[1]) : '';\n\n  const stars_total_raw = extractCountFromAnchor(block, 'stargazers');\n  const forks_total_raw = extractCountFromAnchor(block, 'forks');\n\n  const starsInPeriodMatch = block.match(/([0-9.,kKmM]+)\\s+stars\\s+this\\s+week/i) || block.match(/([0-9.,kKmM]+)\\s+stars\\s+today/i) || block.match(/([0-9.,kKmM]+)\\s+stars\\s+this\\s+month/i);\n\n  extracted.push({\n    ...upstream,\n    rank_on_page: rank,\n    owner,\n    repo,\n    full_name,\n    repo_url,\n    description,\n    language,\n    stars_total_raw,\n    forks_total_raw,\n    stars_in_period_raw: starsInPeriodMatch?.[1] || ''\n  });\n}\n\nreturn extracted.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "6df21227-e87a-4db6-96a1-efe0b60e5b58",
      "name": "Normalize + Enrich-from-page",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        -80
      ],
      "parameters": {
        "jsCode": "function parseNumber(s) {\n  if (!s) return 0;\n  const t = String(s).trim().toLowerCase();\n  if (t.endsWith('k')) return Math.round(parseFloat(t.slice(0, -1)) * 1000);\n  if (t.endsWith('m')) return Math.round(parseFloat(t.slice(0, -1)) * 1000000);\n  return parseInt(t.replace(/,/g, ''), 10) || 0;\n}\n\nreturn items.map((item) => {\n  const j = item.json || {};\n\n  const upstreamKeywords = (j.keywords || []).map(k => String(k).toLowerCase());\n  const langSet = new Set((j.languages || []).map(l => String(l).toLowerCase()));\n\n  const stars_total = parseNumber(j.stars_total_raw);\n  const forks_total = parseNumber(j.forks_total_raw);\n  const stars_in_period = parseNumber(j.stars_in_period_raw);\n\n  const text = `${j.full_name || ''} ${j.description || ''}`.toLowerCase();\n  const matches = upstreamKeywords.filter(k => k && text.includes(k));\n  const uniqueMatches = [...new Set(matches)];\n  const is_dev_tool = uniqueMatches.length > 0;\n\n  let keyword_bonus = 0;\n  if (is_dev_tool) keyword_bonus = Math.min(20, 10 + Math.max(0, (uniqueMatches.length - 1) * 2));\n  const language_bonus = (j.language && langSet.has(String(j.language).toLowerCase())) ? 3 : 0;\n  const score = (stars_in_period * 2) + keyword_bonus + language_bonus;\n\n  const dedupe_key = `${j.since}|${j.full_name}`;\n\n\n  return {\n    json: {\n      captured_at: j.captured_at,\n      week_id: j.week_id,\n      since: j.since,\n      source_url: j.source_url,\n      rank_on_page: j.rank_on_page,\n      full_name: j.full_name,\n      repo_url: j.repo_url,\n      owner: j.owner,\n      repo: j.repo,\n      description: j.description,\n      language: j.language,\n      stars_total,\n      forks_total,\n      stars_in_period,\n      keyword_matches: uniqueMatches.join(', '),\n      is_dev_tool,\n      score,\n      dedupe_key,\n      sheet_id: j.sheet_id,\n      raw_sheet_name: j.raw_sheet_name,\n      brief_sheet_name: j.brief_sheet_name\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "db2a227b-66d9-4c1f-8a25-21dba32aa701",
      "name": "Write to Google Sheets (raw tab)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        864,
        -80
      ],
      "parameters": {
        "columns": {
          "value": {
            "repo": "={{$json.repo}}",
            "owner": "={{$json.owner}}",
            "score": "={{$json.score}}",
            "since": "={{$json.since}}",
            "week_id": "={{$json.week_id}}",
            "language": "={{$json.language}}",
            "repo_url": "={{$json.repo_url}}",
            "full_name": "={{$json.full_name}}",
            "dedupe_key": "={{$json.dedupe_key}}",
            "source_url": "={{$json.source_url}}",
            "captured_at": "={{$json.captured_at}}",
            "description": "={{$json.description}}",
            "forks_total": "={{$json.forks_total}}",
            "stars_total": "={{$json.stars_total}}",
            "rank_on_page": "={{$json.rank_on_page}}",
            "stars_in_period": "={{$json.stars_in_period}}"
          },
          "schema": [
            {
              "id": "captured_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "captured_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "week_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "week_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "since",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "since",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "source_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "source_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rank_on_page",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rank_on_page",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "full_name",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "full_name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repo_url",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "repo_url",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "owner",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "owner",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "repo",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "repo",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "description",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "language",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "language",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stars_total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stars_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "forks_total",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "forks_total",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "stars_in_period",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "stars_in_period",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "score",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "score",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "dedupe_key",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "dedupe_key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "dedupe_key"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "trending_raw"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "d3b522dc-93f3-403a-a923-8c040920710b",
      "name": "Write to Google Sheets (weekly tab)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        400,
        336
      ],
      "parameters": {
        "columns": {
          "value": {
            "notes": "={{$json.notes}}",
            "week_id": "={{$json.week_id}}",
            "top_repos": "={{$json.top_repos}}",
            "top_themes": "={{$json.top_themes}}",
            "generated_at": "={{$json.generated_at}}"
          },
          "schema": [
            {
              "id": "week_id",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "week_id",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "generated_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "generated_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_repos",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "top_repos",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "top_themes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "top_themes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "notes",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "notes",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "message_sent_to",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "message_sent_to",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "link_to_raw_filter",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "link_to_raw_filter",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 723657204,
          "cachedResultUrl": "https://docs.google.com/YOUR_AWS_SECRET_KEY_HERE-g3AdFFy55Ld30gPD-E/edit#gid=723657204",
          "cachedResultName": "weekly_brief"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/YOUR_AWS_SECRET_KEY_HERE-g3AdFFy55Ld30gPD-E/edit?usp=sharing"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "c735eb78-72a1-4183-84fe-44c78746f9f3",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1808,
        -608
      ],
      "parameters": {
        "width": 656,
        "height": 800,
        "content": "# GitHub Trending Tracker (Daily/Weekly/Monthly) \u2192 Google Sheets\n\nThis workflow pulls **GitHub Trending repositories** for a chosen time window (**daily / weekly / monthly**) and writes results to a Google Sheet. It uses **ScrapeOps (community node)** to fetch HTML reliably, then parses the page into structured rows and **upserts** them into Sheets using a `dedupe_key` so you don\u2019t get duplicates.\n\n## How it works\n1) **Set Inputs** defines `since` (daily/weekly/monthly) and optional languages.  \n2) **Build URLs** creates GitHub Trending URLs for the chosen window.  \n3) **ScrapeOps: Fetch HTML** downloads the Trending page HTML.  \n4) **Extract Trending Repos** parses repositories + rank + metadata from HTML.  \n5) **Normalize + Enrich** cleans numbers, calculates score, creates `dedupe_key`.  \n6) **Google Sheets (Append or Update Row)** writes rows and updates existing ones by `dedupe_key`.\n\n## Setup steps\n- Create a free ScrapeOps account & API key: https://scrapeops.io/app/register/n8n  \n- Add ScrapeOps credentials in n8n (community node). Docs: https://scrapeops.io/docs/n8n/overview/  \n- Duplicate the [Google Sheet](https://docs.google.com/spreadsheets/d/1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E/edit?usp=sharing) with matching tabs and columns for node mapping.\n- In **Write to Google Sheets**, set operation to **Append or Update Row** and match on **dedupe_key**.\n\n## Customization tips\n- Change `since` to daily/weekly/monthly in **Set Inputs**.\n- Add languages in `languages_csv` (e.g. `any,python,go,rust`).\n- Adjust delay and parsing if GitHub\u2019s HTML changes.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "a0cef4cb-1d76-4f67-bf07-cf97c3c281b0",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -928,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 304,
        "content": "## Inputs & URL Builder\nSet `since` + languages, then generate GitHub Trending URLs.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "feae26f1-0889-4a52-b705-62a5d55fb2b8",
      "name": "Set Inputs",
      "type": "n8n-nodes-base.set",
      "position": [
        -896,
        -80
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "since",
              "value": "monthly"
            },
            {
              "name": "languages_csv",
              "value": "any"
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 2
    },
    {
      "id": "7c9908e4-a0b1-4a6b-9e65-7fbc91f398e1",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -256,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 320,
        "content": "## Scrape Trending Pages\nFetch HTML via ScrapeOps, then wait briefly to be polite.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c04937d5-93d4-4f1d-8925-5451faecd8d2",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        272,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 320,
        "content": "## Parse & Normalize\nExtract repos + rank from HTML and normalize numbers + scoring + dedupe key.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "101bb8bd-8bad-44f8-881f-cdb22756e186",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        800,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 320,
        "content": "## Save Results\nUpsert rows into Google Sheets using `dedupe_key` to avoid duplicates.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "bb9fefc0-51ca-4006-b1e9-8321923243c9",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -112,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 320,
        "content": "## Weekly Brief (Optional)\nReads raw data and writes a summary into the weekly_brief tab.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "0f912457-142c-4a91-af13-686946f1aad8",
      "name": "Read raw (for weekly)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -64,
        336
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "trending_raw"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1GhCbbPilZXMVDox0hQ0Ncqf5-g3AdFFy55Ld30gPD-E"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 3,
      "alwaysOutputData": true
    },
    {
      "id": "20904aec-177e-4174-a859-bdce20b32e17",
      "name": "Generate Weekly",
      "type": "n8n-nodes-base.code",
      "position": [
        176,
        336
      ],
      "parameters": {
        "jsCode": "const weekId = ($items('Build URLs')[0]?.json?.week_id) || '';\nlet rows = items.map(x => x.json || {});\nif (rows.length === 1 && Array.isArray(rows[0].data)) rows = rows[0].data;\n\nconst weekRows = rows.filter(r => String(r.week_id || '') === String(weekId));\nconst devRows = weekRows.filter(r => String(r.is_dev_tool).toLowerCase() === 'true');\n\nconst top = [...weekRows].sort((a,b)=> (Number(b.score)||0) - (Number(a.score)||0)).slice(0,10);\n\nconst top_repos = top.map(r => r.repo_url).join('\\n');\nconst top_10_list = top.map((r,i)=> `${i+1}) ${r.full_name} \u2014 stars_in_period=${r.stars_in_period||0}, lang=${r.language||''}, score=${r.score||0}`).join('\\n');\n\nconst themeCounts = {};\nfor (const r of devRows) {\n  const parts = String(r.keyword_matches || '').split(',').map(s=>s.trim()).filter(Boolean);\n  for (const p of parts) themeCounts[p] = (themeCounts[p] || 0) + 1;\n}\nconst top_themes = Object.entries(themeCounts).sort((a,b)=>b[1]-a[1]).slice(0,20).map(([k,v])=>`${k}(${v})`).join(', ');\n\nconst why = top.length\n  ? `Top repo this ${$items('Build URLs')[0]?.json?.since || 'period'}: ${top[0].full_name} (${top[0].language || 'n/a'}) with ${top[0].stars_in_period || 0} stars in period.`\n  : '';\n\nreturn [{\n  json: {\n    week_id: weekId,\n    generated_at: new Date().toISOString(),\n    total_repos_captured: weekRows.length,\n    total_devtools_flagged: devRows.length,\n    top_10_list,\n    top_repos,\n    top_themes,\n    notes: why,\n    message_sent_to: '',\n    link_to_raw_filter: ''\n  }\n}];"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "d8af9578-5963-468b-94d8-788fd478ff2d",
  "connections": {
    "Build URLs": {
      "main": [
        [
          {
            "node": "Split URLs (loop)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Inputs": {
      "main": [
        [
          {
            "node": "Build URLs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Read raw (for weekly)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Polite Delay": {
      "main": [
        [
          {
            "node": "Extract Trending Repos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Weekly": {
      "main": [
        [
          {
            "node": "Write to Google Sheets (weekly tab)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split URLs (loop)": {
      "main": [
        [
          {
            "node": "ScrapeOps: Fetch HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cron (daily/weekly)": {
      "main": [
        [
          {
            "node": "Set Inputs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read raw (for weekly)": {
      "main": [
        [
          {
            "node": "Generate Weekly",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "ScrapeOps: Fetch HTML": {
      "main": [
        [
          {
            "node": "Polite Delay",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Trending Repos": {
      "main": [
        [
          {
            "node": "Normalize + Enrich-from-page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + Enrich-from-page": {
      "main": [
        [
          {
            "node": "Write to Google Sheets (raw tab)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}