{
  "id": "tvYjcAp0YmNRddDi",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "AI Internal-Linking Suggester (GSC + Crawl) \u2192 Google Sheet",
  "tags": [],
  "nodes": [
    {
      "id": "7b47f192-9344-4326-91a1-6f5aa1659eee",
      "name": "Run",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        0,
        208
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "c16c50af-3025-4c05-b105-a3db3c7c604c",
      "name": "Config",
      "type": "n8n-nodes-base.set",
      "notes": "Set the site, its sitemap URL, and the GSC property (URL-prefix). maxPages caps the crawl.",
      "position": [
        224,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "siteUrl",
              "type": "string",
              "value": "https://pocketfmmodapk.com/"
            },
            {
              "id": "a2",
              "name": "sitemapUrl",
              "type": "string",
              "value": "https://pocketfmmodapk.com/post-sitemap.xml"
            },
            {
              "id": "a3",
              "name": "gscProperty",
              "type": "string",
              "value": "https://pocketfmmodapk.com/"
            },
            {
              "id": "a4",
              "name": "maxPages",
              "type": "number",
              "value": 50
            },
            {
              "id": "a5",
              "name": "minConfidence",
              "type": "number",
              "value": 0.6
            },
            {
              "id": "a6",
              "name": "maxSuggestionsPerPage",
              "type": "number",
              "value": 3
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "78ac445b-4e28-4746-aa11-02cefb305a7a",
      "name": "GSC Queries",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Search Analytics: page + query rows for the last 90 days. Uses the Google service-account credential.",
      "maxTries": 3,
      "position": [
        448,
        208
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($('Config').item.json.gscProperty) }}/searchAnalytics/query",
        "method": "POST",
        "options": {
          "timeout": 30000
        },
        "jsonBody": "={{ JSON.stringify({ startDate: $now.minus({ days: 90 }).toFormat('yyyy-MM-dd'), endDate: $now.toFormat('yyyy-MM-dd'), dimensions: ['page','query'], rowLimit: 5000 }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        },
        "googleOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "waitBetweenTries": 5000
    },
    {
      "id": "71ecd2d8-9ae6-4b02-9840-d3433b448d43",
      "name": "Fetch Sitemap",
      "type": "n8n-nodes-base.httpRequest",
      "maxTries": 2,
      "position": [
        672,
        208
      ],
      "parameters": {
        "url": "={{ $('Config').item.json.sitemapUrl }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "retryOnFail": true,
      "typeVersion": 4.4,
      "waitBetweenTries": 5000
    },
    {
      "id": "acf0d376-5456-4c78-8655-df5a8ae14d1b",
      "name": "Parse Sitemap XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        896,
        208
      ],
      "parameters": {
        "options": {
          "explicitArray": false
        }
      },
      "typeVersion": 1
    },
    {
      "id": "54ecc620-e3de-4d4b-8585-1c224ed461bc",
      "name": "Extract Page URLs",
      "type": "n8n-nodes-base.code",
      "position": [
        1136,
        208
      ],
      "parameters": {
        "jsCode": "const root = $input.first().json || {};\nfunction locOf(u){ if(!u) return null; if(typeof u === 'string') return u; if(u.loc){ return (typeof u.loc === 'string') ? u.loc : (u.loc._ || null); } return null; }\nlet urls = [];\nif (root.urlset && root.urlset.url) {\n  const arr = Array.isArray(root.urlset.url) ? root.urlset.url : [root.urlset.url];\n  urls = arr.map(locOf).filter(Boolean);\n} else if (root.sitemapindex && root.sitemapindex.sitemap) {\n  const arr = Array.isArray(root.sitemapindex.sitemap) ? root.sitemapindex.sitemap : [root.sitemapindex.sitemap];\n  urls = arr.map(locOf).filter(Boolean);\n}\nconst seen = {};\nconst out = [];\nfor (let i=0;i<urls.length;i++){ const u = urls[i]; if(u && !seen[u]){ seen[u] = 1; out.push({ json: { url: u } }); } }\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "31317623-c672-4b75-bd7e-47a68aa239f2",
      "name": "Limit Pages",
      "type": "n8n-nodes-base.limit",
      "position": [
        1392,
        208
      ],
      "parameters": {
        "maxItems": "={{ $('Config').item.json.maxPages }}"
      },
      "typeVersion": 1
    },
    {
      "id": "5ec2af75-d946-4e71-a4d0-ae2f90241914",
      "name": "Crawl Page",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Fetches each page HTML to extract internal links. neverError keeps the run going on 404s; onError continues on network errors.",
      "onError": "continueRegularOutput",
      "position": [
        1616,
        208
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {
              "neverError": true,
              "responseFormat": "text"
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-internal-link-bot/1.0)"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "6502d00e-bc2d-413c-b50a-c3d0abc5d38b",
      "name": "Build Map + Orphans",
      "type": "n8n-nodes-base.code",
      "position": [
        1840,
        208
      ],
      "parameters": {
        "jsCode": "const cfg = $('Config').first().json || {};\nconst siteUrl = cfg.siteUrl || '';\nlet siteHost = siteUrl;\ntry { siteHost = siteUrl.replace(/^https?:\\/\\//, '').replace(/\\/.*$/, '').replace(/^www\\./, '') || siteUrl; } catch (e) {}\n\nconst pages = $('Limit Pages').all().map(function(x){ return x.json.url; });\nconst crawls = $input.all();\n\nconst gsc = $('GSC Queries').first().json || {};\nconst gscRows = gsc.rows || [];\nconst qByPage = {};\nfor (let i=0;i<gscRows.length;i++){\n  const r = gscRows[i]; const keys = r.keys || [];\n  const page = keys[0]; const query = keys[1];\n  if(!page) continue;\n  if(!qByPage[page]) qByPage[page] = [];\n  qByPage[page].push({ q: query, imp: r.impressions || 0 });\n}\nfunction topQueries(page){\n  const list = qByPage[page] || [];\n  list.sort(function(a,b){ return b.imp - a.imp; });\n  return list.slice(0,5).map(function(x){ return x.q; }).filter(Boolean);\n}\n\nfunction canon(u){ return (u||'').split('#')[0].split('?')[0].replace(/\\/$/, ''); }\n\nfunction normalize(href){\n  if(!href) return null;\n  href = ('' + href).trim();\n  if(!href) return null;\n  const low = href.toLowerCase();\n  if(href.charAt(0) === '#' || low.indexOf('mailto:') === 0 || low.indexOf('tel:') === 0 || low.indexOf('javascript:') === 0) return null;\n  let abs = href;\n  if(href.indexOf('//') === 0){ abs = 'https:' + href; }\n  else if(href.charAt(0) === '/'){ abs = 'https://' + siteHost + href; }\n  else if(href.indexOf('http') !== 0){ abs = 'https://' + siteHost + '/' + href; }\n  const bare = abs.replace(/^https?:\\/\\//, '').replace(/^www\\./, '');\n  if(bare.indexOf(siteHost) !== 0) return null;\n  return abs.split('#')[0].split('?')[0];\n}\n\nfunction extractLinks(html){\n  if(!html || typeof html !== 'string') return [];\n  const out = [];\n  const re = /href\\s*=\\s*[\"']([^\"']+)[\"']/gi;\n  let m;\n  while((m = re.exec(html)) !== null){ const n = normalize(m[1]); if(n) out.push(n); }\n  return out;\n}\n\nconst inbound = {};\nconst existing = {};\nconst pageInfo = [];\nfor (let i=0;i<pages.length;i++){\n  const url = pages[i];\n  const html = (crawls[i] && crawls[i].json) ? crawls[i].json.data : '';\n  const links = extractLinks(html);\n  const srcC = canon(url);\n  const seen = {};\n  for (let j=0;j<links.length;j++){\n    const tgtC = canon(links[j]);\n    if(tgtC === srcC) continue;\n    existing[srcC + '|' + tgtC] = true;\n    if(!seen[tgtC]){ seen[tgtC] = 1; inbound[tgtC] = (inbound[tgtC] || 0) + 1; }\n  }\n  pageInfo.push({ url: url, canon: srcC, topQueries: topQueries(url), outbound: links.length });\n}\nfor (let i=0;i<pageInfo.length;i++){ const p = pageInfo[i]; p.inbound = inbound[p.canon] || 0; p.isOrphan = (p.inbound === 0); }\n\nconst validUrls = pageInfo.map(function(p){ return p.url; });\nconst orphans = pageInfo.filter(function(p){ return p.isOrphan; }).map(function(p){ return p.url; });\nconst llmPages = pageInfo.map(function(p){ return { url: p.url, topQueries: p.topQueries, isOrphan: p.isOrphan }; });\n\nreturn [{ json: {\n  siteHost: siteHost,\n  pages: pageInfo,\n  llmPages: llmPages,\n  orphans: orphans,\n  validUrls: validUrls,\n  existingLinks: Object.keys(existing),\n  minConfidence: (cfg.minConfidence !== undefined ? cfg.minConfidence : 0.6),\n  maxPerPage: (cfg.maxSuggestionsPerPage !== undefined ? cfg.maxSuggestionsPerPage : 3)\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "1cbae9f7-28a7-486f-bef4-103f41079b07",
      "name": "Suggest Internal Links",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        2128,
        208
      ],
      "parameters": {
        "text": "=You are an expert SEO internal-linking assistant for the site {{ $json.siteHost }}.\n\nBelow is a JSON list of the site's pages. Each page has: url, topQueries (the real Google Search queries it ranks for), and isOrphan (true = no internal links currently point to it).\n\nPropose the most valuable internal links BETWEEN these pages.\n\nSTRICT RULES:\n- sourceUrl and targetUrl MUST be copied EXACTLY from the url values in the list below. NEVER use example.com or any placeholder, sample, or invented URL.\n- Only propose links between pages that are genuinely topically related.\n- anchorText MUST be one of the TARGET page's topQueries, phrased to read naturally in a sentence.\n- Never link a page to itself.\n- confidence is a number from 0 to 1 (topical relevance and SEO value).\n- reason is one short sentence.\n- Return between 3 and 10 of the strongest suggestions.\n- Output ONLY the JSON object described by the schema. No markdown, no code fences, no commentary.\n\nAllowed URLs (use only these exact strings):\n{{ JSON.stringify($json.validUrls) }}\n\nPages:\n{{ JSON.stringify($json.llmPages) }}",
        "batching": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 1.9
    },
    {
      "id": "e50f6018-26ed-4ae0-ae23-4c373a084b48",
      "name": "Claude (OpenRouter)",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
      "position": [
        2048,
        384
      ],
      "parameters": {
        "model": "anthropic/claude-sonnet-4.6",
        "options": {
          "temperature": 0.2
        }
      },
      "credentials": {
        "openRouterApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "54f02469-2715-4e51-a5c8-11626e8edbba",
      "name": "Suggestions Schema",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        2160,
        528
      ],
      "parameters": {
        "autoFix": true,
        "schemaType": "manual",
        "inputSchema": "{\"type\":\"object\",\"properties\":{\"suggestions\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"sourceUrl\":{\"type\":\"string\"},\"targetUrl\":{\"type\":\"string\"},\"anchorText\":{\"type\":\"string\"},\"confidence\":{\"type\":\"number\"},\"reason\":{\"type\":\"string\"}},\"required\":[\"sourceUrl\",\"targetUrl\",\"anchorText\",\"confidence\",\"reason\"]}}},\"required\":[\"suggestions\"]}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "f3c9b8df-8172-47e9-802e-109ba0caf4b2",
      "name": "Validate & Dedupe",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        208
      ],
      "parameters": {
        "jsCode": "const map = $('Build Map + Orphans').first().json || {};\nfunction canon(u){ return (u||'').split('#')[0].split('?')[0].replace(/\\/$/, ''); }\nconst validSet = {};\n(map.validUrls || []).forEach(function(u){ validSet[canon(u)] = true; });\nconst existing = {};\n(map.existingLinks || []).forEach(function(k){ existing[k] = true; });\nconst orphanSet = {};\n(map.orphans || []).forEach(function(u){ orphanSet[canon(u)] = true; });\nconst minConf = (map.minConfidence !== undefined ? map.minConfidence : 0.6);\nconst maxPer = (map.maxPerPage !== undefined ? map.maxPerPage : 3);\n\nconst parsed = $input.first().json.output || {};\nconst sugg = parsed.suggestions || [];\n\nconst perSource = {};\nconst rows = [];\nconst today = $now.toFormat('yyyy-MM-dd');\nfor (let i=0;i<sugg.length;i++){\n  const s = sugg[i];\n  if(!s || !s.sourceUrl || !s.targetUrl || !s.anchorText) continue;\n  const sc = canon(s.sourceUrl); const tc = canon(s.targetUrl);\n  if(sc === tc) continue;\n  if(!validSet[sc] || !validSet[tc]) continue;\n  let conf = (typeof s.confidence === 'number') ? s.confidence : parseFloat(s.confidence);\n  if(isNaN(conf)) conf = 0;\n  if(conf < minConf) continue;\n  if(!perSource[sc]) perSource[sc] = 0;\n  if(perSource[sc] >= maxPer) continue;\n  perSource[sc]++;\n  rows.push({ json: {\n    date: today,\n    sourceUrl: s.sourceUrl,\n    targetUrl: s.targetUrl,\n    anchorText: s.anchorText,\n    confidence: Math.round(conf * 100) / 100,\n    reason: s.reason || '',\n    alreadyLinked: existing[sc + '|' + tc] ? 'yes' : 'no',\n    targetIsOrphan: orphanSet[tc] ? 'yes' : 'no',\n    status: 'pending'\n  }});\n}\nreturn rows;"
      },
      "typeVersion": 2
    },
    {
      "id": "a2c3e9da-6c49-484c-aca6-efd7df50030b",
      "name": "Append Suggestions to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Attach your spreadsheet + tab here, then set Columns to \"Map Automatically\". Input fields: date, sourceUrl, targetUrl, anchorText, confidence, reason, targetIsOrphan, status.",
      "position": [
        2704,
        208
      ],
      "parameters": {
        "columns": {
          "value": {
            "date": "={{ $json.date }}",
            "reason": "={{ $json.reason }}",
            "status": "={{ $json.status }}",
            "sourceUrl": "={{ $json.sourceUrl }}",
            "targetUrl": "={{ $json.targetUrl }}",
            "anchorText": "={{ $json.anchorText }}",
            "confidence": "={{ $json.confidence }}",
            "alreadyLinked": "={{ $json.alreadyLinked }}",
            "targetIsOrphan": "={{ $json.targetIsOrphan }}"
          },
          "schema": [
            {
              "id": "date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "sourceUrl",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "sourceUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "targetUrl",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "targetUrl",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "anchorText",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "anchorText",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "confidence",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "confidence",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "reason",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "reason",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "targetIsOrphan",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "targetIsOrphan",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "alreadyLinked",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "alreadyLinked",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1vZDJtBCLS_TARMxaES384NRppy_GXhbljB21f4Eq1_s/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1vZDJtBCLS_TARMxaES384NRppy_GXhbljB21f4Eq1_s"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "70fb7e3d-10b3-4c8e-84fb-7e7f78e16e05",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        -384
      ],
      "parameters": {
        "width": 580,
        "height": 1180,
        "content": "# \ud83d\udd17 AI Internal-Linking Suggester (GSC + Crawl)\n\nSuggests high-value **internal links** using your real **Google Search Console** queries as anchor text, and flags **orphan pages** (pages nothing links to). Suggestions land in a **Google Sheet** for review.\n\n## \u2699\ufe0f How it works\n1. **Run** (manual).\n2. **Config** \u2014 site, sitemap URL, GSC property (URL-prefix), page cap, filters.\n3. **GSC Queries** \u2014 page+query rows (90 days) via the Google service account.\n4. **Fetch + Parse Sitemap** \u2192 page URLs \u2192 **Limit** to maxPages.\n5. **Crawl Page** \u2014 fetches each page's HTML to map existing internal links.\n6. **Build Map + Orphans** \u2014 link graph, orphan detection, page\u2192top-queries map.\n7. **Suggest Internal Links** \u2014 Claude (OpenRouter) proposes links as strict JSON (anchor = a real target query), with an auto-fix parser.\n8. **Validate & Dedupe** \u2014 drops invented urls + low-confidence, caps per page, flags alreadyLinked.\n9. **Append to Sheet** \u2014 one row per suggestion.\n\n## \ud83d\udee0\ufe0f Setup\n\u2022 Confirm the **Google service-account** credential on GSC Queries has property access.\n\u2022 Confirm **OpenRouter** + **Google Sheets** credentials.\n\u2022 On **Append Suggestions to Sheet**: pick your spreadsheet + tab (mapping is automatic).\n\u2022 Adjust **Config** (page cap 50, min confidence 0.6, max 3/page).\n\n## \ud83d\udcdd Notes\n\u2022 Point Config.sitemapUrl at a **urlset** sitemap (a list of pages). If yours is a sitemap index, use a child sitemap.\n\u2022 Anchor text is always one of the target page's actual GSC queries.\n\u2022 On a small fully nav-linked site, suggestions come back alreadyLinked=yes with no orphans \u2014 expected; orphan value shows on larger sites."
      },
      "typeVersion": 1
    },
    {
      "id": "6d5d9271-b5be-481d-a874-02645e03b764",
      "name": "Section 1 - Start",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -48,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 364,
        "content": "## 1 - Start & settings\n**Edit the Config node** to set the site, sitemap URL, GSC property, page cap and filters. Then click **Run**."
      },
      "typeVersion": 1
    },
    {
      "id": "2f627401-6726-4257-987c-04d8537f11da",
      "name": "Section 2 - Inputs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 904,
        "height": 364,
        "content": "## 2 - Gather inputs\nPulls **GSC** page+query data (90 days) and the **sitemap**, then parses it into a clean list of page URLs."
      },
      "typeVersion": 1
    },
    {
      "id": "165d068e-b011-4e33-909b-2979630fabe1",
      "name": "Section 3 - Crawl",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1344,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 676,
        "height": 364,
        "content": "## 3 - Crawl & find orphans\nCaps to **maxPages**, fetches each page's HTML, builds the internal-link graph and flags **orphan pages**."
      },
      "typeVersion": 1
    },
    {
      "id": "0e1942af-f0fd-407a-86e9-bd26fceb849f",
      "name": "Section 4 - AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2032,
        -16
      ],
      "parameters": {
        "color": 7,
        "width": 378,
        "height": 728,
        "content": "## 4 - AI suggestions\n**Claude (OpenRouter)** proposes internal links as strict JSON \u2014 anchor text is a real target-page query. The parser + fixer enforce the schema."
      },
      "typeVersion": 1
    },
    {
      "id": "9bceae21-0595-433d-8d6c-88d006203ffb",
      "name": "Section 5 - Save",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2432,
        32
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 316,
        "content": "## 5 - Filter & save\n**Validate & Dedupe** keeps valid, high-confidence links (flags alreadyLinked). **Append** writes each row to your Google Sheet \u2014 pick your spreadsheet + tab here."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "4b94f076-1062-438b-a4dc-6b1ee5dc71b7",
  "nodeGroups": [],
  "connections": {
    "Run": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "GSC Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Crawl Page": {
      "main": [
        [
          {
            "node": "Build Map + Orphans",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GSC Queries": {
      "main": [
        [
          {
            "node": "Fetch Sitemap",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Limit Pages": {
      "main": [
        [
          {
            "node": "Crawl Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Sitemap": {
      "main": [
        [
          {
            "node": "Parse Sitemap XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Page URLs": {
      "main": [
        [
          {
            "node": "Limit Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Sitemap XML": {
      "main": [
        [
          {
            "node": "Extract Page URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate & Dedupe": {
      "main": [
        [
          {
            "node": "Append Suggestions to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Suggestions Schema": {
      "ai_outputParser": [
        [
          {
            "node": "Suggest Internal Links",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Build Map + Orphans": {
      "main": [
        [
          {
            "node": "Suggest Internal Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claude (OpenRouter)": {
      "ai_languageModel": [
        [
          {
            "node": "Suggest Internal Links",
            "type": "ai_languageModel",
            "index": 0
          },
          {
            "node": "Suggestions Schema",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Suggest Internal Links": {
      "main": [
        [
          {
            "node": "Validate & Dedupe",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}