This workflow corresponds to n8n.io template #11665 — we link there as the canonical source.
This workflow follows the Google Sheets → HTTP Request 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": "TEMPLATE_WORKFLOW_ID",
"name": "SEO Report Template",
"tags": [],
"nodes": [
{
"id": "f64b105d-7cd7-4dca-b4c5-231214af3b25",
"name": "When clicking 'Test workflow'",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-5744,
-2080
],
"parameters": {},
"typeVersion": 1
},
{
"id": "5ebfa39d-b8c5-4229-94c5-867f2f1ed551",
"name": "Sticky Note - External Articles",
"type": "n8n-nodes-base.stickyNote",
"position": [
-7472,
-4768
],
"parameters": {
"color": 5,
"width": 1420,
"height": 364,
"content": "## External Articles Reporting\n**Setup Instructions:**\n1. Add article URLs in the Data Sheet;\n2. Update the report document link in node \"Import Data to Report 3\";\n3. Verify the accuracy of submitted data."
},
"typeVersion": 1
},
{
"id": "2a128afc-8cef-4b0e-a530-ca72a2b67f5c",
"name": "Sticky Note - Internal Articles",
"type": "n8n-nodes-base.stickyNote",
"position": [
-7008,
-4288
],
"parameters": {
"color": 5,
"width": 1920,
"height": 400,
"content": "## Internal Articles Reporting\n**Setup Instructions:**\n1. Add article links in the Data Sheet;\n2. Update the report document link in node \"Import Data to Report 2\";\n3. Verify the accuracy of submitted data;\n4. Manually add monthly search volume in the report column."
},
"typeVersion": 1
},
{
"id": "0d7b4aeb-571d-43da-a508-f8dfce67f592",
"name": "Sticky Note - Organic Reach",
"type": "n8n-nodes-base.stickyNote",
"position": [
-6640,
-3792
],
"parameters": {
"color": 5,
"width": 720,
"height": 364,
"content": "## Organic Reach Reporting\n**Setup Instructions:**\n1. Set date range in node \"Import from Google Analytics 1\";\n2. Update report document link in node \"Import to Report 1\"."
},
"typeVersion": 1
},
{
"id": "f2b230a9-bfc3-45f5-bf9b-d8d8741f662c",
"name": "Convert Dots to Commas 2",
"type": "n8n-nodes-base.code",
"position": [
-5664,
-3200
],
"parameters": {
"jsCode": "return items.map(item => {\n return {\n json: {\n ...item.json,\n CTR: item.json.CTR.toString().replace('.', ','),\n \"Average Position\": item.json[\"Average Position\"].toString().replace('.', ',')\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "7eeb7754-36ce-41eb-8b38-ede9c2eeb5a1",
"name": "Export Keywords from GSC 2",
"type": "n8n-nodes-base.httpRequest",
"position": [
-6128,
-3200
],
"parameters": {
"url": "https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com/searchAnalytics/query",
"method": "POST",
"options": {},
"jsonBody": "{\n \"startDate\": \"2025-11-01\",\n \"endDate\": \"2025-11-25\",\n \"dimensions\": [\"query\"],\n \"rowLimit\": 1000,\n \"startRow\": 0,\n \"aggregationType\": \"byProperty\",\n \"dimensionFilterGroups\": []\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "d8e71fbf-15f9-4902-8611-d4aba111924d",
"name": "Create Table 2",
"type": "n8n-nodes-base.code",
"position": [
-5888,
-3200
],
"parameters": {
"jsCode": "return $json.rows.map(row => ({\n \"Keyword\": row.keys[0],\n \"Clicks\": row.clicks,\n \"Impressions\": row.impressions,\n \"CTR\": row.ctr,\n \"Average Position\": row.position\n}));"
},
"typeVersion": 2
},
{
"id": "8d4aeef9-1a87-461c-bc41-355dd97b14a4",
"name": "Organize Table 2",
"type": "n8n-nodes-base.set",
"position": [
-5456,
-3200
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5919caa6-e750-43fc-893b-64c923090f7a",
"name": "Keyword",
"type": "string",
"value": "={{$json[\"Keyword\"]}}"
},
{
"id": "30fb6df1-ff2d-4911-97cd-92c36558baa8",
"name": "Clicks",
"type": "string",
"value": "={{$json[\"Clicks\"]}}"
},
{
"id": "cb1a7ed9-033a-4a3b-9006-9da3e74270b8",
"name": "Impressions",
"type": "string",
"value": "={{$json[\"Impressions\"]}}"
},
{
"id": "d8689677-7b0c-4aea-a664-217b7ca6031f",
"name": "CTR",
"type": "string",
"value": "={{$json[\"CTR\"]}}"
},
{
"id": "73195bc2-e3b7-4f1e-8747-4edcfe4cc324",
"name": "Average Position",
"type": "string",
"value": "={{$json[\"Average Position\"]}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "f68b89f1-f8b5-4dd9-a355-0942e06ece59",
"name": "Import to Report 2",
"type": "n8n-nodes-base.googleSheets",
"position": [
-5216,
-3200
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Clicks",
"type": "string",
"display": true,
"required": false,
"displayName": "Clicks",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Impressions",
"type": "string",
"display": true,
"required": false,
"displayName": "Impressions",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CTR",
"type": "string",
"display": true,
"required": false,
"displayName": "CTR",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Average Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Average Position",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "54795d4f-2b28-4a80-ac26-a9cadecd2c40",
"name": "Sticky Note - Keyword Ranking",
"type": "n8n-nodes-base.stickyNote",
"position": [
-6224,
-3360
],
"parameters": {
"color": 5,
"width": 1300,
"height": 364,
"content": "## Keyword Ranking\n**Setup Instructions:**\n1. Set date range for report in node \"Export Keywords from GSC 2\";\n2. Reload authorization in node \"Export Keywords from GSC 2\";\n3. Update report document link in node \"Import to Report 2\"."
},
"typeVersion": 1
},
{
"id": "3b6f8367-5fe6-4dcc-ae26-00b2d008f9ad",
"name": "Convert Dots to Commas 3",
"type": "n8n-nodes-base.code",
"position": [
-5200,
-2768
],
"parameters": {
"jsCode": "return items.map(item => {\n return {\n json: {\n ...item.json,\n CTR: item.json.CTR.toString().replace('.', ','),\n \"Average Position\": item.json[\"Average Position\"].toString().replace('.', ',')\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "3b2bb1e7-26a8-496f-97a2-13fa6c914038",
"name": "Export Keywords from GSC 3",
"type": "n8n-nodes-base.httpRequest",
"position": [
-5664,
-2768
],
"parameters": {
"url": "https://www.googleapis.com/webmasters/v3/sites/https%3A%2F%2Fexample.com/searchAnalytics/query",
"method": "POST",
"options": {},
"jsonBody": "{\n \"startDate\": \"2025-09-01\",\n \"endDate\": \"2025-09-30\",\n \"dimensions\": [\"page\"],\n \"rowLimit\": 1000,\n \"startRow\": 0,\n \"dimensionFilterGroups\": []\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "0af80ad4-7657-4416-8344-90109fae1e6f",
"name": "Create Table 3",
"type": "n8n-nodes-base.code",
"position": [
-5424,
-2768
],
"parameters": {
"jsCode": "return $json.rows.map(row => ({\n \"Page\": row.keys[0],\n \"Clicks\": row.clicks,\n \"Impressions\": row.impressions,\n \"CTR\": row.ctr,\n \"Average Position\": row.position\n}));"
},
"typeVersion": 2
},
{
"id": "89698e0d-6d7c-4d81-b9a0-a4c9e5773143",
"name": "Organize Table 3",
"type": "n8n-nodes-base.set",
"position": [
-4992,
-2768
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "5919caa6-e750-43fc-893b-64c923090f7a",
"name": "Keyword",
"type": "string",
"value": "={{$json[\"Page\"]}}"
},
{
"id": "30fb6df1-ff2d-4911-97cd-92c36558baa8",
"name": "Clicks",
"type": "string",
"value": "={{$json[\"Clicks\"]}}"
},
{
"id": "cb1a7ed9-033a-4a3b-9006-9da3e74270b8",
"name": "Impressions",
"type": "string",
"value": "={{$json[\"Impressions\"]}}"
},
{
"id": "d8689677-7b0c-4aea-a664-217b7ca6031f",
"name": "CTR",
"type": "string",
"value": "={{$json[\"CTR\"]}}"
},
{
"id": "73195bc2-e3b7-4f1e-8747-4edcfe4cc324",
"name": "Average Position",
"type": "string",
"value": "={{$json[\"Average Position\"]}}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "182f6345-97e7-48ab-933b-8af692ffa762",
"name": "Sticky Note - Page Ranking",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5760,
-2944
],
"parameters": {
"color": 5,
"width": 1300,
"height": 384,
"content": "## Page Ranking\n**Setup Instructions:**\n1. Set date range for report in node \"Export Keywords from GSC 3\";\n2. Reload authorization in node \"Export Keywords from GSC 3\";\n3. Update report document link in node \"Import to Report 3\"."
},
"typeVersion": 1
},
{
"id": "bef4bd4e-74bf-49ba-b865-53d64a55528c",
"name": "Import from Google Analytics 1",
"type": "n8n-nodes-base.googleAnalytics",
"position": [
-6560,
-3616
],
"parameters": {
"endDate": "2025-11-25T00:00:00",
"dateRange": "custom",
"returnAll": true,
"startDate": "2025-11-01T00:00:00",
"metricsGA4": {
"metricValues": [
{
"name": "organicGoogleSearchClicks",
"listName": "other"
},
{
"name": "organicGoogleSearchImpressions",
"listName": "other"
},
{
"name": "organicGoogleSearchClickThroughRate",
"listName": "other"
},
{
"name": "organicGoogleSearchAveragePosition",
"listName": "other"
}
]
},
"propertyId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GA4_PROPERTY_ID"
},
"dimensionsGA4": {
"dimensionValues": [
{}
]
},
"additionalFields": {}
},
"typeVersion": 2
},
{
"id": "ff6161f0-109a-44bd-99e1-59d65b0f0190",
"name": "Format Data 7",
"type": "n8n-nodes-base.code",
"position": [
-6368,
-3616
],
"parameters": {
"jsCode": "// Sort dates from oldest to newest\nconst sortedItems = items.sort((a, b) => parseInt(a.json.date) - parseInt(b.json.date));\n\n// Initialize variables for summing\nlet totalClicks = 0;\nlet totalImpressions = 0;\nlet totalCtrSum = 0;\nlet totalPositionSum = 0;\nlet count = sortedItems.length;\n\n// Map to transform data and sum values\nconst formattedData = sortedItems.map(item => {\n // Convert date from YYYYMMDD to DD-MM-YYYY\n const rawDate = item.json.date;\n const formattedDate = `${rawDate.slice(6, 8)}-${rawDate.slice(4, 6)}-${rawDate.slice(0, 4)}`;\n\n // Get values and convert to numbers\n const clicks = parseInt(item.json.organicGoogleSearchClicks, 10);\n const impressions = parseInt(item.json.organicGoogleSearchImpressions, 10);\n const ctr = parseFloat(item.json.organicGoogleSearchClickThroughRate) * 100;\n const position = parseFloat(item.json.organicGoogleSearchAveragePosition);\n\n // Sum values\n totalClicks += clicks;\n totalImpressions += impressions;\n totalCtrSum += ctr;\n totalPositionSum += position;\n\n return {\n json: {\n \"Date\": formattedDate, \n \"Clicks\": clicks, \n \"Impressions\": impressions, \n \"CTR (%)\": ctr.toFixed(2).replace(\".\", \",\"), \n \"Average Position\": position.toFixed(2).replace(\".\", \",\")\n }\n };\n});\n\n// Calculate average CTR and position\nconst averageCtr = (totalCtrSum / count).toFixed(2).replace(\".\", \",\");\nconst averagePosition = (totalPositionSum / count).toFixed(2).replace(\".\", \",\");\n\n// Add \"Total\" row\nformattedData.push({\n json: {\n \"Date\": \"Total\", \n \"Clicks\": totalClicks, \n \"Impressions\": totalImpressions, \n \"CTR (%)\": averageCtr, \n \"Average Position\": averagePosition\n }\n});\n\n// Return data to n8n\nreturn formattedData;\n"
},
"typeVersion": 2
},
{
"id": "70b908b4-b2a9-48f4-b3ca-ffda654e9be5",
"name": "Import to Report 1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-6144,
-3616
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Clicks",
"type": "string",
"display": true,
"required": false,
"displayName": "Clicks",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Impressions",
"type": "string",
"display": true,
"required": false,
"displayName": "Impressions",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CTR (%)",
"type": "string",
"display": true,
"required": false,
"displayName": "CTR (%)",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Average Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Average Position",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "ea579d0d-06d5-4c6f-a881-725dbc6a28d6",
"name": "Export Links from Document 1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-6896,
-4080
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_DATA_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "3b9ef20e-a66e-4d37-9541-82fb3883a965",
"name": "Import Data to Report 2",
"type": "n8n-nodes-base.googleSheets",
"position": [
-5360,
-4080
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Title",
"type": "string",
"display": true,
"required": false,
"displayName": "Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Article URL",
"type": "string",
"display": true,
"required": false,
"displayName": "Article URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Publication Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Publication Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Main Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Main Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Reach",
"type": "string",
"display": true,
"required": false,
"displayName": "Reach",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Internal Links",
"type": "string",
"display": true,
"required": false,
"displayName": "Internal Links",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Link Labels",
"type": "string",
"display": true,
"required": false,
"displayName": "Link Labels",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Link Attributes",
"type": "string",
"display": true,
"required": false,
"displayName": "Link Attributes",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "0ee0d8b5-299d-41fe-8cca-cc94b4e9e71c",
"name": "Analyze Links 1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-6624,
-4080
],
"parameters": {
"url": "={{$json[\"URL\"]}}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "d51af504-9210-4ad1-aef6-8cc26153405f",
"name": "Format Data to Table 1",
"type": "n8n-nodes-base.code",
"position": [
-6288,
-4080
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// 1\ufe0f\u20e3 **Check if data is available**\nif (!$json[\"data\"] || typeof $json[\"data\"] !== \"string\") {\n throw new Error(\"No HTML data - make sure HTTP Request returns page content.\");\n}\n\n// 2\ufe0f\u20e3 **Get input HTML**\nlet inputData = $json[\"data\"];\n\nconsole.log(\"=== STARTING ANALYSIS ===\");\nconsole.log(\"HTML length:\", inputData.length);\n\n// 3\ufe0f\u20e3 **Finding 'entry-content' section with multiple strategies**\n\n// Strategy 1: Find exact position of <div class=\"entry-content\">\nlet entryContentStart = inputData.indexOf('<div class=\"entry-content\">');\nlet entryContent = \"\";\nlet foundMethod = \"\";\n\nif (entryContentStart !== -1) {\n console.log(\"\u2705 Found <div class=\\\"entry-content\\\"> at position:\", entryContentStart);\n \n // Find end of this div - look for closing </div>\n // But we need to be careful with nested divs\n let currentPos = entryContentStart + '<div class=\"entry-content\">'.length;\n let divCount = 1; // Start with 1 because we're already in a div\n let endPos = -1;\n \n while (currentPos < inputData.length && divCount > 0) {\n let nextDiv = inputData.indexOf('<div', currentPos);\n let nextCloseDiv = inputData.indexOf('</div>', currentPos);\n \n if (nextCloseDiv === -1) break; // No more closing divs\n \n if (nextDiv !== -1 && nextDiv < nextCloseDiv) {\n // Found opening div before closing\n divCount++;\n currentPos = nextDiv + 4;\n } else {\n // Found closing div\n divCount--;\n if (divCount === 0) {\n endPos = nextCloseDiv;\n break;\n }\n currentPos = nextCloseDiv + 6;\n }\n }\n \n if (endPos !== -1) {\n entryContent = inputData.substring(entryContentStart + '<div class=\"entry-content\">'.length, endPos);\n foundMethod = \"Strategy 1: Precise div parsing\";\n console.log(\"\u2705 Extracted entry-content, length:\", entryContent.length);\n }\n}\n\n// Fallback if we didn't find it or something went wrong\nif (!entryContent || entryContent.length < 100) {\n console.log(\"\u274c Strategy 1 didn't work, trying fallback\");\n let match = inputData.match(/<div class=\"entry-content\">([\\s\\S]*?)(?=<\\/div>)/);\n if (match) {\n entryContent = match[1];\n foundMethod = \"Strategy 2: Regex fallback\";\n } else {\n // Last attempt - take everything between entry-content and footer\n let start = inputData.indexOf('entry-content');\n let end = inputData.indexOf('<footer', start);\n if (start !== -1 && end !== -1) {\n entryContent = inputData.substring(start, end);\n foundMethod = \"Strategy 3: From entry-content to footer\";\n } else {\n throw new Error(\"Cannot find entry-content section in any way\");\n }\n }\n}\n\nconsole.log(\"Used strategy:\", foundMethod);\nconsole.log(\"entry-content length:\", entryContent.length);\nconsole.log(\"entry-content start:\", entryContent.slice(0, 300));\n\n// 4\ufe0f\u20e3 **Finding links in entry-content**\nconsole.log(\"\\n=== LINK ANALYSIS ===\");\n\nconst allLinks = [];\n\n// Very simple and effective regex for links\nconst linkRegex = /<a\\s[^>]*href=[\"']([^\"']+)[\"'][^>]*>((?:(?!<\\/a>)[\\s\\S])*)<\\/a>/gi;\n\nlet match;\nlet linkCount = 0;\n\nwhile ((match = linkRegex.exec(entryContent)) !== null && linkCount < 50) {\n linkCount++;\n \n const href = match[1].trim();\n const fullContent = match[2];\n \n console.log(`\\n--- Link ${linkCount} ---`);\n console.log(\"URL:\", href);\n console.log(\"Content:\", fullContent.slice(0, 100));\n \n // Extract clean anchor text\n let anchorText = fullContent.replace(/<[^>]*>/g, \"\").trim();\n anchorText = anchorText.replace(/\\s+/g, \" \").trim();\n \n // Check rel attribute in entire <a> tag\n const fullTag = match[0];\n const relMatch = fullTag.match(/rel=[\"']([^\"']+)[\"']/i);\n const rel = relMatch ? relMatch[1] : \"follow\";\n \n console.log(\"Anchor text:\", anchorText);\n console.log(\"Rel:\", rel);\n \n // \ud83d\udccc **VERY BROAD internal link filters**\n // Replace 'example.com' with your actual domain\n const domain = \"example.com\";\n const isInternalLink = (\n // All links containing your domain\n href.includes(domain) ||\n // Relative links\n (href.startsWith(\"/\") && !href.startsWith(\"//\")) ||\n // Hash links\n href.startsWith(\"#\") ||\n // Links without protocol starting with domain\n (href.indexOf(\"://\") === -1 && href.indexOf(\"www.\") === -1 && !href.startsWith(\"mailto:\") && !href.startsWith(\"tel:\"))\n );\n \n console.log(\"Is internal:\", isInternalLink);\n \n if (isInternalLink && anchorText && anchorText.length > 0) {\n // Check duplicates\n const isDuplicate = allLinks.some(existingLink => \n existingLink.href === href && existingLink.anchorText === anchorText\n );\n \n if (!isDuplicate) {\n allLinks.push({\n href,\n anchorText,\n rel\n });\n console.log(\"\u2705 ADDED TO LIST\");\n } else {\n console.log(\"\u274c DUPLICATE - skipped\");\n }\n } else {\n console.log(\"\u274c REJECTED - doesn't meet criteria\");\n }\n}\n\n// \u2705 **Link summary**\nconsole.log(`\\n=== LINK SUMMARY ===`);\nconsole.log(\"Total number of internal links found:\", allLinks.length);\n\nallLinks.forEach((link, index) => {\n console.log(`${index + 1}. ${link.href} -> \"${link.anchorText}\" [${link.rel}]`);\n});\n\n// 5\ufe0f\u20e3 **Format results**\nconst internalLinks = allLinks.map(link => link.href).join(\", \");\nconst anchorTexts = allLinks.map(link => link.anchorText).join(\", \");\nconst linkAttributes = allLinks.map(link => link.rel).join(\", \");\n\n// 6\ufe0f\u20e3 **Get metadata**\nconsole.log(\"\\n=== GETTING METADATA ===\");\n\n// Title - first check h1 in entry-content\nlet title = \"No title\";\nconst titleMatch = entryContent.match(/<h1[^>]*>(.*?)<\\/h1>/i);\nif (titleMatch) {\n title = titleMatch[1].replace(/<[^>]*>/g, \"\").trim();\n console.log(\"Title from entry-content:\", title);\n} else {\n // Fallback - title tag\n const titleTagMatch = inputData.match(/<title[^>]*>(.*?)<\\/title>/i);\n if (titleTagMatch) {\n title = titleTagMatch[1].replace(/<[^>]*>/g, \"\").trim();\n console.log(\"Title from title tag:\", title);\n }\n}\n\n// Article URL\nconst canonicalMatch = inputData.match(/<link[^>]+rel=[\"']canonical[\"'][^>]+href=[\"']([^\"']+)[\"']/i);\nconst articleUrl = canonicalMatch ? canonicalMatch[1] : \"No link\";\nconsole.log(\"Article URL:\", articleUrl);\n\n// Publication date\nconst publishedDateMatch = inputData.match(/<meta[^>]+property=[\"']article:published_time[\"'][^>]+content=[\"']([^\"']+)[\"']/i);\nlet publishedDate = publishedDateMatch ? publishedDateMatch[1] : \"No date\";\nconsole.log(\"Publication date (raw):\", publishedDate);\n\nfunction formatDate(dateString) {\n if (!dateString || dateString === \"No date\") return \"No date\";\n \n console.log(\"Formatting date - input:\", dateString);\n \n try {\n // If date contains T or + (ISO format), try directly first\n let date = new Date(dateString);\n \n // If it doesn't work, try to extract only date part\n if (isNaN(date.getTime())) {\n console.log(\"First attempt failed, trying alternative\");\n \n // Extract only YYYY-MM-DD part\n const dateMatch = dateString.match(/(\\d{4})-(\\d{2})-(\\d{2})/);\n if (dateMatch) {\n const [, year, month, day] = dateMatch;\n date = new Date(year, month - 1, day); // month-1 because JavaScript counts months from 0\n console.log(\"Created date from parts:\", year, month, day);\n }\n }\n \n if (isNaN(date.getTime())) {\n console.log(\"All date parsing attempts failed\");\n return \"Invalid date\";\n }\n \n const day = String(date.getDate()).padStart(2, '0');\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const year = String(date.getFullYear()).slice(2);\n \n console.log(`Formatted date: ${day}-${month}-${year}`);\n return `${day}-${month}-${year}`;\n \n } catch (error) {\n console.log(\"Date formatting error:\", error);\n return \"Date formatting error\";\n }\n}\n\npublishedDate = formatDate(publishedDate);\nconsole.log(\"Publication date (formatted):\", publishedDate);\n\n// Keywords\nconst keywordsMatch = inputData.match(/<meta[^>]+name=[\"']keywords[\"'][^>]+content=[\"']([^\"']+)[\"']/i);\nconst keywords = keywordsMatch ? keywordsMatch[1] : \"No keywords\";\nconsole.log(\"Keywords:\", keywords);\n\n// 7\ufe0f\u20e3 **Return results**\nconsole.log(\"\\n=== FINAL RESULTS ===\");\nconsole.log(\"Title:\", title);\nconsole.log(\"Article URL:\", articleUrl);\nconsole.log(\"Published Date:\", publishedDate);\nconsole.log(\"Keywords:\", keywords);\nconsole.log(\"Internal Links:\", internalLinks);\nconsole.log(\"Anchor Texts:\", anchorTexts);\nconsole.log(\"Link Attributes:\", linkAttributes);\n\nreturn {\n title,\n articleUrl,\n publishedDate,\n keywords,\n volume: \"\",\n internalLinks,\n anchorTexts,\n linkAttributes\n};"
},
"typeVersion": 2
},
{
"id": "75f79f2d-4811-4ece-945b-9c6a0ce98183",
"name": "Format Data for Table Paste 1",
"type": "n8n-nodes-base.code",
"position": [
-5856,
-4080
],
"parameters": {
"jsCode": "// Get input data from Google Sheets\nconst data = $input.all().map(item => item.json);\n\n// Map new column headers\nconst newHeaders = {\n title: \"Title\",\n articleUrl: \"Article URL\",\n publishedDate: \"Publication Date\",\n keywords: \"Main Keyword\",\n volume: \"Reach\",\n internalLinks: \"Internal Links\",\n anchorTexts: \"Link Labels\",\n linkAttributes: \"Link Attributes\"\n};\n\n// Function to convert date from ISO to DD-MM-YY format - FIXED\nfunction formatDate(dateString) {\n if (!dateString || dateString === \"No date\" || dateString === \"\") {\n console.log(\"No date to format\");\n return \"\"; // If no date, return empty string\n }\n \n console.log(\"Formatting date in processing - input:\", dateString, \"type:\", typeof dateString);\n \n // Check if date is already in DD-MM-YY format\n if (typeof dateString === \"string\" && /^\\d{2}-\\d{2}-\\d{2}$/.test(dateString)) {\n console.log(\"Date already in DD-MM-YY format, returning unchanged:\", dateString);\n return dateString;\n }\n \n try {\n // If date contains T or + (ISO format), try directly first\n let date = new Date(dateString);\n \n // If it doesn't work, try to extract only date part\n if (isNaN(date.getTime())) {\n console.log(\"First attempt failed, trying alternative\");\n \n // Extract only YYYY-MM-DD part\n const dateMatch = dateString.match(/(\\d{4})-(\\d{2})-(\\d{2})/);\n if (dateMatch) {\n const [, year, month, day] = dateMatch;\n date = new Date(year, month - 1, day); // month-1 because JavaScript counts months from 0\n console.log(\"Created date from parts:\", year, month, day);\n }\n }\n \n // If still no valid date\n if (isNaN(date.getTime())) {\n console.log(\"All date parsing attempts failed for:\", dateString);\n return dateString; // Return original value instead of empty string\n }\n \n const day = String(date.getDate()).padStart(2, '0');\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const year = String(date.getFullYear()).slice(2);\n \n const formattedResult = `${day}-${month}-${year}`;\n console.log(`Formatted date: ${formattedResult}`);\n return formattedResult;\n \n } catch (error) {\n console.log(\"Date formatting error:\", error, \"- returning original value\");\n return dateString; // Return original value instead of empty string\n }\n}\n\n// Process data and format rows\nconst processedData = [];\n\ndata.forEach((row, rowIndex) => {\n console.log(`\\nProcessing row ${rowIndex + 1}:`, row);\n \n const internalLinks = row.internalLinks ? row.internalLinks.split(\", \") : [];\n const anchorTexts = row.anchorTexts ? row.anchorTexts.split(\", \") : [];\n const linkAttributes = row.linkAttributes ? row.linkAttributes.split(\", \") : [];\n \n console.log(\"Found internal links:\", internalLinks.length);\n console.log(\"Publication date (raw):\", row.publishedDate);\n \n // Convert date to DD-MM-YY\n const formattedDate = formatDate(row.publishedDate);\n console.log(\"Publication date (formatted):\", formattedDate);\n \n // First row contains all main data\n if (internalLinks.length > 0) {\n internalLinks.forEach((link, index) => {\n const processedRow = {\n [newHeaders.title]: index === 0 ? row.title : \"\", // Only first row has title\n [newHeaders.articleUrl]: index === 0 ? row.articleUrl : \"\", // Article URL\n [newHeaders.publishedDate]: index === 0 ? formattedDate : \"\", // Formatted date\n [newHeaders.keywords]: index === 0 ? row.keywords : \"\", // Only first row has keyword\n [newHeaders.volume]: index === 0 ? row.volume || \"\" : \"\", // Only first row has reach\n [newHeaders.internalLinks]: link.trim(), // Each link gets separate row\n [newHeaders.anchorTexts]: anchorTexts[index] ? anchorTexts[index].trim() : \"\", // Related link label\n [newHeaders.linkAttributes]: linkAttributes[index] || \"follow\" // Default follow\n };\n \n processedData.push(processedRow);\n console.log(`Added row ${index + 1} for link:`, link.slice(0, 50));\n });\n } else {\n // If no internal links, add row with only basic data\n const processedRow = {\n [newHeaders.title]: row.title,\n [newHeaders.articleUrl]: row.articleUrl, // Added article URL\n [newHeaders.publishedDate]: formattedDate,\n [newHeaders.keywords]: row.keywords,\n [newHeaders.volume]: row.volume || \"\",\n [newHeaders.internalLinks]: \"\",\n [newHeaders.anchorTexts]: \"\",\n [newHeaders.linkAttributes]: \"\"\n };\n \n processedData.push(processedRow);\n console.log(\"Added row without internal links\");\n }\n});\n\nconsole.log(`\\n=== PROCESSING SUMMARY ===`);\nconsole.log(\"Total number of processed rows:\", processedData.length);\nconsole.log(\"First 3 processed rows:\", processedData.slice(0, 3));\n\n// Return processed data in appropriate format\nreturn processedData;"
},
"typeVersion": 2
},
{
"id": "9fc35392-f86d-489a-85fe-50ae03a7ee79",
"name": "Analyze External Links 1",
"type": "n8n-nodes-base.httpRequest",
"position": [
-7184,
-4592
],
"parameters": {
"url": "={{$json[\"URL\"]}}",
"options": {
"response": {
"response": {
"responseFormat": "text"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "5160562a-ef03-43f2-916d-758dddcecb9b",
"name": "Format Data for Table 1",
"type": "n8n-nodes-base.code",
"position": [
-6976,
-4592
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const html = $json[\"data\"] || \"\"; // Get HTML from input data\n\n// Debugging: See what's in html\nconsole.log(\"HTML contains:\", html);\n\n// If HTML is empty, return error\nif (!html || html.trim() === \"\") {\n return {\n error: \"No data retrieved from HTTP Request. Check configuration.\",\n url: $json[\"url\"] || \"No URL\"\n };\n}\n\n// Extract article title\nconst titleMatch = html.match(/<title>(.*?)<\\/title>/i);\nconst title = titleMatch ? titleMatch[1].trim() : \"Title not found\";\n\n// Extract canonical link\nconst canonicalMatch = html.match(/<link[^>]+rel=[\"']canonical[\"'][^>]+href=[\"']([^\"']+)[\"']/i);\nconst canonical = canonicalMatch ? canonicalMatch[1] : \"Canonical not found\";\n\n// Extract article publication date\nconst dateMatch = html.match(/<meta[^>]+property=[\"']article:published_time[\"'][^>]+content=[\"']([^\"']+)[\"']/i);\nlet publishDate = dateMatch ? dateMatch[1] : \"\";\n\n// \ud83d\udd04 **Convert date format to DD-MM-YY**\nfunction formatDate(dateString) {\n if (!dateString) return \"\"; // If no date, return empty string\n const date = new Date(dateString);\n const day = String(date.getDate()).padStart(2, '0');\n const month = String(date.getMonth() + 1).padStart(2, '0'); // Months start from 0\n const year = String(date.getFullYear()).slice(2); // Get only last two digits of year\n return `${day}-${month}-${year}`;\n}\n\n// Convert date to new format\npublishDate = formatDate(publishDate);\n\n// Extract links containing your domain\n// Replace 'example.com' with your actual domain\nconst domain = \"example.com\";\nconst links = [];\nconst regex = /<a[^>]+href=\"([^\"]+)\"[^>]*>(.*?)<\\/a>/g;\nlet match;\n\nwhile ((match = regex.exec(html)) !== null) {\n const href = match[1];\n const text = match[2].trim();\n\n // Check rel=\"nofollow\" attribute\n const relMatch = match[0].match(/rel=[\"']nofollow[\"']/i);\n const relType = relMatch ? \"nofollow\" : \"follow\";\n\n if (href.includes(domain) || text.includes(domain)) {\n links.push({ href, text, rel: relType });\n }\n}\n\n// Return data as single object\nreturn {\n title,\n canonical,\n publishDate, // New field with date in DD-MM-YY format\n links: links.length ? links : [{ message: `No links containing '${domain}' found in provided HTML.` }],\n url: $json[\"url\"] || \"No URL\"\n};"
},
"typeVersion": 2
},
{
"id": "ecde8070-6d59-4544-858f-86fc639768e7",
"name": "Proper Data Formatting",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-6800,
-4592
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o",
"cachedResultName": "GPT-4O"
},
"options": {},
"messages": {
"values": [
{
"content": "=Input data:\nArticle title: {{$json[\"title\"]}}\nCanonical link: {{$json[\"canonical\"]}}\nPublication date: {{$json[\"publishDate\"]}}\n\nLinks in article:\n{{ $json.links.map(link => `- ${link.text} (${link.href}) [${link.rel}]`).join('\\n') }}\n\nTransform this data into tabular format ready to save in Google Sheets:\n\n1. First row: Article title, Canonical link, Publication date.\n2. Next rows: Link, Link description, Follow/nofollow attribute.\n3. Each value separated by comma.\n4. Return data in CSV format, where each line represents one table row."
}
]
}
},
"typeVersion": 1.8
},
{
"id": "1ed5cda6-9aa0-40be-acbc-2c933b5141de",
"name": "Format Data Under Table 1",
"type": "n8n-nodes-base.code",
"position": [
-6464,
-4592
],
"parameters": {
"jsCode": "// Initialize output array\nconst output = [];\n\n// Iterate over each input data element\nitems.forEach(item => {\n // Get CSV data from message.content field\n const content = item.json.message.content;\n\n // Remove Markdown markers and split data into lines\n const lines = content.replace(/```csv/g, '').trim().split('\\n');\n\n // Check if there are enough lines\n if (lines.length < 4) {\n throw new Error(\"Invalid input data format. Check data source.\");\n }\n\n // Separate first part (headers and article title) from links\n const articleInfo = lines.slice(1, 2)[0]?.split(',');\n const links = lines.slice(3);\n\n // Check if headers are correct\n if (!articleInfo || articleInfo.length < 3) {\n throw new Error(\"Missing article title, canonical link or publication date.\");\n }\n\n // Get article title, canonical link and publication date\n const title = articleInfo[0]?.trim() || \"No title\";\n const canonical = articleInfo[1]?.trim() || \"No canonical link\";\n const publishDate = articleInfo[2]?.trim() || \"\"; // New field\n\n // Process each link and link description\n links.forEach((linkRow, index) => {\n if (linkRow.trim() === \"\") return; // Ignore empty rows\n\n const [link, description, relType] = linkRow.split(',');\n\n // Add to results with appropriate data\n output.push({\n \"Article Title\": index === 0 ? title : \"\", \n \"Canonical Link\": index === 0 ? canonical : \"\", \n \"Publication Date\": index === 0 ? publishDate : \"\", // New field\n \"Link\": link?.trim() || \"No link\",\n \"Link Description\": description?.trim() || \"No description\",\n \"Attribute\": relType?.trim() || \"follow\" // New field\n });\n });\n});\n\n// Return result data\nreturn output.map(row => ({ json: row }));"
},
"typeVersion": 2
},
{
"id": "45992637-7145-4374-9dfa-08d6196a5bf0",
"name": "Import Data to Report 3",
"type": "n8n-nodes-base.googleSheets",
"position": [
-6272,
-4592
],
"parameters": {
"columns": {
"value": {
"Article Title": "={{$json[\"Article Title\"]}}",
"Publication Date": "={{$json[\"Publication Date\"]}}"
},
"schema": [
{
"id": "Article Title",
"type": "string",
"display": true,
"required": false,
"displayName": "Article Title",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Canonical Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Canonical Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Publication Date",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Publication Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Link Description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Link Description",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Attribute",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Attribute",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "ddd41e66-02c9-4692-9659-647fbdd1d9ac",
"name": "Import to Report 3",
"type": "n8n-nodes-base.googleSheets",
"position": [
-4752,
-2768
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Clicks",
"type": "string",
"display": true,
"required": false,
"displayName": "Clicks",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Impressions",
"type": "string",
"display": true,
"required": false,
"displayName": "Impressions",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CTR",
"type": "string",
"display": true,
"required": false,
"displayName": "CTR",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Average Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Average Position",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "ac4ae239-4cd2-44b3-ac05-ac0529f3164c",
"name": "Format Data 12",
"type": "n8n-nodes-base.code",
"position": [
-4832,
-2192
],
"parameters": {
"jsCode": "return items.map(item => {\n const convertDotsToCommas = (value) => {\n // Check if the value is a number or can be converted to a number\n const num = Number(value);\n if (!isNaN(num) && typeof value === 'string') {\n return value.replace('.', ',');\n }\n // If it's already a number, convert to string and replace\n if (typeof value === 'number') {\n return value.toString().replace('.', ',');\n }\n return value;\n };\n\n return {\n json: {\n \"Keyword\": item.json[\"Keyword\"],\n \"URL\": item.json[\"URL\"],\n \"Avg. Monthly Searches\": item.json[\"Avg. Monthly Searches\"],\n \"Position\": item.json[\"Position\"],\n \"Previous Position\": item.json[\"Previous Position\"],\n \"Position Change\": item.json[\"Position Change\"],\n \"Estimated Traffic\": convertDotsToCommas(item.json[\"Estimated Traffic\"]),\n \"Previous Estimated Traffic\": convertDotsToCommas(item.json[\"Previous Estimated Traffic\"]),\n \"Estimated Traffic Change\": convertDotsToCommas(item.json[\"Estimated Traffic Change\"]),\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "2da61380-ac2c-4b10-b368-a8bdb4620e9e",
"name": "Format Data 13",
"type": "n8n-nodes-base.code",
"position": [
-4848,
-1712
],
"parameters": {
"jsCode": "return items.map(item => {\n const convertDotsToCommas = (value) => {\n // Check if the value is a number or can be converted to a number\n const num = Number(value);\n if (!isNaN(num) && typeof value === 'string') {\n return value.replace('.', ',');\n }\n // If it's already a number, convert to string and replace\n if (typeof value === 'number') {\n return value.toString().replace('.', ',');\n }\n return value;\n };\n\n return {\n json: {\n \"Keyword\": item.json[\"Keyword\"],\n \"URL\": item.json[\"URL\"],\n \"Avg. Monthly Searches\": item.json[\"Avg. Monthly Searches\"],\n \"Position\": item.json[\"Position\"],\n \"Previous Position\": item.json[\"Previous Position\"],\n \"Position Change\": item.json[\"Position Change\"],\n \"Estimated Traffic\": convertDotsToCommas(item.json[\"Estimated Traffic\"]),\n \"Previous Estimated Traffic\": convertDotsToCommas(item.json[\"Previous Estimated Traffic\"]),\n \"Estimated Traffic Change\": convertDotsToCommas(item.json[\"Estimated Traffic Change\"]),\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "77867ec9-6e2f-45a0-83b7-ce6891c4c17b",
"name": "Export Data from Document 1",
"type": "n8n-nodes-base.googleSheets",
"position": [
-5056,
-2192
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_DATA_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "9346647f-74f3-4c5e-b242-2fb8f2b7d3a0",
"name": "Export Data from Document 2",
"type": "n8n-nodes-base.googleSheets",
"position": [
-5056,
-1712
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_DATA_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "be44d212-6e2b-45e7-ab1b-0fe143515d7e",
"name": "Import Data to Sheet 10",
"type": "n8n-nodes-base.googleSheets",
"position": [
-4608,
-2192
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "URL",
"type": "string",
"display": true,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Avg. Monthly Searches",
"type": "string",
"display": true,
"required": false,
"displayName": "Avg. Monthly Searches",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Position",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Previous Position",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Position Change",
"type": "string",
"display": true,
"required": false,
"displayName": "Position Change",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Estimated Traffic",
"type": "string",
"display": true,
"required": false,
"displayName": "Estimated Traffic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Estimated Traffic",
"type": "string",
"display": true,
"required": false,
"displayName": "Previous Estimated Traffic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Estimated Traffic Change",
"type": "string",
"display": true,
"required": false,
"displayName": "Estimated Traffic Change",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "YOUR_SHEET_NAME"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.5
},
{
"id": "20f781ef-e427-430f-9a71-a3c0c427ad8a",
"name": "Import Data to Sheet 11",
"type": "n8n-nodes-base.googleSheets",
"position": [
-4624,
-1712
],
"parameters": {
"columns": {
"value": {},
"schema": [
{
"id": "Keyword",
"type": "string",
"display": true,
"required": false,
"displayName": "Keyword",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "URL",
"type": "string",
"display": true,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Avg. Monthly Searches",
"type": "string",
"display": true,
"required": false,
"displayName": "Avg. Monthly Searches",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Position",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Position",
"type": "string",
"display": true,
"required": false,
"displayName": "Previous Position",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Position Change",
"type": "string",
"display": true,
"required": false,
"displayName": "Position Change",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Estimated Traffic",
"type": "string",
"display": true,
"required": false,
"displayName": "Estimated Traffic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Estimated Traffic",
"type": "string",
"display": true,
"required": false,
"displayName": "Previous Estimated Traffic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Estimated Traffic Change",
"type": "string",
"display": true,
"required": false,
"displayName": "Estimated Traffic Change",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "autoMapInputData",
"matchingColumns": [],
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates comprehensive SEO reporting by: Extracting keyword rankings and page performance from Google Search Console. Gathering organic reach metrics from Google Analytics. Analyzing internal and external article links. Tracking keyword position changes (gains and…
Source: https://n8n.io/workflows/11665/ — 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 template automates the enrichment of business leads from a Google Sheet by: Triggering when a row is activated Searching for company information with Serper.dev Generating and validating potentia
The automation starts by retreiving the unused queries from a sheet, executes queries in the web using Serper API and extracts linkedin profiles of decision makers.
This workflow helps you automatically collect verified business leads from Google Search using SerpAPI — no coding required. It extracts company names, websites, emails, and phone numbers directly fro
My workflow 11. Uses manualTrigger, splitOut, httpRequest, splitInBatches. Event-driven trigger; 14 nodes.
Trending YouTube Videos copy. Uses googleSheets, openAi, httpRequest, stickyNote. Event-driven trigger; 12 nodes.