{
  "nodes": [
    {
      "id": "1c4e1f91-f6fa-4e4b-a90b-cd8d1a269904",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1136,
        -576
      ],
      "parameters": {
        "width": 460,
        "height": 936,
        "content": "## Competitor Content Gap Analyzer \u2014 GPT-4o-mini + Slack\n\nFor SEO teams, content strategists, and agency analysts who want to instantly compare any two web pages and get a detailed content gap report in Slack. Submit your page URL, a competitor page URL, and a target keyword via the form. The workflow scrapes both pages in parallel, strips all HTML leaving only readable text, and passes both to GPT-4o-mini which produces a 6-section analysis covering keyword usage, missing topics, your competitive advantages, content depth, five priority actions, and a quick verdict. The full report is posted directly to your Slack channel.\n\n## How it works\n- **1. Form \u2014 Submit Page URLs** collects your URL, competitor URL, target keyword, and business name\n- **2. Set \u2014 Extract Form Fields** maps inputs to clean variables and adds a run timestamp\n- **3\u20134. HTTP nodes** scrape both pages simultaneously in parallel\n- **5\u20136. Code nodes** strip scripts, styles, and HTML tags \u2014 keeping only readable text limited to 8000 characters\n- **7. Merge \u2014 Combine Both Pages** joins both cleaned pages into one pipeline\n- **8. Code \u2014 Combine Page Data** safely merges both items into one clean object\n- **9. AI Agent \u2014 Gap Analyzer** uses GPT-4o-mini to produce a 6-section content gap report\n- **11. Set \u2014 Prepare Slack Message** assembles all report fields\n- **12. Slack \u2014 Send Gap Report** posts the full analysis to your channel\n\n## Set up steps\n1. In **10. OpenAI \u2014 GPT-4o-mini Model** \u2014 connect your OpenAI API credential\n2. In **12. Slack \u2014 Send Gap Report** \u2014 connect your Slack OAuth2 credential and set your channel name\n3. Activate the workflow and copy the Form URL from node 1\n4. Open the Form URL in a browser, fill in all four fields, and submit"
      },
      "typeVersion": 1
    },
    {
      "id": "8279653d-a645-49d2-b8d3-d27b4e9e6214",
      "name": "Section \u2014 Form Input and Field Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -624,
        -304
      ],
      "parameters": {
        "color": 5,
        "width": 436,
        "height": 404,
        "content": "## Form Input and Field Extraction\nUser submits both page URLs, the target keyword, and business name. All inputs are mapped to clean variables with a run timestamp added automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "0af6d093-d6fe-4368-9cc5-683427fe4a2e",
      "name": "Section \u2014 Parallel Page Scraping and Cleaning",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -160,
        -432
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 660,
        "content": "## Parallel Page Scraping and HTML Cleaning\nBoth pages are scraped simultaneously. Each Code node strips scripts, styles, and all HTML tags \u2014 keeping only readable text limited to 8000 characters for GPT token efficiency."
      },
      "typeVersion": 1
    },
    {
      "id": "10c3dfc2-19c5-4e6e-972d-6bdbdf694bf9",
      "name": "Section \u2014 Data Merge",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        304,
        -320
      ],
      "parameters": {
        "color": 6,
        "width": 420,
        "height": 420,
        "content": "## Data Merge\nBoth cleaned page texts are merged into a single pipeline item. A Code node safely combines them with fallback handling if either page failed to scrape."
      },
      "typeVersion": 1
    },
    {
      "id": "06247d1a-6f92-4dde-b547-89fbe572d051",
      "name": "Section \u2014 AI Content Gap Analysis",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        768,
        -416
      ],
      "parameters": {
        "color": 6,
        "width": 344,
        "height": 644,
        "content": "## AI Content Gap Analysis\nGPT-4o-mini compares both pages and produces a 6-section report: keyword usage, missing topics, your advantages, content depth, five priority actions, and a quick verdict."
      },
      "typeVersion": 1
    },
    {
      "id": "54cc755a-8708-40da-926a-2662e09afa60",
      "name": "Section \u2014 Slack Report Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1152,
        -272
      ],
      "parameters": {
        "color": 4,
        "width": 420,
        "height": 388,
        "content": "## Slack Report Delivery\nAssembles the full report message with business name, keyword, URLs, and AI analysis. Posts the complete 6-section gap report to your Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "68df874f-a996-445a-8c1a-7595e22571cd",
      "name": "Note \u2014 Bot-Protected Sites May Block Scraping",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        128,
        288
      ],
      "parameters": {
        "color": 3,
        "width": 700,
        "content": "## \u26a0\ufe0f Bot-Protected Sites May Return 403\nSome websites block automated HTTP requests and return a 403 Forbidden error. If scraping fails, add a User-Agent header to this node: Name = User-Agent, Value = Mozilla/5.0 (compatible; n8n-bot/1.0). Do the same for node 4."
      },
      "typeVersion": 1
    },
    {
      "id": "a9879d2d-85da-46ff-9563-d7fbf2ab4326",
      "name": "1. Form \u2014 Submit Page URLs",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -576,
        -112
      ],
      "parameters": {
        "options": {},
        "formTitle": "Competitor Content Gap Analyzer",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Your Page URL",
              "placeholder": "https://www.yoursite.com/your-page",
              "requiredField": true
            },
            {
              "fieldLabel": "Competitor Page URL",
              "placeholder": "https://www.competitorsite.com/their-page",
              "requiredField": true
            },
            {
              "fieldLabel": "Target Keyword",
              "placeholder": "e.g. dental implants london",
              "requiredField": true
            },
            {
              "fieldLabel": "Your Business Name",
              "placeholder": "e.g. Incrementors",
              "requiredField": true
            }
          ]
        },
        "formDescription": "Enter your page URL and a competitor's page URL. AI will scrape both, compare them, and send a full content gap report to your Slack channel."
      },
      "typeVersion": 2.2
    },
    {
      "id": "33c6adfd-f768-43a5-9120-218521ea1e78",
      "name": "2. Set \u2014 Extract Form Fields",
      "type": "n8n-nodes-base.set",
      "position": [
        -352,
        -112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-001",
              "name": "yourUrl",
              "type": "string",
              "value": "={{ $json['Your Page URL'] }}"
            },
            {
              "id": "cfg-002",
              "name": "competitorUrl",
              "type": "string",
              "value": "={{ $json['Competitor Page URL'] }}"
            },
            {
              "id": "cfg-003",
              "name": "targetKeyword",
              "type": "string",
              "value": "={{ $json['Target Keyword'] }}"
            },
            {
              "id": "cfg-004",
              "name": "businessName",
              "type": "string",
              "value": "={{ $json['Your Business Name'] }}"
            },
            {
              "id": "cfg-005",
              "name": "runDate",
              "type": "string",
              "value": "={{ $now.toFormat('dd MMM yyyy HH:mm') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0b52a604-0d33-4dbf-84a8-03ab464d5aaf",
      "name": "3. HTTP \u2014 Scrape Your Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -112,
        -256
      ],
      "parameters": {
        "url": "={{ $json.yourUrl }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {}
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "22aff6a2-8c0d-4801-9f53-3493f738eeaf",
      "name": "4. HTTP \u2014 Scrape Competitor Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -112,
        32
      ],
      "parameters": {
        "url": "={{ $('2. Set \u2014 Extract Form Fields').item.json.competitorUrl }}",
        "options": {
          "timeout": 15000,
          "response": {
            "response": {}
          }
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6a4f7779-4912-467e-9693-402d60eeeb4c",
      "name": "5. Code \u2014 Clean Your Page HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        -256
      ],
      "parameters": {
        "jsCode": "// Get both scraped pages\nconst yourHtml = $input.first().json.data || $input.first().json.body || $input.first().json || '';\nconst config = $('2. Set \u2014 Extract Form Fields').item.json;\n\n// Clean HTML \u2014 remove scripts, styles, keep text\nfunction cleanHtml(html) {\n  if (typeof html !== 'string') html = JSON.stringify(html);\n  return html\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .substring(0, 8000);\n}\n\nconst cleanedYours = cleanHtml(yourHtml);\n\nreturn [{\n  json: {\n    yourPageText: cleanedYours,\n    yourUrl: config.yourUrl,\n    competitorUrl: config.competitorUrl,\n    targetKeyword: config.targetKeyword,\n    businessName: config.businessName,\n    runDate: config.runDate\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "041aaf0d-c096-46d3-bb15-175353b652f1",
      "name": "6. Code \u2014 Clean Competitor Page HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        112,
        32
      ],
      "parameters": {
        "jsCode": "// Get competitor scraped page\nconst competitorHtml = $input.first().json.data || $input.first().json.body || $input.first().json || '';\nconst yourPageData = $('5. Code \u2014 Clean Your Page HTML').item.json;\n\n// Clean HTML \u2014 remove scripts, styles, keep text\nfunction cleanHtml(html) {\n  if (typeof html !== 'string') html = JSON.stringify(html);\n  return html\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, '')\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, '')\n    .replace(/<[^>]+>/g, ' ')\n    .replace(/\\s+/g, ' ')\n    .trim()\n    .substring(0, 8000);\n}\n\nconst cleanedCompetitor = cleanHtml(competitorHtml);\n\nreturn [{\n  json: {\n    yourPageText: yourPageData.yourPageText,\n    competitorPageText: cleanedCompetitor,\n    yourUrl: yourPageData.yourUrl,\n    competitorUrl: yourPageData.competitorUrl,\n    targetKeyword: yourPageData.targetKeyword,\n    businessName: yourPageData.businessName,\n    runDate: yourPageData.runDate\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "2a05b77c-e1eb-4ee5-9b10-595e2c808857",
      "name": "7. Merge \u2014 Combine Both Pages",
      "type": "n8n-nodes-base.merge",
      "position": [
        368,
        -112
      ],
      "parameters": {
        "mode": "combine",
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "f434b710-929c-4eab-b5f3-c0be60e761d5",
      "name": "8. Code \u2014 Combine Page Data",
      "type": "n8n-nodes-base.code",
      "position": [
        576,
        -112
      ],
      "parameters": {
        "jsCode": "// Merge both cleaned page texts into one item for AI Agent\nconst items = $input.all();\n\n// Item 0 = Your page (from Clean Your Page HTML)\n// Item 1 = Competitor page (from Clean Competitor Page HTML)\nconst yourData = items[0]?.json || {};\nconst competitorData = items[1]?.json || {};\n\nreturn [{\n  json: {\n    yourPageText: yourData.yourPageText || 'Could not scrape your page.',\n    competitorPageText: competitorData.competitorPageText || 'Could not scrape competitor page.',\n    yourUrl: yourData.yourUrl || competitorData.yourUrl,\n    competitorUrl: yourData.competitorUrl || competitorData.competitorUrl,\n    targetKeyword: yourData.targetKeyword || competitorData.targetKeyword,\n    businessName: yourData.businessName || competitorData.businessName,\n    runDate: yourData.runDate || competitorData.runDate\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ac6a3d06-bbff-4a5c-ba51-1473927e5764",
      "name": "9. AI Agent \u2014 Gap Analyzer",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        816,
        -112
      ],
      "parameters": {
        "text": "=You are a senior SEO content strategist.\n\nYour job is to compare two web pages targeting the same keyword and produce a detailed content gap analysis.\n\nBUSINESS: {{ $json.businessName }}\nTARGET KEYWORD: {{ $json.targetKeyword }}\nYOUR PAGE URL: {{ $json.yourUrl }}\nCOMPETITOR PAGE URL: {{ $json.competitorUrl }}\n\nYOUR PAGE CONTENT:\n{{ $json.yourPageText }}\n\nCOMPETITOR PAGE CONTENT:\n{{ $json.competitorPageText }}\n\nAnalyze both pages carefully and produce the following report. Use plain text only. No markdown. No asterisks. Use CAPS for all section headings.\n\nSECTION 1 \u2014 KEYWORD USAGE COMPARISON\nFor the target keyword and related terms, compare how each page uses them. Note which page uses the keyword more naturally, in headings, in opening paragraphs, and throughout the body.\n\nSECTION 2 \u2014 CONTENT TOPICS YOUR PAGE IS MISSING\nList the specific topics, subtopics, questions, or angles the competitor covers that your page does not. Number each gap. Be specific \u2014 not generic.\n\nSECTION 3 \u2014 CONTENT TOPICS YOU HAVE THAT COMPETITOR LACKS\nList what your page covers that the competitor does not. This is your competitive advantage. Number each point.\n\nSECTION 4 \u2014 CONTENT DEPTH AND QUALITY COMPARISON\nCompare word count estimate, use of examples, use of data or stats, use of FAQs, clarity of value proposition, and calls to action.\n\nSECTION 5 \u2014 TOP 5 PRIORITY ACTIONS\nList exactly 5 specific actions to improve your page based on the gap analysis. Each action must be concrete and actionable. Number them 1 to 5. Prioritize highest impact first.\n\nSECTION 6 \u2014 QUICK VERDICT\nIn 3 sentences or less: is your page stronger or weaker than the competitor for this keyword, and what is the single most important thing to fix first.\n\nRULES:\n- Plain text only. No markdown symbols.\n- Be specific and data-driven. Reference actual content from both pages.\n- Do not write generic SEO advice. Only insights from these two specific pages.",
        "options": {},
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "bdf72fa1-45cf-43cc-a5bd-069501a9544e",
      "name": "10. OpenAI \u2014 GPT-4o-mini Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        816,
        96
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {
          "maxTokens": 1500,
          "temperature": 0.4
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "9bd4af4b-4f8c-4b96-8b28-35b994eb3a28",
      "name": "11. Set \u2014 Prepare Slack Message",
      "type": "n8n-nodes-base.set",
      "position": [
        1200,
        -112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "slack-001",
              "name": "gapReport",
              "type": "string",
              "value": "={{ $json.output || 'Gap analysis could not be generated. Please check OpenAI credentials.' }}"
            },
            {
              "id": "slack-002",
              "name": "yourUrl",
              "type": "string",
              "value": "={{ $('8. Code \u2014 Combine Page Data').item.json.yourUrl }}"
            },
            {
              "id": "slack-003",
              "name": "competitorUrl",
              "type": "string",
              "value": "={{ $('8. Code \u2014 Combine Page Data').item.json.competitorUrl }}"
            },
            {
              "id": "slack-004",
              "name": "targetKeyword",
              "type": "string",
              "value": "={{ $('8. Code \u2014 Combine Page Data').item.json.targetKeyword }}"
            },
            {
              "id": "slack-005",
              "name": "businessName",
              "type": "string",
              "value": "={{ $('8. Code \u2014 Combine Page Data').item.json.businessName }}"
            },
            {
              "id": "slack-006",
              "name": "runDate",
              "type": "string",
              "value": "={{ $('8. Code \u2014 Combine Page Data').item.json.runDate }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ac4cccec-ae59-4383-bb20-68867ff0f123",
      "name": "12. Slack \u2014 Send Gap Report",
      "type": "n8n-nodes-base.slack",
      "position": [
        1408,
        -112
      ],
      "parameters": {
        "text": "=*Competitor Content Gap Report*\n\n*Business:* {{ $json.businessName }}\n*Target Keyword:* {{ $json.targetKeyword }}\n*Run Date:* {{ $json.runDate }}\n\n*Your Page:* {{ $json.yourUrl }}\n*Competitor Page:* {{ $json.competitorUrl }}\n\n---\n\n{{ $json.gapReport }}\n\n---\n_Report generated by n8n + GPT-4o-mini_",
        "otherOptions": {
          "mrkdwn": true
        },
        "authentication": "oAuth2"
      },
      "typeVersion": 2.2
    }
  ],
  "connections": {
    "1. Form \u2014 Submit Page URLs": {
      "main": [
        [
          {
            "node": "2. Set \u2014 Extract Form Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "3. HTTP \u2014 Scrape Your Page": {
      "main": [
        [
          {
            "node": "5. Code \u2014 Clean Your Page HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "9. AI Agent \u2014 Gap Analyzer": {
      "main": [
        [
          {
            "node": "11. Set \u2014 Prepare Slack Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "8. Code \u2014 Combine Page Data": {
      "main": [
        [
          {
            "node": "9. AI Agent \u2014 Gap Analyzer",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "2. Set \u2014 Extract Form Fields": {
      "main": [
        [
          {
            "node": "3. HTTP \u2014 Scrape Your Page",
            "type": "main",
            "index": 0
          },
          {
            "node": "4. HTTP \u2014 Scrape Competitor Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "7. Merge \u2014 Combine Both Pages": {
      "main": [
        [
          {
            "node": "8. Code \u2014 Combine Page Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "10. OpenAI \u2014 GPT-4o-mini Model": {
      "ai_languageModel": [
        [
          {
            "node": "9. AI Agent \u2014 Gap Analyzer",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "5. Code \u2014 Clean Your Page HTML": {
      "main": [
        [
          {
            "node": "7. Merge \u2014 Combine Both Pages",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "11. Set \u2014 Prepare Slack Message": {
      "main": [
        [
          {
            "node": "12. Slack \u2014 Send Gap Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "4. HTTP \u2014 Scrape Competitor Page": {
      "main": [
        [
          {
            "node": "6. Code \u2014 Clean Competitor Page HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "6. Code \u2014 Clean Competitor Page HTML": {
      "main": [
        [
          {
            "node": "7. Merge \u2014 Combine Both Pages",
            "type": "main",
            "index": 1
          }
        ]
      ]
    }
  }
}