AutomationFlowsAI & RAG › Add Internal Links to Wordpress Posts with Openai, Notion, and Slack

Add Internal Links to Wordpress Posts with Openai, Notion, and Slack

ByManik Chandra Dhor @youthfireit on n8n.io

This workflow runs manually or every Monday at 9AM to analyze published WordPress posts with OpenAI embeddings, injects semantically relevant internal links using AI-selected anchor text, logs link updates to Notion, and posts Slack alerts for orphan pages and hub-page…

Event trigger★★★★☆ complexityAI-powered21 nodesWordPressHTTP RequestOpenAINotionSlack
AI & RAG Trigger: Event Nodes: 21 Complexity: ★★★★☆ AI nodes: yes Added:
Add Internal Links to Wordpress Posts with Openai, Notion, and Slack — n8n workflow card showing WordPress, HTTP Request, OpenAI integration

This workflow corresponds to n8n.io template #16985 — we link there as the canonical source.

This workflow follows the HTTP Request → Notion 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
{
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "name": "Add internal links to WordPress posts with AI embeddings",
  "tags": [],
  "nodes": [
    {
      "id": "lite-manual",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -40,
        240
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "lite-sched",
      "name": "Weekly Monday 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -40,
        400
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "lite-config",
      "name": "Configuration",
      "type": "n8n-nodes-base.code",
      "position": [
        300,
        320
      ],
      "parameters": {
        "jsCode": "// \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n// \u2502  Edit the values below, then click Test workflow \u2502\n// \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nconst config = {\n  brandName: 'Acme Analytics',     // used in Slack alerts\n  wpUrl:     'https://acme.com'    // your WordPress site\n};\nreturn [{ json: config }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "ee537680-ade4-4608-84f1-c6ec03779f29",
      "name": "Fetch All Posts",
      "type": "n8n-nodes-base.wordpress",
      "notes": "Fetches all published posts from WordPress.",
      "maxTries": 3,
      "position": [
        680,
        300
      ],
      "parameters": {
        "filter": {
          "status": "publish",
          "perPage": 100,
          "postType": "post"
        },
        "resource": "post",
        "operation": "getAll"
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "8d03e356-dbe6-4326-9fa5-319212cf6955",
      "name": "Extract Post Data",
      "type": "n8n-nodes-base.code",
      "notes": "Cleans and extracts post data for embedding.",
      "position": [
        900,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Extract title + first 500 chars of content for embedding\nconst items = $input.all();\nreturn items.map(item => {\n  const json = item.json;\n  return {\n    json: {\n      id: json.id,\n      title: json.title?.rendered || json.title || '',\n      excerpt: (json.excerpt?.rendered || '').replace(/<[^>]+>/g, '').slice(0, 500),\n      url: json.link,\n      content: (json.content?.rendered || '').replace(/<[^>]+>/g, '').slice(0, 2000),\n      brandName: $('Configuration').first().json.brandName\n    }\n  };\n});\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "fef737b1-45d3-45b6-95c9-ac24ff30ef39",
      "name": "Loop Embeddings",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1120,
        300
      ],
      "parameters": {
        "options": {},
        "batchSize": 20
      },
      "typeVersion": 1
    },
    {
      "id": "1ffbb33a-789e-41a2-ae47-7bb8c334f450",
      "name": "OpenAI Embeddings",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Generates embeddings for each post using text-embedding-3-small (cost-effective).",
      "maxTries": 3,
      "position": [
        1640,
        300
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/embeddings",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": false,
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={\"model\": \"text-embedding-3-small\", \"input\": \"{{ $json.title + ' ' + $json.excerpt }}\"}",
        "sendBody": true,
        "sendQuery": false,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "specifyHeaders": "keypair",
        "queryParameters": {},
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 4,
      "waitBetweenTries": 2000
    },
    {
      "id": "62e88ec0-c124-4c7b-94a8-24a58356139d",
      "name": "Build Link Graph",
      "type": "n8n-nodes-base.code",
      "notes": "Computes cosine similarity and identifies orphans + hub candidates.",
      "position": [
        1860,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Build similarity matrix using cosine similarity\nconst items = $input.all();\n\n// Cosine similarity function\nfunction cosineSim(a, b) {\n  let dot = 0, magA = 0, magB = 0;\n  for (let i = 0; i < a.length; i++) {\n    dot += a[i] * b[i];\n    magA += a[i] * a[i];\n    magB += b[i] * b[i];\n  }\n  return dot / (Math.sqrt(magA) * Math.sqrt(magB));\n}\n\n// Compute pairwise similarities (top 5 per post)\nconst suggestions = [];\nfor (let i = 0; i < items.length; i++) {\n  const post = items[i].json;\n  const embedding = post.data?.[0]?.embedding || [];\n  if (embedding.length === 0) continue;\n\n  const sims = [];\n  for (let j = 0; j < items.length; j++) {\n    if (i === j) continue;\n    const other = items[j].json;\n    const otherEmb = other.data?.[0]?.embedding || [];\n    if (otherEmb.length === 0) continue;\n    const sim = cosineSim(embedding, otherEmb);\n    sims.push({targetPost: other, similarity: sim});\n  }\n\n  // Top 5 most similar\n  const top5 = sims.sort((a, b) => b.similarity - a.similarity).slice(0, 5);\n\n  // Identify orphans (no existing internal links pointing to this post)\n  suggestions.push({\n    sourcePost: {id: post.id, title: post.title, url: post.url, content: post.content},\n    suggestions: top5.filter(s => s.similarity > 0.7).map(s => ({\n      targetId: s.targetPost.id,\n      targetTitle: s.targetPost.title,\n      targetUrl: s.targetPost.url,\n      similarity: Math.round(s.similarity * 100) / 100\n    })),\n    isOrphan: top5.every(s => s.similarity < 0.6),\n    isHubCandidate: top5.filter(s => s.similarity > 0.8).length >= 5\n  });\n}\n\nreturn suggestions.map(s => ({json: s}));\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "a149120b-4c16-4482-b54c-065f85ed0cb3",
      "name": "Has Suggestions?",
      "type": "n8n-nodes-base.if",
      "position": [
        2080,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ ($json.suggestions || []).length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "a4a7093b-51ba-489a-905f-0a0a4512fd82",
      "name": "Pick Anchor Text",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "notes": "Picks natural anchor text from source content.",
      "maxTries": 3,
      "position": [
        2300,
        300
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {
          "temperature": 0.3,
          "responseFormat": "json_object"
        },
        "messages": {
          "messageValues": [
            {
              "role": "system",
              "content": "You pick natural anchor text for internal links. Output JSON."
            },
            {
              "role": "user",
              "content": "=Source post: {{$json.sourcePost.title}}\nSource content (first 1000 chars): {{$json.sourcePost.content.slice(0, 1000)}}\nTarget posts: {{JSON.stringify($json.suggestions)}}\n\nFor each target, return JSON: [{\"targetId\": ..., \"targetTitle\": ..., \"anchorText\": \"<natural phrase from source content>\", \"suggestedSentence\": \"<the sentence in source where link should go>\"}]"
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "d2b3e102-8ff9-451f-ad6f-2e07f5ce6915",
      "name": "Build Update Payload",
      "type": "n8n-nodes-base.code",
      "notes": "Builds updated content with injected internal links.",
      "position": [
        2820,
        300
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "// Build WP update payload with internal links injected\nconst json = $input.first().json;\nconst links = JSON.parse(json.message?.content || '[]');\n\n// Get original content and inject links\nlet updatedContent = json.sourcePost.content;\nlinks.forEach(link => {\n  if (link.anchorText && link.suggestedSentence) {\n    // Find the sentence and replace with linked version\n    const linkedSentence = link.suggestedSentence.replace(\n      link.anchorText,\n      `<a href=\"${link.targetUrl}\">${link.anchorText}</a>`\n    );\n    updatedContent = updatedContent.replace(link.suggestedSentence, linkedSentence);\n  }\n});\n\nreturn [{\n  json: {\n    ...json,\n    postId: json.sourcePost.id,\n    updatedContent,\n    injectedLinks: links.length\n  }\n}];\n"
      },
      "notesInFlow": true,
      "typeVersion": 2
    },
    {
      "id": "f3f1f456-e2ef-4a93-b3e0-fadd95a894e3",
      "name": "Update WP Post",
      "type": "n8n-nodes-base.wordpress",
      "notes": "Updates post with new internal links. Set to draft first for review if preferred.",
      "maxTries": 3,
      "position": [
        3040,
        300
      ],
      "parameters": {
        "postId": "={{ $json.postId }}",
        "resource": "post",
        "operation": "update",
        "additionalFields": {
          "content": "={{ $json.updatedContent }}"
        }
      },
      "credentials": {
        "wordpressApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 1,
      "waitBetweenTries": 2000
    },
    {
      "id": "1e38a30d-d07e-49a7-adda-f24744bd8f73",
      "name": "Log Link Updates",
      "type": "n8n-nodes-base.notion",
      "notes": "Logs internal link injections.",
      "maxTries": 3,
      "position": [
        3260,
        300
      ],
      "parameters": {
        "resource": "databasePage",
        "operation": "create",
        "databaseId": "YOUR_LINK_LOG_DB",
        "propertiesUi": {
          "propertyValues": [
            {
              "key": "Date|date",
              "date": "={{ $today.toISODate() }}"
            },
            {
              "key": "Post|title",
              "title": "={{ $json.sourcePost.title }}"
            },
            {
              "key": "Links Added|number",
              "number": "={{ $json.injectedLinks }}"
            },
            {
              "key": "Is Orphan|checkbox",
              "checkbox": "={{ $json.isOrphan }}"
            },
            {
              "key": "Is Hub Candidate|checkbox",
              "checkbox": "={{ $json.isHubCandidate }}"
            },
            {
              "key": "Client|select",
              "select_name": "={{ $('Configuration').first().json.brandName }}"
            }
          ]
        }
      },
      "credentials": {
        "notionApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 2,
      "waitBetweenTries": 2000
    },
    {
      "id": "a2b2025a-f385-429a-a877-37a0d016b247",
      "name": "Is Orphan?",
      "type": "n8n-nodes-base.if",
      "position": [
        3480,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "eq"
              },
              "leftValue": "={{ $json.isOrphan }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "aa36e4b5-b002-4344-a843-ec95944302dc",
      "name": "Orphan Page Alert",
      "type": "n8n-nodes-base.slack",
      "notes": "Alerts on orphan pages with no internal links.",
      "maxTries": 3,
      "position": [
        3700,
        300
      ],
      "parameters": {
        "text": "=:warning: *Orphan page detected!*\n*Post*: {{$json.sourcePost.title}}\n*URL*: {{$json.sourcePost.url}}\n\nThis page has no semantic siblings above 0.6 similarity threshold. Consider:\n- Creating supporting cluster content\n- Adding manual internal links from pillar page\n- Improving content to align with topical cluster",
        "select": "channel",
        "channelId": "YOUR_SLACK_CHANNEL_ID",
        "operation": "post",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 2,
      "waitBetweenTries": 2000
    },
    {
      "id": "b1e76ee5-97df-4ebc-885e-721786a6000a",
      "name": "Hub Candidate?",
      "type": "n8n-nodes-base.if",
      "position": [
        3920,
        300
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "eq"
              },
              "leftValue": "={{ $json.isHubCandidate }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "32ff6b95-f651-4f26-b91a-fe9b43152964",
      "name": "Hub Candidate Alert",
      "type": "n8n-nodes-base.slack",
      "notes": "Alerts on hub page candidates.",
      "maxTries": 3,
      "position": [
        4140,
        300
      ],
      "parameters": {
        "text": "=:star: *Hub page candidate identified!*\n*Post*: {{$json.sourcePost.title}}\n*URL*: {{$json.sourcePost.url}}\n\nThis post has 5+ semantic siblings above 0.8 similarity. Consider:\n- Upgrading to a comprehensive pillar page\n- Building a topic cluster around it\n- Adding a 'Related Posts' section",
        "select": "channel",
        "channelId": "YOUR_SLACK_CHANNEL_ID",
        "operation": "post",
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "notesInFlow": true,
      "retryOnFail": true,
      "typeVersion": 2,
      "waitBetweenTries": 2000
    },
    {
      "id": "main",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -817
      ],
      "parameters": {
        "width": 760,
        "height": 967,
        "content": "## Add Internal Links to WordPress Posts with AI\n\nReads every published post on your WordPress site, uses AI embeddings to find the posts that are genuinely related, writes natural anchor-text links between them, and flags orphan pages (no internal links in) and hub candidates (strong link targets). Internal linking is one of the highest-impact on-page SEO tasks, and this runs it for you every week.\n\n**Who's it for:** WordPress site owners and content teams who want internal linking handled automatically.\n\n## How it works\n1. Fetches all published WordPress posts.\n2. Creates OpenAI embeddings for each post.\n3. Ranks the most semantically related posts for every article.\n4. Picks natural anchor text with AI and updates the post.\n5. Logs each change to Notion and alerts Slack on orphan and hub pages.\n\n## Setup\n1. Add credentials: WordPress, OpenAI (Header Auth, Bearer token), Notion, Slack.\n2. Open the Configuration node and set your brand and site URL.\n3. Create a Notion 'Link Updates' database with columns: Date (date), Post (title), Links Added (number), Is Orphan (checkbox), Is Hub Candidate (checkbox), Client (select). Select it in Log Link Updates.\n4. Set your Slack channel.\n5. Run with Test workflow first to preview link edits, then activate. Tip: disable Update WP Post to preview before writing live changes."
      },
      "typeVersion": 1
    },
    {
      "id": "sec0",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        170
      ],
      "parameters": {
        "color": "color7",
        "width": 700,
        "height": 90,
        "content": "## 1. Read all posts"
      },
      "typeVersion": 1
    },
    {
      "id": "sec1",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1600,
        170
      ],
      "parameters": {
        "color": "color7",
        "width": 920,
        "height": 90,
        "content": "## 2. Embed & match"
      },
      "typeVersion": 1
    },
    {
      "id": "sec2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2780,
        170
      ],
      "parameters": {
        "color": "color7",
        "width": 1580,
        "height": 90,
        "content": "## 3. Link, log & alert"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Is Orphan?": {
      "main": [
        [
          {
            "node": "Orphan Page Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Hub Candidate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configuration": {
      "main": [
        [
          {
            "node": "Fetch All Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hub Candidate?": {
      "main": [
        [
          {
            "node": "Hub Candidate Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update WP Post": {
      "main": [
        [
          {
            "node": "Log Link Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch All Posts": {
      "main": [
        [
          {
            "node": "Extract Post Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Embeddings": {
      "main": [
        [
          {
            "node": "OpenAI Embeddings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Link Graph": {
      "main": [
        [
          {
            "node": "Has Suggestions?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Suggestions?": {
      "main": [
        [
          {
            "node": "Pick Anchor Text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Link Updates": {
      "main": [
        [
          {
            "node": "Is Orphan?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pick Anchor Text": {
      "main": [
        [
          {
            "node": "Build Update Payload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Post Data": {
      "main": [
        [
          {
            "node": "Loop Embeddings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Embeddings": {
      "main": [
        [
          {
            "node": "Build Link Graph",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Orphan Page Alert": {
      "main": [
        [
          {
            "node": "Hub Candidate?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Monday 9AM": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Update Payload": {
      "main": [
        [
          {
            "node": "Update WP Post",
            "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

This workflow runs manually or every Monday at 9AM to analyze published WordPress posts with OpenAI embeddings, injects semantically relevant internal links using AI-selected anchor text, logs link updates to Notion, and posts Slack alerts for orphan pages and hub-page…

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This advanced n8n workflow automates the full lead enrichment, qualification, and personalized outreach process tailored specifically for the B2B real estate sector. Integrating top platforms like Api

N8N Nodes Fillout, OpenAI Chat, Pinecone Vector Store +11
AI & RAG

Security incidents. Uses googleDriveTrigger, googleDrive, vectorStorePinecone, documentDefaultDataLoader. Event-driven trigger; 29 nodes.

Google Drive Trigger, Google Drive, Pinecone Vector Store +9
AI & RAG

02 — RAG Chatbot: Ingestion + Grounded Q&A. Uses googleDrive, openAi, httpRequest, googleSheets. Event-driven trigger; 23 nodes.

Google Drive, OpenAI, HTTP Request +3
AI & RAG

Alfred (funcional). Uses gmailTool, googleCalendarTool, gmail, embeddingsOpenAi. Event-driven trigger; 83 nodes.

Gmail Tool, Google Calendar Tool, Gmail +24
AI & RAG

Your AI workforce is ready. Are you?

Google Sheets Tool, Mcp Trigger, Google Drive +29