AutomationFlowsData & Sheets › Find Internal Linking Opportunities with Serpapi and Google Sheets

Find Internal Linking Opportunities with Serpapi and Google Sheets

ByRobin Geuens @robingeuens on n8n.io

Use this workflow to spot internal linking ideas on your site and improve your search performance. It takes your target URLs and keywords, finds related pages, and suggests where to add links. Strong internal linking helps search engines understand your site. You provide a list…

Event trigger★★★★☆ complexity12 nodesHTTP RequestGoogle Sheets
Data & Sheets Trigger: Event Nodes: 12 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #9071 — 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 →

Download .json
{
  "id": "3qFkempg015uG0dB",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Find internal linking opportunities using SerAPI and Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "8c2aa4ad-94d0-4c52-b6bb-bee296a4420b",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        480,
        144
      ],
      "parameters": {
        "options": {},
        "batchSize": 5
      },
      "typeVersion": 3
    },
    {
      "id": "7fccd9bf-8f71-4adf-97e4-2eff330d757f",
      "name": "Get search results using SerpAPI",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        832,
        160
      ],
      "parameters": {
        "url": "https://serpapi.com/search",
        "options": {},
        "sendQuery": true,
        "authentication": "predefinedCredentialType",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.Keyword }} site:{{ $json.Domain }} -inurl:{{ $json.URL }}"
            }
          ]
        },
        "nodeCredentialType": "serpApi"
      },
      "credentials": {
        "serpApi": {
          "name": "<your credential>"
        },
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "420bdbbe-a304-4bfe-8cd8-54ab467a6d71",
      "name": "Extract links from JSON",
      "type": "n8n-nodes-base.set",
      "position": [
        1184,
        160
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "eae05e00-7188-4cca-994f-0cdf091e5618",
              "name": "URL",
              "type": "string",
              "value": "={{ $('Loop Over Items').item.json.URL }}"
            },
            {
              "id": "1e7647fb-a137-4971-8f8c-5f0709d0f92f",
              "name": "organic_results",
              "type": "array",
              "value": "={{ $json.organic_results.map(item => item.link) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "44ca48a2-fae4-4de4-a5d7-e4d16960b914",
      "name": "Add internal URL to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1536,
        160
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.URL }}",
            "Internal link 1": "={{ $json.organic_results[0] ? $json.organic_results[0] : \"N/A\"}}",
            "Internal link 2": "={{ $json.organic_results[1] ? $json.organic_results[1] : \"N/A\"}}",
            "Internal link 3": "={{ $json.organic_results[2] ? $json.organic_results[2] : \"N/A\"}}",
            "Internal link 4": "={{ $json.organic_results[3] ? $json.organic_results[3] : \"N/A\"}}",
            "Internal link 5": "={{ $json.organic_results[4] ? $json.organic_results[4] : \"N/A\"}}",
            "Internal link 6": "={{ $json.organic_results[5] ? $json.organic_results[5] : \"N/A\"}}",
            "Internal link 7": "={{ $json.organic_results[6] ? $json.organic_results[6] : \"N/A\"}}",
            "Internal link 8": "={{ $json.organic_results[7] ? $json.organic_results[7] : \"N/A\"}}",
            "Internal link 9": "={{ $json.organic_results[8] ? $json.organic_results[8] : \"N/A\"}}",
            "Internal link 10": "={{ $json.organic_results[9] ? $json.organic_results[9] : \"N/A\"}}"
          },
          "schema": [
            {
              "id": "Domain",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Domain",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Keyword",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "Keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 4",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 5",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 6",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 6",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 8",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 8",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Internal link 10",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Internal link 10",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "URL"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit#gid=0",
          "cachedResultName": "Internal links"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit?usp=drivesdk",
          "cachedResultName": "Find internal links using SerpAPI - N8N demo"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "172e75f7-9698-4c87-819e-edf94ea20612",
      "name": "Get URLs and keywords",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        128,
        144
      ],
      "parameters": {
        "options": {},
        "filtersUI": {
          "values": [
            {
              "lookupColumn": "Internal link 1"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit#gid=0",
          "cachedResultName": "Internal links"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1U0whmy1d7wPoWTNTa1u68QrLTE1NqYgyyFXxARWo_1M/edit?usp=drivesdk",
          "cachedResultName": "Find internal links using SerpAPI - N8N demo"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "1d1a37d8-8b2b-4399-9abe-13dff9cc1ad2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        16,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 704,
        "content": "## Get URLs and keywords\nIn this node, we're getting the different URLs that we want suggestions for, and the keywords/topics those pages are targeting.\n\nWe're filtering out everything that already has a value in the `internal link 1` column so we don't override rows we already processed in a previous run\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2f549918-a4ca-4a99-b60a-94b903c7f994",
      "name": "Manual trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -112,
        144
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "36b666a5-613b-43bc-a940-b5bb546bdbfa",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 704,
        "content": "## Loop over URLs\nLoop over each URL to get the results. Here we're doing it in batches of 5\n\n:warning: Note :warning:\nDon't set the batch size too high or you'll start hitting rate limits for some of the APIs, especially Google Sheets. \n\nIf you have a small number of URLs to process you can leave it at five. If you have a larger set of URLs consider setting the batch size lower, or adding a wait node at the end.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3dc9bb5d-03c4-423b-96d0-120badad6810",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        720,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 704,
        "content": "## Get search results using SerpAPI\nGet the search results from SerpAPI.\n\nWe're searching for the target keyword, while doing a `site:<domain>` search so we only get results from our site.\n\nWe use `-inurl:<url>` to exclude the URL we are currently working with. This gives us results similar to the URL we have that we can building internal links from.\n\n:warning: Note :warning:\nIf you have to process a large set of URLs it's worth [setting up a programmable search engine](https://drlee.io/build-your-own-google-create-a-custom-search-engine-with-trusted-sources-c1c113e845cc) instead of using SerAPI\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "75af678f-5af2-4aa7-8f00-d6fc84ee18e6",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 704,
        "content": "## Extract organic links from JSON\nSerpAPI gives us a bunch of data we don't need for this use case so we just filter for the organic results. specifically, we filter the URLs it gives us\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "72d62473-fab0-4db5-92f8-4cac1ab9acb0",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1424,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "height": 704,
        "content": "## Update Google Sheet\nHere we're adding the URLs to the different column in our sheet.\n\nNote that we're using an inline `if` statement so, if there's no URL present, it just adds 'N/A' to our Google Sheet. Not all URLs will have 10 internal link opportunities\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "e229c5a9-44c9-44c5-af81-ca2859283831",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -592,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 704,
        "content": "## Overview\n\nUse this workflow to spot internal linking ideas on your site and improve your search performance. It takes your target URLs and keywords, finds related pages, and suggests where to add links. Strong internal linking helps search engines understand your site.\n\n## How it works\n- You provide a list of target URLs and the keywords you want to rank for\n- The workflow uses the SERP API to search your site for related pages, skipping the target URL\n- It filters the results and pulls relevant URLs\n- It writes the suggestions to a Google Sheet, and adds \u201cN/A\u201d if no good matches are found\n\n## Setup steps\n1. Turn on the Google Sheets API and create a sheet with your domain, target URLs, and keywords\n2. Create a SERP API account and get an API key\n3. Optional: Set up a Google Programmable Search Engine if you prefer not to use the SERP API\n4. Add your SERP API key and Google Sheets credentials to n8n.\n5. Run the workflow to generate internal link suggestions in your Google Sheet"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5d62c2bc-3ff0-4b0c-8af5-76d8fe285226",
  "connections": {
    "Manual trigger": {
      "main": [
        [
          {
            "node": "Get URLs and keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Get search results using SerpAPI",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get URLs and keywords": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract links from JSON": {
      "main": [
        [
          {
            "node": "Add internal URL to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add internal URL to Google Sheet": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get search results using SerpAPI": {
      "main": [
        [
          {
            "node": "Extract links from JSON",
            "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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Use this workflow to spot internal linking ideas on your site and improve your search performance. It takes your target URLs and keywords, finds related pages, and suggests where to add links. Strong internal linking helps search engines understand your site. You provide a list…

Source: https://n8n.io/workflows/9071/ — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

This template is ideal for solo store owners, eCommerce marketers, automation beginners, or anyone using Shopify and Gmail who wants to recover lost revenue without coding.

HTTP Request, Gmail, Twilio +3
Data & Sheets

PCN. Uses googleSheets, httpRequest, @n-octo-n/n8n-nodes-json-database, itemLists. Event-driven trigger; 60 nodes.

Google Sheets, HTTP Request, @N Octo N/N8N Nodes Json Database +3
Data & Sheets

The workflow automates the process of gathering extensive keyword data for a "Main Keyword." It starts by reading initial parameters from a Google Sheets template, creates a new dedicated Google Sheet

Google Sheets, Google Drive, HTTP Request
Data & Sheets

🔥 March Sale – n8n Community Members Get ideoGener8r for Just $27! (Reg. $47) Use Coupon Code: (Valid until 3/31/2025 for n8n community members)

HTTP Request, Google Drive, Google Sheets
Data & Sheets

📄 Documentation: Notion Guide

Google Sheets, Google Drive, HTTP Request +2