This workflow corresponds to n8n.io template #17812 — we link there as the canonical source.
This workflow follows the Chainllm → Google Sheets recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"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
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleApigoogleOAuth2ApigoogleSheetsOAuth2ApiopenRouterApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow pulls page-query data from Google Search Console, crawls URLs from your sitemap to map existing internal links and identify orphan pages, then uses Claude via OpenRouter to propose high-confidence internal-link opportunities and appends the suggestions to a Google…
Source: https://n8n.io/workflows/17812/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
This powerful workflow automates the evaluation of new digital tools, websites, or platforms with the goal of assessing their potential impact on your business. By leveraging Telegram for user input,
Instead of guessing or relying on shallow placeholders, it scrapes real website content, summarizes it intelligently, and feeds that context into an LLM to produce outreach that feels relevant and hum
Community nodes are used in this workflow. B2B Sales Teams & SDRs Recruitment Agencies & Tech Recruiters Startup Founders Growth Marketing Teams Scrape Hiring Signals: The workflow starts by using an
Telegram Lead Gen - Apify Google Maps to Sheets. Uses telegramTrigger, telegram, httpRequest, openAi. Event-driven trigger; 25 nodes.
[VICTOR] Google Maps Extractor. Uses formTrigger, outputParserStructured, lmChatOpenAi, httpRequest. Event-driven trigger; 21 nodes.