{
  "name": "13 - Broken Link Monitor \u2192 Slack Alert",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 * * * *"
            }
          ]
        }
      },
      "id": "node-schedule",
      "name": "Schedule - Every Hour",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        260,
        400
      ]
    },
    {
      "parameters": {
        "jsCode": "// Define URLs to monitor\n// Add your URLs here \u2014 can also be loaded from Google Sheets or a config file\nconst urlsToCheck = [\n  { url: 'https://yoursite.com', label: 'Homepage', critical: true },\n  { url: 'https://yoursite.com/pricing', label: 'Pricing Page', critical: true },\n  { url: 'https://yoursite.com/blog', label: 'Blog', critical: false },\n  { url: 'https://yoursite.com/api/health', label: 'API Health Check', critical: true },\n  { url: 'https://yoursite.com/contact', label: 'Contact Page', critical: false },\n  { url: 'https://yoursite.com/login', label: 'Login Page', critical: true },\n  { url: 'https://yoursite.com/docs', label: 'Documentation', critical: false },\n  { url: 'https://app.yoursite.com', label: 'App Homepage', critical: true }\n];\n\nreturn urlsToCheck.map(item => ({ json: item }));"
      },
      "id": "node-url-list",
      "name": "Code - URL List to Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        480,
        400
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ $json.url }}",
        "options": {
          "timeout": 10000,
          "allowUnauthorizedCerts": false,
          "response": {
            "response": {
              "responseFormat": "text",
              "fullResponse": true
            }
          },
          "redirect": {
            "redirect": {
              "followRedirects": true,
              "maxRedirects": 5
            }
          }
        }
      },
      "id": "node-http-check",
      "name": "HTTP Request - Check URL",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        700,
        400
      ],
      "continueOnFail": true
    },
    {
      "parameters": {
        "jsCode": "const urlData = $('Code - URL List to Check').item.json;\nconst response = $input.first().json;\nconst error = $input.first().error;\n\nconst statusCode = response.statusCode || (error ? 0 : 200);\nconst responseTime = response.responseTime || 0;\n\nconst isBroken = statusCode === 0 || statusCode >= 400;\nconst isSlow = responseTime > 3000; // >3s is slow\n\nlet statusIcon = '\u2705';\nif (statusCode === 0) statusIcon = '\ud83d\udc80';\nelse if (statusCode >= 500) statusIcon = '\ud83d\udd34';\nelse if (statusCode >= 400) statusIcon = '\ud83d\udfe1';\nelse if (statusCode >= 300) statusIcon = '\ud83d\udfe0';\nelse if (isSlow) statusIcon = '\ud83d\udc22';\n\nreturn [{\n  json: {\n    url: urlData.url,\n    label: urlData.label,\n    critical: urlData.critical,\n    statusCode,\n    responseTime,\n    isBroken,\n    isSlow,\n    hasIssue: isBroken || (isSlow && urlData.critical),\n    statusIcon,\n    errorMessage: error?.message || '',\n    checkedAt: new Date().toISOString()\n  }\n}];"
      },
      "id": "node-evaluate",
      "name": "Code - Evaluate Response",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        920,
        400
      ]
    },
    {
      "parameters": {
        "conditions": {
          "conditions": [
            {
              "leftValue": "={{ $json.hasIssue }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "node-if-broken",
      "name": "IF - Has Issue?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1140,
        400
      ]
    },
    {
      "parameters": {
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "C_DEVOPS_CHANNEL",
          "mode": "id"
        },
        "messageType": "block",
        "blocksUi": {
          "blocksValues": [
            {
              "type": "header",
              "text": {
                "type": "plain_text",
                "text": "{{ $json.statusCode >= 500 ? '\ud83d\udea8 CRITICAL' : $json.statusCode === 0 ? '\ud83d\udc80 UNREACHABLE' : '\u26a0\ufe0f WARNING' }} \u2014 Link Monitor Alert"
              }
            },
            {
              "type": "section",
              "fields": [
                {
                  "type": "mrkdwn",
                  "text": "*{{ $json.statusIcon }} {{ $json.label }}*\n`{{ $json.url }}`"
                },
                {
                  "type": "mrkdwn",
                  "text": "*Status Code:*\n{{ $json.statusCode === 0 ? 'TIMEOUT/UNREACHABLE' : $json.statusCode }}"
                },
                {
                  "type": "mrkdwn",
                  "text": "*Response Time:*\n{{ $json.responseTime }}ms"
                },
                {
                  "type": "mrkdwn",
                  "text": "*Critical Link:*\n{{ $json.critical ? 'YES \u26a0\ufe0f' : 'No' }}"
                }
              ]
            },
            {
              "type": "section",
              "text": {
                "type": "mrkdwn",
                "text": "{{ $json.errorMessage ? '*Error:* ' + $json.errorMessage : '*Issue:* ' + ($json.isBroken ? 'HTTP ' + $json.statusCode + ' response' : 'Slow response (>' + $json.responseTime + 'ms)') }}"
              }
            },
            {
              "type": "context",
              "elements": [
                {
                  "type": "mrkdwn",
                  "text": "Detected at {{ $json.checkedAt }} \u2022 n8n Link Monitor"
                }
              ]
            }
          ]
        }
      },
      "id": "node-slack-alert",
      "name": "Slack - Send Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1360,
        280
      ],
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_MONITOR_SPREADSHEET_ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Monitor Log",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Timestamp": "={{ $json.checkedAt }}",
            "Label": "={{ $json.label }}",
            "URL": "={{ $json.url }}",
            "Status Code": "={{ $json.statusCode }}",
            "Response Time (ms)": "={{ $json.responseTime }}",
            "Is Broken": "={{ $json.isBroken ? 'YES' : 'NO' }}",
            "Is Slow": "={{ $json.isSlow ? 'YES' : 'NO' }}",
            "Critical": "={{ $json.critical ? 'YES' : 'NO' }}",
            "Error": "={{ $json.errorMessage }}"
          }
        },
        "options": {}
      },
      "id": "node-log",
      "name": "Google Sheets - Log Check Result",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [
        1360,
        520
      ],
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      }
    }
  ],
  "connections": {
    "Schedule - Every Hour": {
      "main": [
        [
          {
            "node": "Code - URL List to Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - URL List to Check": {
      "main": [
        [
          {
            "node": "HTTP Request - Check URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request - Check URL": {
      "main": [
        [
          {
            "node": "Code - Evaluate Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Code - Evaluate Response": {
      "main": [
        [
          {
            "node": "IF - Has Issue?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Has Issue?": {
      "main": [
        [
          {
            "node": "Slack - Send Alert",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Google Sheets - Log Check Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Slack - Send Alert": {
      "main": [
        [
          {
            "node": "Google Sheets - Log Check Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "monitoring"
    },
    {
      "name": "devops"
    },
    {
      "name": "alerts"
    }
  ],
  "meta": {
    "description": "Every hour, checks a configurable list of URLs via HTTP GET, evaluates status codes and response times, alerts Slack for broken/slow links, and logs every check result to Google Sheets for trending.",
    "prerequisites": [
      "Slack Bot Token with chat:write scope",
      "Google Sheets OAuth2 (optional, for logging)",
      "Update the URL list in Code node with your actual URLs",
      "Enable continueOnFail on HTTP node (already set) to handle timeouts gracefully"
    ],
    "testingScenario": {
      "happy_path": "All URLs return 200 \u2192 no Slack alert, all logged",
      "test_broken_link": "Add 'https://httpstat.us/404' to URL list \u2192 should trigger Slack alert",
      "test_timeout": "Add 'https://httpstat.us/200?sleep=15000' \u2192 triggers timeout/slow alert",
      "edge_cases": [
        "Redirect chains \u2192 followed up to 5 redirects",
        "SSL cert error \u2192 error message captured, shows as broken",
        "Rate limited (429) \u2192 shows as 429 warning, not critical",
        "Same URL alerting every hour \u2192 add dedup logic or Slack throttling"
      ]
    }
  }
}