AutomationFlowsContent & Video › Validate Website Requests & Send Email

Validate Website Requests & Send Email

Original n8n title: Startseiten-anfragecheck - Live V1

Startseiten-Anfragecheck - Live V1. Uses httpRequest, emailSend. Webhook trigger; 26 nodes.

Webhook trigger★★★★☆ complexity26 nodesHTTP RequestEmail Send
Content & Video Trigger: Webhook Nodes: 26 Complexity: ★★★★☆ Added:

This workflow follows the Emailsend → 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
{
  "name": "Startseiten-Anfragecheck - Live V1",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "audit",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -420,
        -360
      ],
      "id": "eec8d340-6935-4c25-8403-399424ed6a58",
      "name": "Webhook"
    },
    {
      "parameters": {
        "jsCode": "const req = $input.first().json || {};\nconst body = req.body || {};\nconst ui = body.__submission?.user_inputs || body;\nlet url = (ui.url || body.url || '').toString().trim();\nconst ip = (body.__submission?.ip || req.ip || req.headers?.['x-forwarded-for'] || '').toString();\nconst ua = (body.__submission?.browser || req.headers?.['user-agent'] || '').toString();\nconst errors = [];\n\nfunction normalizeUrl(input, required) {\n  let value = (input || '').toString().trim();\n  if (!value) {\n    if (required) errors.push('Bitte eine Seiten-URL angeben.');\n    return '';\n  }\n  if (!/^https?:\\/\\//i.test(value)) value = 'https://' + value;\n  try {\n    const u = new URL(value);\n    if (!/^https?:$/.test(u.protocol)) {\n      errors.push('Nur http/https URLs sind erlaubt.');\n      return '';\n    }\n    u.hash = '';\n    return u.toString().replace(/\\/+$/, '');\n  } catch (e) {\n    errors.push('URL ist ungueltig. Beispiel: https://example.de');\n    return '';\n  }\n}\n\nfunction isPrivateIPv4(host) {\n  const m = host.match(/^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$/);\n  if (!m) return false;\n  const a = m.slice(1).map((x) => parseInt(x, 10));\n  if (a.some((x) => Number.isNaN(x) || x < 0 || x > 255)) return true;\n  if (a[0] === 10) return true;\n  if (a[0] === 127) return true;\n  if (a[0] === 169 && a[1] === 254) return true;\n  if (a[0] === 192 && a[1] === 168) return true;\n  if (a[0] === 172 && a[1] >= 16 && a[1] <= 31) return true;\n  return false;\n}\n\nurl = normalizeUrl(url, true);\n\nif (errors.length === 0 && url) {\n  const host = new URL(url).hostname.toLowerCase();\n  const hostNoWww = host.replace(/^www\\./, '');\n  const denyHosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1', '169.254.169.254'];\n  const isIp = /^[0-9.]+$/.test(hostNoWww) || hostNoWww.includes(':');\n\n  if (denyHosts.includes(hostNoWww)) errors.push('Diese URL ist nicht erlaubt.');\n  if (isIp && isPrivateIPv4(hostNoWww)) errors.push('Private oder interne IPs sind nicht erlaubt.');\n  if (/\\.local$|\\.internal$|\\.lan$|\\.home$/.test(hostNoWww)) errors.push('Interne Domains sind nicht erlaubt.');\n\n  try {\n    const sd = $getWorkflowStaticData('global');\n    const now = Date.now();\n    const hour = Math.floor(now / (60 * 60 * 1000));\n    const key = 'rl:' + (ip || 'unknown') + ':' + hour;\n    sd[key] = (sd[key] || 0) + 1;\n    if (sd[key] > 30) errors.push('Zu viele Anfragen. Bitte in 60 Minuten erneut versuchen.');\n  } catch (e) {}\n}\n\nconst jobId = 'job_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);\nlet domain = null;\nif (url) {\n  try {\n    domain = new URL(url).hostname.replace(/^www\\./, '');\n  } catch (e) {}\n}\n\nreturn [{\n  json: {\n    ok: errors.length === 0,\n    error: errors.length ? errors.join(' ') : null,\n    url,\n    domain,\n    jobId,\n    ip,\n    ua,\n    ts: new Date().toISOString(),\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -180,
        -360
      ],
      "id": "b373da95-246e-4b5a-9875-7a454051638a",
      "name": "URL Validator"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.ok }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -20,
        -360
      ],
      "id": "9cd2f2b2-dab0-4012-aef4-5396eb5a18e2",
      "name": "IF Valid Request"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: false, status: $json.status || \"error\", error: $json.error || \"Ungueltige Anfrage\" } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        220,
        -220
      ],
      "id": "0837f051-6d95-4dd0-9cc9-ab0ff5ac8513",
      "name": "Respond Error"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, jobId: $json.jobId, status: \"processing\", message: \"Check gestartet. Die Seite wird jetzt gelesen und priorisiert. Das Ergebnis erscheint in ca. 20-40 Sekunden direkt hier.\", pollUrl: \"/webhook/audit-status?jobId=\" + $json.jobId } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        220,
        -500
      ],
      "id": "35455d39-85c5-4b12-a146-c30a60cacfae",
      "name": "Respond OK"
    },
    {
      "parameters": {
        "jsCode": "const sd = $getWorkflowStaticData('global');\nconst meta = $('URL Validator').first().json || {};\nconst now = Date.now();\nconst ttlMs = 2 * 60 * 60 * 1000;\n\nsd[meta.jobId] = JSON.stringify({\n  status: 'processing',\n  message: 'Check laeuft...',\n  request: {\n    url: meta.url,\n    domain: meta.domain || null,\n    ip: meta.ip || '',\n    ua: meta.ua || '',\n  },\n  storedAt: now,\n  updatedAt: now,\n  expiresAt: now + ttlMs,\n});\n\nconst hour = Math.floor(now / (60 * 60 * 1000));\nfor (const key of Object.keys(sd)) {\n  if (key.startsWith('job_')) {\n    try {\n      const entry = JSON.parse(sd[key]);\n      if (entry.expiresAt && entry.expiresAt < now) delete sd[key];\n    } catch (e) {\n      delete sd[key];\n    }\n    continue;\n  }\n\n  if (key.startsWith('rl:')) {\n    const keyHour = parseInt(key.split(':')[2] || '0', 10);\n    if (Number.isFinite(keyHour) && keyHour < hour - 1) delete sd[key];\n  }\n}\n\nreturn [{ json: meta }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        -500
      ],
      "id": "87771a69-54fd-493f-a895-02508ab7d0c3",
      "name": "Store Processing"
    },
    {
      "parameters": {
        "url": "=https://r.jina.ai/{{ $json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; AuditBot/1.0)"
            }
          ]
        },
        "options": {
          "allowUnauthorizedCerts": false,
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 20000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        100,
        -360
      ],
      "id": "bf5ac95a-39c3-4f11-b3da-6d1f5fc69218",
      "name": "Jina Reader",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "={{ $(\"URL Validator\").first().json.url }}",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; AuditBot/1.0)"
            },
            {
              "name": "Accept",
              "value": "text/html"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 15000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        420,
        -340
      ],
      "id": "4a4ff5b8-e4f5-4616-8d29-c41476b7974e",
      "name": "Raw HTML Fetch",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=performance&category=accessibility&category=best-practices",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $(\"URL Validator\").first().json.url }}"
            },
            {
              "name": "strategy",
              "value": "mobile"
            },
            {
              "name": "category",
              "value": "performance"
            },
            {
              "name": "category",
              "value": "accessibility"
            },
            {
              "name": "category",
              "value": "best-practices"
            },
            {
              "name": "locale",
              "value": "de_DE"
            },
            {
              "name": "key",
              "value": "={{ $vars.GOOGLE_PAGESPEED_API_KEY || $env.GOOGLE_PAGESPEED_API_KEY }}"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        620,
        -420
      ],
      "id": "d13d3b5d-0120-4af7-b165-c5b653239726",
      "name": "PageSpeed API",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function unwrap(x) {\n  if (!x) return {};\n  if (x.lighthouseResult || x.error) return x;\n  if (x.body && (x.body.lighthouseResult || x.body.error)) return x.body;\n  if (x.data && (x.data.lighthouseResult || x.data.error)) return x.data;\n  if (x.response && (x.response.lighthouseResult || x.response.error)) return x.response;\n  if (x.response?.body && (x.response.body.lighthouseResult || x.response.body.error)) return x.response.body;\n  return x;\n}\n\nconst p = unwrap($input.first().json || {});\nconst needRetry = (!p.lighthouseResult) || !!p.error;\nreturn [{ json: { needRetry } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        780,
        -420
      ],
      "id": "e6e90a7c-196f-4ed1-9ee8-e08fba671917",
      "name": "Check PageSpeed"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.needRetry }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        940,
        -420
      ],
      "id": "117094b2-f825-41d9-ac29-6eb1f2ac805c",
      "name": "IF PageSpeed Retry?"
    },
    {
      "parameters": {
        "amount": 12,
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1100,
        -240
      ],
      "id": "76928715-a72f-4c24-ab6c-9d653cfc220c",
      "name": "Wait 12s"
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=performance&category=accessibility&category=best-practices",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $(\"URL Validator\").first().json.url }}"
            },
            {
              "name": "strategy",
              "value": "mobile"
            },
            {
              "name": "category",
              "value": "performance"
            },
            {
              "name": "category",
              "value": "accessibility"
            },
            {
              "name": "category",
              "value": "best-practices"
            },
            {
              "name": "locale",
              "value": "de_DE"
            },
            {
              "name": "key",
              "value": "={{ $vars.GOOGLE_PAGESPEED_API_KEY || $env.GOOGLE_PAGESPEED_API_KEY }}"
            }
          ]
        },
        "options": {
          "timeout": 120000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1260,
        -240
      ],
      "id": "7454108e-fa98-40e4-a4f2-71fcbab9652a",
      "name": "PageSpeed API Retry",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "function unwrap(x) {\n  if (!x) return {};\n  if (x.lighthouseResult || x.error) return x;\n  if (x.body && (x.body.lighthouseResult || x.body.error)) return x.body;\n  if (x.data && (x.data.lighthouseResult || x.data.error)) return x.data;\n  if (x.response && (x.response.lighthouseResult || x.response.error)) return x.response;\n  if (x.response?.body && (x.response.body.lighthouseResult || x.response.body.error)) return x.response.body;\n  return x;\n}\n\nfunction perfScore(obj) {\n  const s = obj?.lighthouseResult?.categories?.performance?.score;\n  return typeof s === 'number' ? Math.round(s * 100) : 0;\n}\n\nfunction ok(obj) {\n  return !!obj?.lighthouseResult && !obj?.error;\n}\n\nlet p1 = {};\nlet p2 = {};\ntry { p1 = unwrap($('PageSpeed API').first().json); } catch (e) {}\ntry { p2 = unwrap($('PageSpeed API Retry').first().json); } catch (e) {}\n\nlet chosen = p1;\nif (ok(p2) && (!ok(p1) || perfScore(p2) >= perfScore(p1))) chosen = p2;\nchosen = chosen || {};\n\nchosen._auditMeta = chosen._auditMeta || {};\nchosen._auditMeta.pagespeed = {\n  chosen: ok(chosen) ? (chosen === p2 ? 'retry' : 'first') : 'none',\n  try1_ok: ok(p1),\n  try2_ok: ok(p2),\n  score_try1: perfScore(p1),\n  score_try2: perfScore(p2)\n};\n\nreturn [{ json: chosen }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1260,
        -420
      ],
      "id": "67c3ac61-098f-487c-8644-e0517b957822",
      "name": "PageSpeed Final"
    },
    {
      "parameters": {
        "jsCode": "const meta = $('URL Validator').first().json || {};\n\nfunction safeString(value) {\n  if (typeof value === 'string') return value;\n  if (value === null || value === undefined) return '';\n  try { return JSON.stringify(value); } catch (e) { return String(value); }\n}\n\nfunction toTextPayload(payload) {\n  if (!payload) return '';\n  if (typeof payload === 'string') return payload;\n  return safeString(payload.data || payload.body || payload.response?.body || payload.response || '');\n}\n\nfunction hasRequestError(payload) {\n  if (!payload || typeof payload !== 'object') return false;\n  if (payload.error) return true;\n  if (payload.statusCode && Number(payload.statusCode) >= 400) return true;\n  if (payload.status && Number(payload.status) >= 400) return true;\n  return false;\n}\n\nfunction stripHtml(html) {\n  return String(html || '')\n    .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n    .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n    .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n    .replace(/<svg[\\s\\S]*?<\\/svg>/gi, ' ')\n    .replace(/<[^>]+>/g, ' ');\n}\n\nfunction cleanText(text) {\n  return String(text || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction wordCount(text) {\n  return cleanText(text).split(/\\s+/).filter(Boolean).length;\n}\n\nfunction firstMatch(text, regex) {\n  const match = String(text || '').match(regex);\n  return match ? cleanText(match[1] || match[0]) : '';\n}\n\nfunction truncate(text, max) {\n  const value = cleanText(text);\n  if (value.length <= max) return value;\n  return value.slice(0, Math.max(0, max - 1)).trim() + '\u2026';\n}\n\nfunction boolLabel(value, positive, negative) {\n  return value ? positive : negative;\n}\n\nfunction boolStatus(value, negativeStatus) {\n  return value ? 'good' : (negativeStatus || 'bad');\n}\n\nlet html = '';\nlet rawHtmlOk = false;\nlet rawHtmlStatus = 'unknown';\ntry {\n  const rn = $('Raw HTML Fetch').first().json || {};\n  rawHtmlStatus = String(rn.statusCode || rn.status || 'ok');\n  html = toTextPayload(rn);\n  rawHtmlOk = !hasRequestError(rn) && html.length > 300;\n} catch (e) {\n  rawHtmlStatus = 'exception';\n}\n\nlet readerText = '';\nlet readerOk = false;\ntry {\n  const jn = $('Jina Reader').first().json || {};\n  readerText = cleanText(toTextPayload(jn));\n  readerOk = !hasRequestError(jn) && wordCount(readerText) > 80;\n} catch (e) {}\n\nconst strippedHtml = cleanText(stripHtml(html));\nconst combinedText = cleanText((readerOk ? readerText + ' ' : '') + strippedHtml);\nconst contentReliable = rawHtmlOk || readerOk || wordCount(strippedHtml) > 80;\nconst topText = truncate(combinedText.split(/\\s+/).slice(0, 140).join(' '), 600);\n\nconst headMatch = html.match(/<head[^>]*>([\\s\\S]*?)<\\/head>/i);\nconst head = headMatch ? headMatch[1] : html.slice(0, 8000);\nconst title = firstMatch(head, /<title[^>]*>([^<]{0,240})<\\/title>/i);\nconst metaDesc = firstMatch(head, /<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']{0,320})[\"']/i) || firstMatch(head, /<meta[^>]*content=[\"']([^\"']{0,320})[\"'][^>]*name=[\"']description[\"']/i);\nconst canonical = firstMatch(head, /<link[^>]*rel=[\"']canonical[\"'][^>]*href=[\"']([^\"']+)[\"']/i);\nconst h1Matches = [...html.matchAll(/<h1[^>]*>([\\s\\S]*?)<\\/h1>/gi)].map((match) => cleanText(stripHtml(match[1]))).filter(Boolean);\nconst h1 = h1Matches[0] || '';\nconst h1WordCount = wordCount(h1);\nconst titleWordCount = wordCount(title);\n\nconst htmlLinks = [...html.matchAll(/<a[^>]+href=[\"']([^\"']+)[\"']/gi)].map((match) => String(match[1] || '').trim()).filter(Boolean);\nconst bodyLower = combinedText.toLowerCase();\nconst firstScreenLower = topText.toLowerCase();\n\nconst hasOutcomeHint = /(anfragen|leads|kunden|termine|bewerber|umsatz|vertrauen|sichtbarkeit|wachstum|conversion|mehr|weniger|schneller|effizient)/i.test((h1 + ' ' + title + ' ' + topText));\nconst hasAudienceHint = /(fuer|fuer\b|b2b|unternehmen|teams|agenturen|dienstleister|kanzleien|aerzte|praxen|saas|shops|marken)/i.test((h1 + ' ' + title + ' ' + topText));\nconst hasSpecificityHint = /(\\b\\d+\\b|tage|wochen|monate|schritte|cases|kunden|projekten|projekten|projekte|minutes|minuten)/i.test((h1 + ' ' + title + ' ' + topText));\nconst titleMatchesH1 = !!(title && h1) && (title.toLowerCase().includes(h1.toLowerCase().slice(0, 20)) || h1.toLowerCase().includes(title.toLowerCase().slice(0, 20)));\n\nconst hasCases = /case.?stud|referenz|kundenprojekt|erfolgsgeschichte|portfolio|projektbeispiel/i.test(combinedText + ' ' + html);\nconst hasTestimonials = /testimonial|kundenstimm|bewertung|rezension|review|stimmen unserer kunden|das sagen kunden/i.test(combinedText + ' ' + html);\nconst hasStats = /\\b\\d+\\+?\\s*(kunden|projekte|jahre|bewertungen|reviews|mio|million|leads|anfragen|umsatz|downloads)\\b/i.test(combinedText);\nconst hasLogos = /logos|bekannt aus|vertrauen auf|arbeitet fuer|partner/i.test(combinedText + ' ' + html);\n\nconst hasForm = /<form[\\s>]/i.test(html) || /kontaktformular|anfrageformular|formular/i.test(combinedText);\nconst hasBookingLink = htmlLinks.some((link) => /cal\\.com|calendly|youcanbook|bookeo|book|buchen|termin|demo|strategiegespraech|beratungsgespraech/i.test(link)) || /termin buchen|demo buchen|gespraech buchen|beratungsgespraech|strategiegespraech/i.test(combinedText);\nconst hasEmail = /mailto:|[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/i.test(html + ' ' + combinedText);\nconst hasPhone = /(\\+?\\d{2,4}[\\s./-]?\\d{2,}[\\s./-]?\\d{2,})/.test(combinedText);\nconst hasCtaAboveFold = /(kontakt|anfrage|termin|demo|gespraech|beratung|starten|buchen|angebot|anrufen)/i.test(firstScreenLower);\nconst isHttps = String(meta.url || '').startsWith('https://');\n\nlet mobileScore = 0;\nlet lcp = 'n/a';\nlet lcpMs = 0;\nlet cls = 'n/a';\nlet clsVal = 0;\nlet tbt = 'n/a';\nlet tbtMs = 0;\nlet pagespeedOk = false;\ntry {\n  const gd = $('PageSpeed Final').first().json || {};\n  if (gd.lighthouseResult) {\n    const lh = gd.lighthouseResult;\n    const audits = lh.audits || {};\n    pagespeedOk = true;\n    mobileScore = typeof lh.categories?.performance?.score === 'number' ? Math.round(lh.categories.performance.score * 100) : 0;\n    lcp = audits['largest-contentful-paint']?.displayValue || 'n/a';\n    lcpMs = audits['largest-contentful-paint']?.numericValue || 0;\n    cls = audits['cumulative-layout-shift']?.displayValue || 'n/a';\n    clsVal = audits['cumulative-layout-shift']?.numericValue || 0;\n    tbt = audits['total-blocking-time']?.displayValue || 'n/a';\n    tbtMs = audits['total-blocking-time']?.numericValue || 0;\n  }\n} catch (e) {}\n\nlet promiseScore = 0;\nif (h1) promiseScore += 1;\nif (title) promiseScore += 1;\nif (metaDesc) promiseScore += 1;\nif (hasOutcomeHint) promiseScore += 1;\nif (hasAudienceHint) promiseScore += 1;\nif (hasSpecificityHint) promiseScore += 1;\nif (titleMatchesH1) promiseScore += 1;\nif (h1WordCount >= 4 && h1WordCount <= 16) promiseScore += 1;\n\nlet promiseStatus = 'warning';\nlet promiseValue = 'teilweise lesbar';\nlet promiseHelp = 'Die Seite war heute nicht vollstaendig auslesbar.';\nlet promiseSummary = 'Die Seite konnte nicht komplett gelesen werden. Aussagen zum Seitenversprechen bleiben deshalb vorsichtig.';\nif (contentReliable) {\n  if (promiseScore >= 6) {\n    promiseStatus = 'good';\n    promiseValue = 'scharf';\n    promiseHelp = 'H1, Title und Nutzenrichtung sind im ersten Eindruck gut erkennbar.';\n    promiseSummary = 'Das Seitenversprechen ist im ersten Eindruck greifbar. Besucher verstehen relativ schnell, worum es geht.';\n  } else if (promiseScore >= 3) {\n    promiseStatus = 'warning';\n    promiseValue = 'verbesserbar';\n    promiseHelp = 'Eine Richtung ist erkennbar, aber noch nicht spitz genug.';\n    promiseSummary = 'Die Seite sagt schon etwas, aber noch nicht klar genug fuer wen sie gedacht ist und warum man hier bleiben sollte.';\n  } else {\n    promiseStatus = 'bad';\n    promiseValue = 'zu diffus';\n    promiseHelp = 'Zu wenig Klarheit im ersten Eindruck.';\n    promiseSummary = 'Der erste Eindruck bleibt zu allgemein. Besucher muessen zu viel selbst zusammensetzen, bevor Vertrauen entstehen kann.';\n  }\n}\n\nconst proofScore = [hasCases, hasTestimonials, hasStats, hasLogos].filter(Boolean).length;\nlet proofStatus = 'warning';\nlet proofValue = 'teilweise lesbar';\nlet proofHelp = 'Proof-Signale sind heute nicht vollstaendig lesbar.';\nlet proofSummary = 'Die Seite konnte inhaltlich nur teilweise gelesen werden. Aussagen zu Referenzen und Beweis bleiben vorsichtig.';\nif (contentReliable) {\n  if (proofScore >= 3) {\n    proofStatus = 'good';\n    proofValue = 'stark';\n    proofHelp = 'Mehrere sichtbare Proof-Signale wurden erkannt.';\n    proofSummary = 'Die Seite liefert bereits sichtbaren Beweis. Das staerkt Vertrauen frueh genug im Besuch.';\n  } else if (proofScore >= 1) {\n    proofStatus = 'warning';\n    proofValue = 'duenn';\n    proofHelp = 'Einzelne Proof-Signale sind da, aber noch nicht dicht genug.';\n    proofSummary = 'Es gibt erste Vertrauenssignale, aber noch nicht genug Beweis genau dort, wo die Anfrage entschieden wird.';\n  } else {\n    proofStatus = 'bad';\n    proofValue = 'fehlt fast ganz';\n    proofHelp = 'Kaum sichtbarer Beweis fuer Vertrauen.';\n    proofSummary = 'Die Seite zeigt zu wenig Beweis dafuer, warum ein Besucher ausgerechnet hier anfragen sollte.';\n  }\n}\n\nlet nextStepStatus = 'warning';\nlet nextStepValue = 'teilweise lesbar';\nlet nextStepHelp = 'Der Conversion-Pfad ist heute nicht vollstaendig lesbar.';\nlet nextStepSummary = 'Die Seite konnte inhaltlich nur teilweise gelesen werden. Aussagen zum naechsten Schritt bleiben deshalb vorsichtig.';\nif (contentReliable) {\n  if ((hasForm || hasBookingLink) && hasCtaAboveFold) {\n    nextStepStatus = 'good';\n    nextStepValue = 'klar';\n    nextStepHelp = 'Es gibt einen sichtbaren und gut anschlussfaehigen naechsten Schritt.';\n    nextStepSummary = 'Ein Besucher findet einen klaren naechsten Schritt. Reibung entsteht hier nicht als Erstes.';\n  } else if (hasForm || hasBookingLink || hasEmail || hasPhone) {\n    nextStepStatus = 'warning';\n    nextStepValue = 'vorhanden';\n    nextStepHelp = 'Kontakt ist moeglich, aber noch nicht stark genug gefuehrt.';\n    nextStepSummary = 'Interesse kann entstehen, aber der naechste Schritt ist noch nicht klar oder sichtbar genug priorisiert.';\n  } else {\n    nextStepStatus = 'bad';\n    nextStepValue = 'zu weich';\n    nextStepHelp = 'Kein sauber gefuehrter naechster Schritt gefunden.';\n    nextStepSummary = 'Die Seite laesst Interesse versickern, bevor daraus eine Anfrage wird.';\n  }\n}\n\nlet mobileStatus = 'warning';\nlet mobileValue = 'nicht messbar';\nlet mobileHelp = 'PageSpeed konnte heute nicht belastbar geladen werden.';\nlet mobileSummary = 'Die technische Basis ist heute nur eingeschraenkt messbar.';\nif (pagespeedOk) {\n  if (mobileScore >= 85 && lcpMs > 0 && lcpMs < 2500) {\n    mobileStatus = 'good';\n    mobileValue = 'stark';\n    mobileHelp = 'Mobile Score ' + mobileScore + '/100 \u00b7 LCP ' + lcp;\n    mobileSummary = 'Der mobile Eindruck wirkt nicht wie der groesste Blocker.';\n  } else if (mobileScore >= 60) {\n    mobileStatus = 'warning';\n    mobileValue = 'okay';\n    mobileHelp = 'Mobile Score ' + mobileScore + '/100 \u00b7 LCP ' + lcp;\n    mobileSummary = 'Die Seite ist mobil brauchbar, aber noch nicht stark genug fuer kalten Traffic.';\n  } else {\n    mobileStatus = 'bad';\n    mobileValue = 'zu langsam';\n    mobileHelp = 'Mobile Score ' + mobileScore + '/100 \u00b7 LCP ' + lcp;\n    mobileSummary = 'Der mobile Eindruck bremst frueher, als er sollte. Noch bevor Vertrauen entsteht, ist bereits Reibung da.';\n  }\n}\n\nconst diagnostics = {\n  promise: {\n    title: 'Seitenversprechen',\n    status: promiseStatus,\n    valueLabel: promiseValue,\n    helpLabel: promiseHelp,\n    summary: promiseSummary,\n    evidence: [\n      h1 ? 'H1: ' + truncate(h1, 90) : 'Keine H1 gefunden.',\n      title ? 'Title vorhanden.' : 'Kein sauberer Title gefunden.',\n      metaDesc ? 'Meta Description vorhanden.' : 'Keine saubere Meta Description gefunden.'\n    ],\n    action: 'Headline, Subline und CTA auf einen klaren Nutzen fuer einen klaren Besuchertyp zuspitzen.'\n  },\n  proof: {\n    title: 'Proof',\n    status: proofStatus,\n    valueLabel: proofValue,\n    helpLabel: proofHelp,\n    summary: proofSummary,\n    evidence: [\n      'Cases: ' + boolLabel(hasCases, 'gefunden', 'nicht gefunden'),\n      'Testimonials: ' + boolLabel(hasTestimonials, 'gefunden', 'nicht gefunden'),\n      'Zahlen oder Resultate: ' + boolLabel(hasStats, 'gefunden', 'nicht gefunden')\n    ],\n    action: 'Proof nicht verteilen, sondern direkt in den ersten sichtbaren Kaufkontext der Seite ziehen.'\n  },\n  nextStep: {\n    title: 'Naechster Schritt',\n    status: nextStepStatus,\n    valueLabel: nextStepValue,\n    helpLabel: nextStepHelp,\n    summary: nextStepSummary,\n    evidence: [\n      'Formular: ' + boolLabel(hasForm, 'vorhanden', 'nicht gefunden'),\n      'Terminbuchung: ' + boolLabel(hasBookingLink, 'vorhanden', 'nicht gefunden'),\n      'CTA im ersten Screen: ' + boolLabel(hasCtaAboveFold, 'erkennbar', 'nicht klar erkennbar')\n    ],\n    action: 'Nur einen naechsten Schritt priorisieren und ihn oberhalb des Folds klar anschlussfaehig machen.'\n  },\n  mobile: {\n    title: 'Mobiler Eindruck',\n    status: mobileStatus,\n    valueLabel: mobileValue,\n    helpLabel: mobileHelp,\n    summary: mobileSummary,\n    evidence: [\n      'Mobile Score: ' + (pagespeedOk ? mobileScore + '/100' : 'nicht messbar'),\n      'Largest Contentful Paint: ' + lcp,\n      'Total Blocking Time: ' + tbt\n    ],\n    action: 'Above-the-fold-Bereich, Bildgewicht und Skriptlast vor kosmetischen Aenderungen priorisieren.'\n  }\n};\n\nreturn [{ json: {\n  url: meta.url,\n  domain: meta.domain,\n  metaData: {\n    title,\n    metaDesc,\n    canonical,\n    h1,\n    h1Count: h1Matches.length,\n    topText\n  },\n  pageFacts: {\n    isHttps,\n    hasH1: !!h1,\n    hasCases,\n    hasTestimonials,\n    hasStats,\n    hasLogos,\n    hasForm,\n    hasBookingLink,\n    hasEmail,\n    hasPhone,\n    hasCtaAboveFold,\n    hasAudienceHint,\n    hasOutcomeHint,\n    hasSpecificityHint,\n    titleMatchesH1\n  },\n  performance: {\n    mobileScore,\n    lcp,\n    lcpMs,\n    cls,\n    clsVal,\n    tbt,\n    tbtMs\n  },\n  sourceHealth: {\n    rawHtmlOk,\n    rawHtmlStatus,\n    rawHtmlLength: html.length,\n    readerOk,\n    readerWordCount: wordCount(readerText),\n    pagespeedOk,\n    contentReliable\n  },\n  diagnostics,\n  rawSnapshot: {\n    firstScreen: topText,\n    title: truncate(title, 140),\n    h1: truncate(h1, 140)\n  }\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1500,
        -420
      ],
      "id": "df5e0c99-a558-46b0-a8b4-6a4489ba7d4d",
      "name": "Analyze Page"
    },
    {
      "parameters": {
        "jsCode": "const d = $('Analyze Page').first().json || {};\nconst diagnostics = d.diagnostics || {};\nconst metaData = d.metaData || {};\nconst facts = d.pageFacts || {};\nconst perf = d.performance || {};\nconst sourceHealth = d.sourceHealth || {};\n\nfunction severity(status) {\n  if (status === 'bad') return 3;\n  if (status === 'warning') return 2;\n  return 1;\n}\n\nfunction toneLabel(status) {\n  if (status === 'bad') return 'Kritisch';\n  if (status === 'warning') return 'Verbesserbar';\n  return 'Stark';\n}\n\nfunction phrase(title) {\n  switch (title) {\n    case 'Seitenversprechen': return 'das Seitenversprechen';\n    case 'Proof': return 'der sichtbare Beweis';\n    case 'Naechster Schritt': return 'der naechste Schritt';\n    case 'Mobiler Eindruck': return 'der mobile Eindruck';\n    default: return 'dieser Hebel';\n  }\n}\n\nfunction rowStatus(value, negativeStatus) {\n  return value ? 'good' : (negativeStatus || 'bad');\n}\n\nfunction rowValue(value, positive, negative) {\n  return value ? positive : negative;\n}\n\nfunction truncate(text, max) {\n  const value = String(text || '').trim();\n  if (!value) return '';\n  if (value.length <= max) return value;\n  return value.slice(0, Math.max(0, max - 1)).trim() + '\u2026';\n}\n\nconst ordered = ['Seitenversprechen', 'Proof', 'Naechster Schritt', 'Mobiler Eindruck'];\nconst allFindings = [diagnostics.promise, diagnostics.proof, diagnostics.nextStep, diagnostics.mobile]\n  .filter(Boolean)\n  .map((item) => ({\n    title: item.title,\n    status: item.status || 'warning',\n    summary: item.summary || '',\n    evidence: Array.isArray(item.evidence) ? item.evidence.slice(0, 3) : [],\n    action: item.action || '',\n    valueLabel: item.valueLabel || '',\n    helpLabel: item.helpLabel || '',\n    severity: severity(item.status || 'warning')\n  }))\n  .sort((a, b) => {\n    const diff = b.severity - a.severity;\n    if (diff !== 0) return diff;\n    return ordered.indexOf(a.title) - ordered.indexOf(b.title);\n  });\n\nconst topFindings = allFindings.slice(0, 3).map((item, index) => ({\n  title: item.title,\n  status: item.status,\n  impact: 'Hebel #' + (index + 1),\n  toneLabel: toneLabel(item.status),\n  summary: item.summary,\n  evidence: item.evidence,\n  action: item.action,\n  valueLabel: item.valueLabel,\n  helpLabel: item.helpLabel\n}));\n\nconst primary = topFindings[0] || { title: 'Seitenversprechen', status: 'warning', summary: 'Die Seite braucht eine klarere Priorisierung.' };\nconst secondary = topFindings[1] || null;\n\nconst badCount = allFindings.filter((item) => item.status === 'bad').length;\nconst warningCount = allFindings.filter((item) => item.status === 'warning').length;\n\nlet verdictStatus = 'warning';\nlet verdictBadge = 'Priorisierung noetig';\nif (badCount >= 2) {\n  verdictStatus = 'bad';\n  verdictBadge = 'Akute Bremsen';\n} else if (badCount === 0 && warningCount <= 1) {\n  verdictStatus = 'good';\n  verdictBadge = 'Starke Basis';\n}\n\nlet verdictHeadline = 'Bei ' + (d.domain || 'dieser Seite') + ' bremst vor allem ' + phrase(primary.title) + '.';\nif (secondary) {\n  verdictHeadline = 'Bei ' + (d.domain || 'dieser Seite') + ' bremsen vor allem ' + phrase(primary.title) + ' und ' + phrase(secondary.title) + '.';\n}\n\nlet verdictSubline = primary.summary || 'Die Analyse zeigt, welche Hebel zuerst priorisiert werden sollten.';\nif (!sourceHealth.contentReliable && !sourceHealth.pagespeedOk) {\n  verdictStatus = 'warning';\n  verdictBadge = 'Teilweise lesbar';\n  verdictHeadline = 'Die Seite war heute nur teilweise technisch lesbar, aber die Haupthebel lassen sich bereits priorisieren.';\n  verdictSubline = 'Fuer die genaue Umsetzung sollte man die Seite einmal live durchgehen. Die Richtung der Hebel ist trotzdem klar genug.';\n}\n\nconst highlights = topFindings.map((item) => ({\n  label: item.title,\n  value: item.valueLabel || toneLabel(item.status),\n  help: item.helpLabel || item.summary,\n  tone: item.status\n}));\n\nconst storyParts = [];\nstoryParts.push(\n  verdictStatus === 'bad'\n    ? 'Die Seite ist nicht kaputt. Sie arbeitet nur noch nicht sauber als Anfrage-Seite. ' + phrase(primary.title) + ' und ' + (secondary ? phrase(secondary.title) : 'ein weiterer Kernhebel') + ' greifen noch nicht sauber ineinander.'\n    : verdictStatus === 'good'\n      ? 'Die Seite hat bereits eine brauchbare Basis. Der groesste Hebel liegt jetzt weniger im Komplettumbau als in sauberer Zuspitzung.'\n      : 'Die Seite hat Substanz, aber noch nicht genug Klarheit im ersten Eindruck und im Uebergang zur Anfrage.'\n);\n\nstoryParts.push(\n  primary.title === 'Seitenversprechen'\n    ? 'Wenn die erste sichtbare Botschaft nicht schnell genug sagt, fuer wen die Seite gedacht ist und was sie konkret loest, wird selbst guter Traffic teurer als noetig.'\n    : primary.title === 'Proof'\n      ? 'Aufmerksamkeit alleine reicht nicht. Ohne sichtbaren Beweis fehlt der Grund, genau hier und nicht beim naechsten Anbieter anzufragen.'\n      : primary.title === 'Naechster Schritt'\n        ? 'Interesse darf nicht offen im Raum stehen bleiben. Ohne klaren naechsten Schritt verliert die Seite Momentum in dem Moment, in dem der Besucher eigentlich handeln koennte.'\n        : 'Der mobile Eindruck entscheidet frueher als viele denken. Wenn die Seite dort Reibung aufbaut, entstehen Zweifel schon vor dem eigentlichen Argument.'\n);\n\nstoryParts.push(\n  'Der sinnvolle naechste Schritt ist jetzt kein weiterer Tool-Report, sondern eine kurze Priorisierung direkt an der Seite. Danach ist klar, was sofort Wirkung bringt, was warten kann und welche Aenderung zuerst Anfragen wahrscheinlicher macht.'\n);\n\nconst frontendData = {\n  meta: {\n    mode: 'startseiten_anfragecheck',\n    domain: d.domain || '',\n    url: d.url || '',\n    branche: '',\n    standort: '',\n    generatedAt: new Date().toISOString()\n  },\n  verdict: {\n    status: verdictStatus,\n    badge: verdictBadge,\n    headline: verdictHeadline,\n    subline: verdictSubline\n  },\n  highlights,\n  findings: topFindings,\n  details: {\n    sections: [\n      {\n        title: 'Versprechen',\n        rows: [\n          { label: 'H1', value: metaData.h1 ? truncate(metaData.h1, 90) : 'nicht gefunden', status: metaData.h1 ? 'good' : 'bad' },\n          { label: 'Title', value: metaData.title ? truncate(metaData.title, 90) : 'nicht gefunden', status: metaData.title ? 'good' : 'warning' },\n          { label: 'Meta Description', value: metaData.metaDesc ? 'vorhanden' : 'nicht gefunden', status: metaData.metaDesc ? 'good' : 'warning' },\n          { label: 'Klarheit im ersten Screen', value: diagnostics.promise?.valueLabel || 'unbekannt', status: diagnostics.promise?.status || 'warning' }\n        ],\n        note: 'Hier entscheidet sich, ob ein Besucher in wenigen Sekunden versteht, worum es geht und warum er bleiben sollte.'\n      },\n      {\n        title: 'Proof',\n        rows: [\n          { label: 'Cases / Referenzen', value: rowValue(facts.hasCases, 'gefunden', 'nicht gefunden'), status: rowStatus(facts.hasCases, 'bad') },\n          { label: 'Testimonials / Reviews', value: rowValue(facts.hasTestimonials, 'gefunden', 'nicht gefunden'), status: rowStatus(facts.hasTestimonials, 'bad') },\n          { label: 'Zahlen oder Resultate', value: rowValue(facts.hasStats, 'gefunden', 'nicht gefunden'), status: rowStatus(facts.hasStats, 'warning') },\n          { label: 'HTTPS', value: rowValue(facts.isHttps, 'aktiv', 'fehlt'), status: rowStatus(facts.isHttps, 'bad') }\n        ],\n        note: 'Proof muss nicht laut sein, aber frueh genug sichtbar werden, damit aus Aufmerksamkeit Vertrauen wird.'\n      },\n      {\n        title: 'Naechster Schritt',\n        rows: [\n          { label: 'Formular', value: rowValue(facts.hasForm, 'vorhanden', 'nicht gefunden'), status: rowStatus(facts.hasForm, 'bad') },\n          { label: 'Terminbuchung', value: rowValue(facts.hasBookingLink, 'vorhanden', 'nicht gefunden'), status: rowStatus(facts.hasBookingLink, 'warning') },\n          { label: 'E-Mail oder direkter Kontakt', value: (facts.hasEmail || facts.hasPhone) ? 'vorhanden' : 'nicht gefunden', status: (facts.hasEmail || facts.hasPhone) ? 'good' : 'warning' },\n          { label: 'CTA im ersten Screen', value: rowValue(facts.hasCtaAboveFold, 'erkennbar', 'nicht klar erkennbar'), status: rowStatus(facts.hasCtaAboveFold, 'warning') }\n        ],\n        note: 'Interesse braucht einen klaren Anschluss. Sonst wird aus Besuch kein Gespraech.'\n      },\n      {\n        title: 'Mobiler Eindruck',\n        rows: [\n          { label: 'Mobile Score', value: sourceHealth.pagespeedOk ? String(perf.mobileScore || 0) + '/100' : 'nicht messbar', status: sourceHealth.pagespeedOk ? ((perf.mobileScore || 0) >= 85 ? 'good' : (perf.mobileScore || 0) >= 60 ? 'warning' : 'bad') : 'warning' },\n          { label: 'Largest Contentful Paint', value: perf.lcp || 'n/a', status: sourceHealth.pagespeedOk ? ((perf.lcpMs || 0) > 0 && (perf.lcpMs || 0) < 2500 ? 'good' : (perf.lcpMs || 0) < 4000 ? 'warning' : 'bad') : 'warning' },\n          { label: 'Cumulative Layout Shift', value: perf.cls || 'n/a', status: sourceHealth.pagespeedOk ? ((perf.clsVal || 0) > 0 && (perf.clsVal || 0) < 0.1 ? 'good' : 'warning') : 'warning' },\n          { label: 'Total Blocking Time', value: perf.tbt || 'n/a', status: sourceHealth.pagespeedOk ? ((perf.tbtMs || 0) < 250 ? 'good' : (perf.tbtMs || 0) < 600 ? 'warning' : 'bad') : 'warning' }\n        ],\n        note: 'Technik verkauft nicht allein. Sie entscheidet aber, wie sauber die Seite ihren ersten Eindruck ueberhaupt ausspielen kann.'\n      }\n    ]\n  },\n  story: storyParts.join('\\\\n\\\\n'),\n  cta: {\n    headline: 'Wenn Sie aus dieser Diagnose eine klare Reihenfolge machen wollen, ist jetzt der richtige Schritt das Gespraech.',\n    subline: 'Wir priorisieren gemeinsam, was auf dieser Seite zuerst Wirkung bringt und was bewusst warten kann.',\n    primary: {\n      label: 'Analyse gemeinsam priorisieren',\n      sublabel: '20 Minuten \u00b7 klare Reihenfolge \u00b7 keine Verkaufsshow'\n    },\n    secondary: {\n      url: 'https://cal.com/hasim/30min',\n      label: 'Direkt Strategiegespraech buchen'\n    }\n  }\n};\n\nreturn [{ json: { frontendData, domain: d.domain || '' } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1740,
        -420
      ],
      "id": "652a521a-74c0-4f1a-97dc-283405ded0bb",
      "name": "Build Frontend Data"
    },
    {
      "parameters": {
        "jsCode": "const sd = $getWorkflowStaticData('global');\nconst meta = $('URL Validator').first().json || {};\nconst now = Date.now();\nconst ttlMs = 2 * 60 * 60 * 1000;\nconst frontendData = $json.frontendData || {};\n\nlet existing = {};\ntry {\n  existing = JSON.parse(sd[meta.jobId] || '{}');\n} catch (e) {}\n\nsd[meta.jobId] = JSON.stringify({\n  status: 'done',\n  data: frontendData,\n  request: {\n    url: meta.url,\n    domain: meta.domain || frontendData?.meta?.domain || null,\n    ip: meta.ip || '',\n    ua: meta.ua || '',\n  },\n  storedAt: existing.storedAt || now,\n  updatedAt: now,\n  finishedAt: now,\n  expiresAt: now + ttlMs,\n});\n\nconst hour = Math.floor(now / (60 * 60 * 1000));\nfor (const key of Object.keys(sd)) {\n  if (key.startsWith('job_')) {\n    try {\n      const entry = JSON.parse(sd[key]);\n      if (entry.expiresAt && entry.expiresAt < now) delete sd[key];\n    } catch (e) {\n      delete sd[key];\n    }\n    continue;\n  }\n\n  if (key.startsWith('rl:')) {\n    const keyHour = parseInt(key.split(':')[2] || '0', 10);\n    if (Number.isFinite(keyHour) && keyHour < hour - 1) delete sd[key];\n  }\n}\n\nreturn [{ json: { stored: true, jobId: meta.jobId, domain: frontendData?.meta?.domain || meta.domain || null, frontendData } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        -420
      ],
      "id": "a5b95a96-655c-4e1d-9f1a-8a4429594e24",
      "name": "Store Results"
    },
    {
      "parameters": {
        "path": "audit-status",
        "responseMode": "responseNode",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "GET, OPTIONS"
              },
              {
                "name": "Cache-Control",
                "value": "no-cache"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -420,
        80
      ],
      "id": "5f09e9d5-f3d5-4169-bd2b-ef3fef918181",
      "name": "Webhook Status"
    },
    {
      "parameters": {
        "jsCode": "const sd = $getWorkflowStaticData('global');\nconst req = $('Webhook Status').first().json || {};\nconst jobId = req.query?.jobId || req.params?.jobId || '';\n\nif (!jobId || !jobId.startsWith('job_')) {\n  return [{ json: { ok: false, status: 'error', error: 'Ungueltige Job-ID.' } }];\n}\n\nconst raw = sd[jobId];\nif (!raw) {\n  return [{ json: { ok: true, status: 'processing', jobId, message: 'Check laeuft noch...' } }];\n}\n\nlet entry;\ntry {\n  entry = JSON.parse(raw);\n} catch (e) {\n  return [{ json: { ok: false, status: 'error', jobId, error: 'Fehler beim Laden.' } }];\n}\n\nif (entry.expiresAt && entry.expiresAt < Date.now()) {\n  delete sd[jobId];\n  return [{ json: { ok: false, status: 'expired', jobId, error: 'Ergebnis abgelaufen.' } }];\n}\n\nif (entry.status === 'done') {\n  return [{ json: { ok: true, status: 'done', jobId, data: entry.data } }];\n}\n\nif (entry.status === 'error') {\n  return [{ json: { ok: false, status: 'error', jobId, error: entry.error || 'Check fehlgeschlagen.' } }];\n}\n\nreturn [{ json: { ok: true, status: entry.status || 'processing', jobId, message: entry.message || 'Check laeuft noch...' } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -180,
        80
      ],
      "id": "d07efb43-7200-4364-8220-a62774703461",
      "name": "Lookup Results"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        60,
        80
      ],
      "id": "4571cb37-d47c-4c9a-ad96-89c553b0cb61",
      "name": "Respond Status"
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "audit-consultation",
        "responseMode": "responseNode",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              },
              {
                "name": "Access-Control-Allow-Methods",
                "value": "POST, OPTIONS"
              },
              {
                "name": "Cache-Control",
                "value": "no-cache"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -420,
        240
      ],
      "id": "2dae7916-8d63-4ce8-8246-4aa95258a999",
      "name": "Webhook Consultation"
    },
    {
      "parameters": {
        "jsCode": "const req = $input.first().json || {};\nconst body = req.body || {};\nconst ui = body.__submission?.user_inputs || body;\nconst errors = [];\n\nconst name = (ui.name || body.name || '').toString().trim();\nconst email = (ui.email || body.email || '').toString().trim();\nconst company = (ui.company || body.company || '').toString().trim();\nconst message = (ui.message || body.message || '').toString().trim();\nconst jobId = (ui.jobId || body.jobId || '').toString().trim();\nconst url = (ui.url || body.url || '').toString().trim();\nconst domain = (ui.domain || body.domain || '').toString().trim();\nconst primaryFinding = (ui.primaryFinding || body.primaryFinding || '').toString().trim();\nconst ip = (body.__submission?.ip || req.ip || req.headers?.['x-forwarded-for'] || '').toString();\nconst ua = (body.__submission?.browser || req.headers?.['user-agent'] || '').toString();\n\nif (!name) errors.push('Bitte Ihren Namen angeben.');\nif (!email || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) errors.push('Bitte eine gueltige E-Mail-Adresse angeben.');\n\nconst subject = '[Check] Analyse priorisieren - ' + (company || domain || name);\nconst lines = [\n  'Neue Anfrage aus dem Startseiten-Check.',\n  '',\n  'Name: ' + name,\n  'E-Mail: ' + email,\n  company ? 'Unternehmen: ' + company : null,\n  domain ? 'Domain: ' + domain : null,\n  url ? 'URL: ' + url : null,\n  jobId ? 'Job ID: ' + jobId : null,\n  primaryFinding ? 'Primaerer Hebel: ' + primaryFinding : null,\n  '',\n  'Nachricht:',\n  message || '(keine Zusatznachricht)'\n].filter(Boolean);\n\nconst text = lines.join('\\n');\nconst html = [\n  '<div style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;line-height:1.6;color:#0f172a;\">',\n  '<h2 style=\"margin:0 0 16px;\">Neue Anfrage aus dem Startseiten-Check</h2>',\n  '<p><strong>Name:</strong> ' + name + '</p>',\n  '<p><strong>E-Mail:</strong> ' + email + '</p>',\n  company ? '<p><strong>Unternehmen:</strong> ' + company + '</p>' : '',\n  domain ? '<p><strong>Domain:</strong> ' + domain + '</p>' : '',\n  url ? '<p><strong>URL:</strong> ' + url + '</p>' : '',\n  jobId ? '<p><strong>Job ID:</strong> ' + jobId + '</p>' : '',\n  primaryFinding ? '<p><strong>Primaerer Hebel:</strong> ' + primaryFinding + '</p>' : '',\n  '<hr style=\"border:none;border-top:1px solid #e2e8f0;margin:20px 0;\">',\n  '<p><strong>Nachricht:</strong></p>',\n  '<p>' + (message || '(keine Zusatznachricht)').replace(/\\n/g, '<br>') + '</p>',\n  '</div>'\n].join('');\n\nreturn [{ json: {\n  ok: errors.length === 0,\n  error: errors.length ? errors.join(' ') : null,\n  name,\n  email,\n  company,\n  message,\n  jobId,\n  url,\n  domain,\n  primaryFinding,\n  ip,\n  ua,\n  subject,\n  text,\n  html,\n  submittedAt: new Date().toISOString(),\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -180,
        240
      ],
      "id": "be2d1c4d-006e-4d39-abd9-07c344e9c76d",
      "name": "Consultation Validator"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.ok }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        -20,
        240
      ],
      "id": "1b6da0f8-13f6-4694-b501-5f0537eae43e",
      "name": "IF Consultation Valid"
    },
    {
      "parameters": {
        "fromEmail": "performance@hasimuener.de",
        "toEmail": "info@hasimuener.de",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.html }}"
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        220,
        120
      ],
      "id": "1dcfed10-02ef-4ea5-9258-2d4bfef75f85",
      "name": "Send Consultation Notification",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, status: \"accepted\", message: \"Anfrage ist raus. Wenn es schneller gehen soll, koennen Sie direkt einen Strategiecall buchen.\" } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        460,
        120
      ],
      "id": "f2c52ae4-53f8-4b19-956b-ade10c2b0ce3",
      "name": "Respond Consultation OK"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: false, status: \"error\", error: $json.error || \"Ungueltige Beratungsanfrage.\" } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        220,
        300
      ],
      "id": "0fff6d81-59d6-44ef-8011-8a729ec4fa60",
      "name": "Respond Consultation Error"
    }
  ],
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "URL Validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "URL Validator": {
      "main": [
        [
          {
            "node": "IF Valid Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Valid Request": {
      "main": [
        [
          {
            "node": "Respond OK",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Respond OK": {
      "main": [
        [
          {
            "node": "Store Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Processing": {
      "main": [
        [
          {
            "node": "Jina Reader",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Jina Reader": {
      "main": [
        [
          {
            "node": "Raw HTML Fetch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Raw HTML Fetch": {
      "main": [
        [
          {
            "node": "PageSpeed API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PageSpeed API": {
      "main": [
        [
          {
            "node": "Check PageSpeed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check PageSpeed": {
      "main": [
        [
          {
            "node": "IF PageSpeed Retry?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF PageSpeed Retry?": {
      "main": [
        [
          {
            "node": "Wait 12s",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "PageSpeed Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 12s": {
      "main": [
        [
          {
            "node": "PageSpeed API Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PageSpeed API Retry": {
      "main": [
        [
          {
            "node": "PageSpeed Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PageSpeed Final": {
      "main": [
        [
          {
            "node": "Analyze Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Page": {
      "main": [
        [
          {
            "node": "Build Frontend Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Frontend Data": {
      "main": [
        [
          {
            "node": "Store Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Status": {
      "main": [
        [
          {
            "node": "Lookup Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup Results": {
      "main": [
        [
          {
            "node": "Respond Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook Consultation": {
      "main": [
        [
          {
            "node": "Consultation Validator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Consultation Validator": {
      "main": [
        [
          {
            "node": "IF Consultation Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Consultation Valid": {
      "main": [
        [
          {
            "node": "Send Consultation Notification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Consultation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Consultation Notification": {
      "main": [
        [
          {
            "node": "Respond Consultation OK",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {},
  "meta": {
    "templateCredsSetupCompleted": true
  }
}

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

How this works

This workflow automates the validation and processing of incoming website requests to ensure they meet quality standards before further handling, saving time for content teams by filtering out invalid submissions instantly. It is ideal for web publishers or SEO specialists managing high volumes of URL suggestions for homepage updates. The key step involves fetching and analysing the target webpage's raw HTML via an HTTP request to Jina Reader, followed by checks that trigger an email notification only for approved requests.

Use this workflow when you receive frequent user-submitted URLs needing quick viability checks, such as for news sites curating external links. Avoid it for simple form validations without web content analysis, or if your volume is too low to justify automation. Common variations include adding AI nodes for semantic relevance scoring or integrating with a database to log processed requests over time.

About this workflow

Startseiten-Anfragecheck - Live V1. Uses httpRequest, emailSend. Webhook trigger; 26 nodes.

Source: https://github.com/Hasim-Uner/meine-wordpress-site-2fe6f514/blob/9995a0e87dbbac8e05b211a48bdb95e7bd12a838/automations/n8n/workflows/audit-funnel__instant-results__live-import.json — original creator credit. Request a take-down →

More Content & Video workflows → · Browse all categories →

Related workflows

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

Content & Video

Audit Funnel - Customer Journey Audit Runner V2. Uses httpRequest, openAi, emailSend. Webhook trigger; 38 nodes.

HTTP Request, OpenAI, Email Send
Content & Video

Digital-Product-Expert-Bundle-Courses. Uses openai, googleDrive, httpRequest, twitter. Webhook trigger; 10 nodes.

OpenAI, Google Drive, HTTP Request +5
Content & Video

Flux Ai Image Generator. Uses respondToWebhook, stickyNote, s3, formTrigger. Webhook trigger; 19 nodes.

S3, Form Trigger, HTTP Request
Content & Video

This workflow runs two parallel flows that together create a fully hands-off SEO content pipeline, from topic selection to Google indexing.

HTTP Request
Content & Video

Optimize your WordPress titles and meta descriptions with AI (OpenAI), update them directly in Yoast SEO, log results in Google Sheets, and receive a styled report by email. All from your own n8n inst

HTTP Request, OpenAI, Stop And Error +2