{
  "name": "Audit Funnel - Customer Journey Audit Runner V2",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "audit",
        "responseMode": "responseNode",
        "options": {}
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2.1,
      "position": [
        -384,
        -400
      ],
      "id": "fcba7c74-bb4c-474b-8d6d-64dac8287b64",
      "name": "Webhook"
    },
    {
      "parameters": {
        "jsCode": "const req = $input.first().json || {};\nconst body = req.body || {};\nconst ui = body.__submission?.user_inputs || body;\nconst stepRaw = (ui.step || body.step || 'start').toString().trim().toLowerCase();\nconst mode = stepRaw === 'email_capture' ? 'email_capture' : 'start';\n\nlet url = (ui.url || body.url || '').toString().trim();\nlet email = (ui.email || body.email || '').toString().trim();\nconst requestedJobId = (ui.jobId || body.jobId || '').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 Website-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 ung\u00fcltig. 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\nif (mode === 'email_capture') {\n  if (!requestedJobId || !requestedJobId.startsWith('job_')) {\n    errors.push('Ung\u00fcltige Job-ID.');\n  }\n  if (!email || !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n    errors.push('Bitte eine g\u00fcltige E-Mail-Adresse angeben.');\n  }\n  if (url) {\n    url = normalizeUrl(url, false);\n  }\n} else {\n  url = normalizeUrl(url, true);\n  if (email && !/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n    errors.push('Bitte eine g\u00fcltige E-Mail-Adresse angeben.');\n  }\n\n  if (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/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}\n\nconst jobId = mode === 'email_capture'\n  ? requestedJobId\n  : 'job_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10);\n\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    mode,\n    step: stepRaw,\n    error: errors.length ? errors.join(' ') : null,\n    url,\n    email,\n    domain,\n    jobId,\n    ip,\n    ua,\n    ts: new Date().toISOString(),\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -144,
        -400
      ],
      "id": "df87a1c4-f39e-47af-8f09-47e282ff2c5c",
      "name": "URL Validator"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.ok }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        0,
        -400
      ],
      "id": "41387c66-4043-4615-ba48-1863b45e16eb",
      "name": "IF Valid Request"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: false, status: $json.status || \"error\", error: $json.error || \"Ung\u00fcltige Anfrage\" } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        240,
        -240
      ],
      "id": "d4b584b9-31a2-4b23-ab6b-3e5b1df634af",
      "name": "Respond Error"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.mode === \"email_capture\" }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        224,
        -400
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00001",
      "name": "IF Email Capture?"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, jobId: $json.jobId, status: \"processing\", message: \"Audit gestartet. Ergebnisse werden in ca. 30-60 Sekunden angezeigt.\", pollUrl: \"/webhook/audit-status?jobId=\" + $json.jobId } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        240,
        -480
      ],
      "id": "f7e33837-3218-4257-90e0-1d6215fdc3f0",
      "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: 'Audit l\u00e4uft...',\n  request: {\n    url: meta.url,\n    email: meta.email || '',\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": [
        432,
        -520
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00002",
      "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": [
        96,
        -400
      ],
      "id": "8f5f5c07-772d-4501-9665-767d68801f06",
      "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": [
        416,
        -336
      ],
      "id": "e28a40f8-5414-4ec7-84d5-29d85b249a22",
      "name": "Raw HTML Fetch",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=performance&category=seo&category=best-practices&category=accessibility",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $('URL Validator').first().json.url }}"
            },
            {
              "name": "strategy",
              "value": "mobile"
            },
            {
              "name": "category",
              "value": "performance"
            },
            {
              "name": "category",
              "value": "seo"
            },
            {
              "name": "category",
              "value": "best-practices"
            },
            {
              "name": "category",
              "value": "accessibility"
            },
            {
              "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": [
        576,
        -400
      ],
      "id": "17506824-228a-468d-b13f-b77bc476fd1d",
      "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 } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        736,
        -400
      ],
      "id": "2e62e945-5f81-4cde-a27b-8d0131f8696e",
      "name": "Check PageSpeed"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.needRetry }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        896,
        -400
      ],
      "id": "6ccda909-ccbb-442d-8875-c14ace21402f",
      "name": "IF PageSpeed Retry?"
    },
    {
      "parameters": {
        "amount": 12,
        "unit": "seconds"
      },
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1056,
        -240
      ],
      "id": "86a13e86-facd-4bef-b124-8f4f52711c31",
      "name": "Wait 12s"
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=performance&category=seo&category=best-practices&category=accessibility",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "url",
              "value": "={{ $('URL Validator').first().json.url }}"
            },
            {
              "name": "strategy",
              "value": "mobile"
            },
            {
              "name": "category",
              "value": "performance"
            },
            {
              "name": "category",
              "value": "seo"
            },
            {
              "name": "category",
              "value": "best-practices"
            },
            {
              "name": "category",
              "value": "accessibility"
            },
            {
              "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": [
        1216,
        -240
      ],
      "id": "f8913a0e-1b12-457a-8f44-0cee91e8aba4",
      "name": "PageSpeed API Retry",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// PageSpeed Final \u2014 pick best response (try1 vs retry) + unwrap common wrappers\n\nfunction 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 }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1216,
        -400
      ],
      "id": "8cf5f2c2-6214-4c0a-b914-1a1fc51df501",
      "name": "PageSpeed Final"
    },
    {
      "parameters": {
        "url": "={{ $('URL Validator').first().json.url }}/sitemap_index.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1456,
        -400
      ],
      "id": "8605b5ad-5cac-4b29-af97-41a9fd71a9b0",
      "name": "Sitemap Fetch",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "url": "={{ $('URL Validator').first().json.url }}/sitemap.xml",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          },
          "timeout": 10000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        1616,
        -240
      ],
      "id": "8d197ff5-f570-4c59-8053-2dc39028481f",
      "name": "Sitemap Fetch Fallback",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "let a = '';\nlet b = '';\ntry { a = $('Sitemap Fetch').first().json?.data || $('Sitemap Fetch').first().json?.body || ''; } catch(e) {}\ntry { b = $('Sitemap Fetch Fallback').first().json?.data || $('Sitemap Fetch Fallback').first().json?.body || ''; } catch(e) {}\n\na = (typeof a === 'string') ? a : JSON.stringify(a||'');\nb = (typeof b === 'string') ? b : JSON.stringify(b||'');\n\nconst hasLocA = /<loc>[^<]+<\\/loc>/i.test(a);\nconst hasLocB = /<loc>[^<]+<\\/loc>/i.test(b);\n\nlet chosen = hasLocA ? a : (hasLocB ? b : a || b || '');\n\nreturn [{ json: { body: chosen, meta: { ok: !!chosen, used: hasLocA ? 'index' : (hasLocB ? 'fallback' : 'none') } } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1776,
        -400
      ],
      "id": "ff68f5b1-c164-4859-9131-6a02fa52c3a6",
      "name": "Sitemap Final"
    },
    {
      "parameters": {
        "jsCode": "const meta = $('URL Validator').first().json;\n\n// \u2550\u2550\u2550 PAGESPEED DIAGNOSE \u2550\u2550\u2550\nlet mobileScore = 0;\nlet scores = { performance: 0, seo: 0, bestPractices: 0, accessibility: 0, pwa: 0 };\nlet metrics = { LCP: 'n/a', LCP_ms: 0, CLS: 'n/a', CLS_val: 0, TBT: 'n/a', TBT_ms: 0, FCP: 'n/a', TTFB: 'n/a' };\nlet debugMsg = \"OK\";\n\ntry {\n  // Wichtig: Wir holen die Daten von FINAL (Retry-Logik)\n  const gd = $('PageSpeed Final').first().json;\n  \n  if (gd && gd.lighthouseResult) {\n    const lh = gd.lighthouseResult, au = lh.audits || {};\n    mobileScore = Math.round((lh.categories?.performance?.score || 0) * 100);\n    const cats = lh.categories || {};\n    const to100 = (v) => (typeof v === 'number' ? Math.round(v * 100) : 0);\n    scores = {\n      performance: to100(cats?.performance?.score),\n      seo: to100(cats?.seo?.score),\n      bestPractices: to100(cats?.['best-practices']?.score),\n      accessibility: to100(cats?.accessibility?.score),\n      pwa: to100(cats?.pwa?.score),\n    };\n    metrics = {\n      LCP: au['largest-contentful-paint']?.displayValue ?? 'n/a',\n      LCP_ms: au['largest-contentful-paint']?.numericValue || 0,\n      CLS: au['cumulative-layout-shift']?.displayValue ?? 'n/a',\n      CLS_val: au['cumulative-layout-shift']?.numericValue || 0,\n      TBT: au['total-blocking-time']?.displayValue ?? 'n/a',\n      TBT_ms: au['total-blocking-time']?.numericValue || 0,\n      FCP: au['first-contentful-paint']?.displayValue ?? 'n/a',\n      TTFB: au['server-response-time']?.displayValue ?? 'n/a'\n    };\n  } else {\n    if (gd.error) debugMsg = `Google Fehler: ${gd.error.message} (Code: ${gd.error.code})`;\n    else debugMsg = \"Google hat leeres Ergebnis geliefert.\";\n  }\n} catch(e) {\n  debugMsg = \"Script Fehler: \" + e.message;\n}\n\n// \u2550\u2550\u2550 HTML \u2550\u2550\u2550\nlet html = '', head = '';\ntry {\n  const rn = $('Raw HTML Fetch').first().json;\n  const raw = rn?.data || rn?.body || (typeof rn === 'string' ? rn : '') || '';\n  html = typeof raw === 'string' ? raw : JSON.stringify(raw);\n  const hm = html.match(/<head[^>]*>([\\s\\S]*?)<\\/head>/i);\n  head = hm ? hm[1] : html.substring(0, 8000);\n} catch(e) {}\n\nconst titleM = head.match(/<title[^>]*>([^<]{0,200})<\\/title>/i);\nconst title = titleM ? titleM[1].trim() : null;\nconst descM = head.match(/<meta[^>]*name=[\"']description[\"'][^>]*content=[\"']([^\"']{0,300})[\"']/i) || head.match(/<meta[^>]*content=[\"']([^\"']{0,300})[\"'][^>]*name=[\"']description[\"']/i);\nconst metaDesc = descM ? descM[1].trim() : null;\nconst canonical = (head.match(/<link[^>]*rel=[\"']canonical[\"'][^>]*href=[\"']([^\"']+)[\"']/i) || [])[1] || null;\nconst hasOg = (/<meta[^>]*property=[\"']og:(title|description|image)[\"']/gi.test(html));\nconst schemaBlocks = [...html.matchAll(/<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi)];\nconst schemaTypes = []; schemaBlocks.forEach(b => { try { const p = JSON.parse(b[1]); if (p['@type']) schemaTypes.push(p['@type']); if (Array.isArray(p['@graph'])) p['@graph'].forEach(i => { if (i['@type']) schemaTypes.push(i['@type']); }); } catch(e) {} });\nconst h1Count = (html.match(/<h1[^>]*>[\\s\\S]*?<\\/h1>/gi) || []).length;\n\n// Tracking\nconst hasGa4 = !!html.match(/G-[A-Z0-9]{6,12}/) || /gtag\\s*\\(/i.test(html);\nconst gtmM = html.match(/GTM-[A-Z0-9]{4,10}/);\nconst hasGtm = !!gtmM;\nconst gtmId = gtmM ? gtmM[0] : null;\nconst sgtmH = html.match(/https?:\\/\\/[a-z0-9.-]+\\.[a-z]+\\/gtm\\.js/i);\nconst hasSgtm = sgtmH ? !sgtmH[0].includes('googletagmanager.com') : false;\nconst consentTools = [];\nif (/borlabs/i.test(html)) consentTools.push('Borlabs');\nif (/cookiebot/i.test(html)) consentTools.push('Cookiebot');\nif (/complianz/i.test(html)) consentTools.push('Complianz');\nif (/usercentrics/i.test(html)) consentTools.push('Usercentrics');\nif (/real-cookie-banner/i.test(html)) consentTools.push('Real Cookie Banner');\nconst hasConsent = consentTools.length > 0;\nconst paidPixels = [];\nif (/fbq\\(|connect\\.facebook\\.net/i.test(html)) paidPixels.push('Meta');\nif (/AW-[0-9]+|googleads/i.test(html)) paidPixels.push('Google Ads');\nif (/linkedin\\.com\\/insight/i.test(html)) paidPixels.push('LinkedIn');\n\n// Security\nconst isWP = /wp-content|wp-includes|wp-json/i.test(html);\nconst wpVer = (html.match(/<meta[^>]*name=[\"']generator[\"'][^>]*content=[\"']WordPress\\s*([\\d.]+)[\"']/i) || [])[1] || null;\nconst isHttps = meta.url.startsWith('https');\n\n// Sitemap\nlet sitemapUrls = 0, blogUrls = 0, hasSitemap = false;\ntry {\n  const smR = $('Sitemap Final').first().json?.data || $('Sitemap Final').first().json?.body || '';\n  const sm = typeof smR === 'string' ? smR : '';\n  if (/<sitemapindex/i.test(sm)) {\n    const subs = [...sm.matchAll(/<loc>([^<]+)<\\/loc>/g)].map(m => m[1]);\n    hasSitemap = subs.length > 0; sitemapUrls = subs.length * 10;\n    blogUrls = subs.filter(u => /post/i.test(u)).length > 0 ? 5 : 0;\n  } else {\n    const urls = [...sm.matchAll(/<loc>([^<]+)<\\/loc>/g)].map(m => m[1]);\n    hasSitemap = urls.length > 0; sitemapUrls = urls.length;\n    blogUrls = urls.filter(u => /blog|artikel|beitrag|post|ratgeber/i.test(u)).length;\n  }\n} catch(e) {}\nif (!hasSitemap) {\n  if (/<link[^>]*rel=[\"']sitemap[\"']/i.test(html) || /sitemap_index\\.xml|sitemap\\.xml|wp-sitemap/i.test(html)) { hasSitemap = true; sitemapUrls = 10; }\n  else if (isWP) { hasSitemap = true; sitemapUrls = 5; }\n}\n\n// Jina Text\nlet jinaText = '';\ntry { const jn = $('Jina Reader').first().json; jinaText = (jn?.data || jn?.body || ''); } catch(e) {}\nconst jLow = jinaText.toLowerCase();\nconst wordCount = jinaText.split(/\\s+/).length;\nconst hasImpressum = /impressum/i.test(jinaText);\nconst hasDatenschutz = /datenschutz|privacy/i.test(jinaText);\n\n// Content signals\nconst hasBlog = blogUrls > 0 || /blog/i.test(jinaText) || /blog|artikel|magazin|ratgeber/i.test(html);\nconst hasCases = /case.?stud|referenz|fallstud|projekt|erfolgsgeschichte|portfolio|kundenprojekt/i.test(jLow) || /case|referenz|portfolio/i.test(html);\nconst hasTestimonials = /testimonial|kundenstimm|bewertung|rezension|\\d+\\s*sterne|google.*review|trustpilot|ausgezeichnet|empfehl|zufried|feedback/i.test(jLow);\nconst hasLeadMagnet = /lead.?magnet|download|whitepaper|e-?book|checkliste|gratis|kostenlos.*(guide|download|pdf|audit|analyse|check)|freebie|newsletter|kostenloses?.*(gespr|berat|analys|check|call)|jetzt.*anfrag|jetzt.*starten/i.test(jLow);\nconst hasContactForm = /kontakt|contact|formular|anfrage|nachricht.*senden/i.test(jLow);\nconst hasPhone = /\\+?\\d{2,4}[\\s.-]?\\d{3,}[\\s.-]?\\d{3,}/g.test(jinaText);\n\nreturn [{ json: {\n  url: meta.url, email: meta.email, domain: meta.domain,\n  speed: { mobileScore, metrics, scores },\n  debug_error: debugMsg,\n  seo: { title, metaDesc, canonical, hasOg, schemaTypes, h1Count, hasSitemap, sitemapUrls },\n  tracking: { hasGa4, hasGtm, gtmId, hasSgtm, hasConsent, consentTools, paidPixels },\n  security: { isWP, wpVer, isHttps, hasImpressum, hasDatenschutz },\n  content: { hasBlog, blogUrls, hasCases, hasTestimonials, hasLeadMagnet, hasContactForm, hasPhone, wordCount },\n  jinaText: jinaText.substring(0, 3000)\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2016,
        -400
      ],
      "id": "3314c901-cf84-4357-9bd4-994c92d685ab",
      "name": "Analyze Site"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.2",
          "mode": "id"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "Du bist ein SEO-Experte der Keywords mit kommerziellem Intent identifiziert.\n\nAnalysiere die Website und generiere 6 Suchbegriffe die ein ZAHLENDER Kunde bei Google eingeben wuerde \u2014 nicht jemand der sich informieren will, sondern jemand der KAUFEN oder BEAUFTRAGEN will.\n\nRegeln:\n1. Erkenne: Branche, Kerndienstleistungen, Stadt/Region\n2. NUR deutsche Keywords\n3. Mix: 2x [Dienstleistung + Stadt] (z.B. \"webdesign agentur hannover\"), 2x [Dienstleistung + Kaufintent] (z.B. \"website erstellen lassen kosten\"), 2x [Problem mit Loesungsintent] (z.B. \"wordpress seite zu langsam was tun\")\n4. Suchvolumen: Schaetze KONSERVATIV und realistisch. Lokale Keywords (mit Stadt): 100-600. Allgemeine: 300-2000. Nischen-Longtail: 50-300. NICHT uebertreiben.\n5. Kundenwert: Schaetze den durchschnittlichen Auftragswert fuer EINEN Neukunden in dieser Branche. Webdesign: 3000-8000. Beratung: 1500-5000. E-Commerce: 5000-15000.\n\nAntworte NUR mit JSON \u2014 kein anderer Text, keine Backticks:\n{\"branche\": \"...\", \"standort\": \"...\", \"kundenwert_eur\": 3000, \"keywords\": [{\"q\": \"webdesign agentur hannover\", \"vol\": 400}, ...]}"
            },
            {
              "content": "=Website: {{ $json.domain }}\nDienstleistungen und Inhalte:\n{{ $json.jinaText }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        2256,
        -400
      ],
      "id": "5b05e05a-42ed-4099-ad38-2d11dcf9855c",
      "name": "Keyword Generator",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Read GPT output\nconst j = $('Keyword Generator').first().json;\nlet raw = '';\nif (j?.output?.[0]?.content?.[0]?.text) raw = j.output[0].content[0].text;\nelse if (j?.content?.[0]?.text) raw = j.content[0].text;\nelse if (typeof j?.message?.content === 'string') raw = j.message.content;\nelse if (typeof j?.text === 'string') raw = j.text;\nelse raw = JSON.stringify(j);\n\n// Clean and parse JSON\nraw = raw.replace(/```json|```/g, '').trim();\nlet parsed;\ntry { parsed = JSON.parse(raw); } catch(e) {\n  // Fallback: extract JSON from text\n  const m = raw.match(/\\{[\\s\\S]*\\}/);\n  if (m) parsed = JSON.parse(m[0]);\n  else parsed = { branche: 'Unbekannt', standort: '', kundenwert_eur: 3000, keywords: [{ q: 'dienstleistung', vol: 300 }] };\n}\n\nconst site = $('Analyze Site').first().json;\nconst kws = parsed.keywords || [];\n\n// Output one item per keyword for SERP check\nconst items = kws.slice(0, 6).map(kw => ({\n  json: {\n    keyword: kw.q,\n    volume: kw.vol || 300,\n    domain: site.domain,\n    branche: parsed.branche || '',\n    standort: parsed.standort || '',\n    kundenwert: parsed.kundenwert_eur || 3000\n  }\n}));\n\nreturn items;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2496,
        -400
      ],
      "id": "45c2b18b-5e0d-44dd-b4f3-6c7a5f6242be",
      "name": "Extract Keywords"
    },
    {
      "parameters": {
        "url": "https://www.googleapis.com/customsearch/v1",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "key",
              "value": "={{ $vars.GOOGLE_CSE_KEY || $env.GOOGLE_CSE_KEY }}"
            },
            {
              "name": "cx",
              "value": "={{ $vars.GOOGLE_CSE_CX || $env.GOOGLE_CSE_CX || '0063ebe7a8a39425d' }}"
            },
            {
              "name": "q",
              "value": "={{ $json.keyword }}"
            },
            {
              "name": "gl",
              "value": "de"
            },
            {
              "name": "hl",
              "value": "de"
            },
            {
              "name": "num",
              "value": "10"
            }
          ]
        },
        "options": {
          "timeout": 15000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        2736,
        -400
      ],
      "id": "34288a61-aa48-4497-8383-a7cfdf560afd",
      "name": "SERP Check",
      "alwaysOutputData": true,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Collect all SERP results and site data\nconst serpItems = $input.all();\nconst site = $('Analyze Site').first().json;\nconst kwItems = $('Extract Keywords').all();\n\nconst kwData = kwItems[0]?.json || {};\nconst branche = kwData.branche || 'Dienstleister';\nconst standort = kwData.standort || '';\nconst kundenwert = kwData.kundenwert || 3000;\n\n// Process SERP results\nconst serpResults = [];\nlet totalLostTraffic = 0;\n\nfor (let i = 0; i < serpItems.length; i++) {\n  const serp = serpItems[i].json;\n  const kw = kwItems[i]?.json || {};\n  const keyword = kw.keyword || 'unbekannt';\n  const volume = kw.volume || 300;\n  const domain = kw.domain || site.domain;\n  \n  // Check if domain appears in results\n  const items = serp?.items || [];\n  let position = -1;\n  let competitors = [];\n  \n  for (let r = 0; r < items.length; r++) {\n    const link = (items[r]?.link || '').toLowerCase();\n    const displayLink = (items[r]?.displayLink || '').toLowerCase();\n    if (link.includes(domain) || displayLink.includes(domain)) {\n      position = r + 1;\n    } else if (competitors.length < 3) {\n      competitors.push({ name: items[r]?.displayLink || '', title: items[r]?.title || '' });\n    }\n  }\n  \n  // Estimate lost traffic if not ranking\n  let lostTraffic = 0;\n  if (position === -1) {\n    lostTraffic = Math.round(volume * 0.03); // ~3% CTR for page 1 avg\n  } else if (position > 3) {\n    lostTraffic = Math.round(volume * 0.015); // could get more traffic if higher\n  }\n  totalLostTraffic += lostTraffic;\n  \n  serpResults.push({\n    keyword,\n    volume,\n    position, // -1 = not found\n    lostTraffic,\n    competitors: competitors.slice(0, 2),\n    status: position === -1 ? 'not_found' : position <= 3 ? 'top3' : position <= 10 ? 'page1' : 'low'\n  });\n}\n\n// Revenue Gap\nconst conversionRate = 0.02;\nconst lostLeadsMonth = Math.round(totalLostTraffic * conversionRate * 10) / 10;\nconst lostRevenueMonth = Math.round(lostLeadsMonth * kundenwert);\nconst lostRevenueYear = lostRevenueMonth * 12;\n\n// Journey Steps\nconst s = site;\nconst steps = [];\n\n// Step 1: Google Search\nconst notRanking = serpResults.filter(r => r.status === 'not_found');\nconst ranking = serpResults.filter(r => r.status !== 'not_found');\nsteps.push({\n  nr: 1,\n  title: 'Ihr Kunde sucht bei Google',\n  icon: '\\uD83D\\uDD0D',\n  results: serpResults,\n  summary: notRanking.length + ' von ' + serpResults.length + ' Keywords: nicht auf Seite 1',\n  status: notRanking.length === 0 ? 'good' : notRanking.length <= 2 ? 'warning' : 'bad'\n});\n\n// Step 2: Website Visit\nconst loadTime = s.speed.metrics.LCP || 'n/a';\nconst loadMs = s.speed.metrics.LCP_ms || 0;\nsteps.push({\n  nr: 2,\n  title: 'Ein Besucher landet auf Ihrer Seite',\n  icon: '\\u26A1',\n  details: [\n    { label: 'Ladezeit (LCP)', value: loadTime, status: loadMs < 2500 ? 'good' : loadMs < 4000 ? 'warning' : 'bad' },\n    { label: 'Mobile Score', value: s.speed.mobileScore + '/100', status: s.speed.mobileScore >= 90 ? 'good' : s.speed.mobileScore >= 50 ? 'warning' : 'bad' },\n    { label: 'SEO Score', value: (s.speed.scores?.seo ?? 0) + '/100', status: (s.speed.scores?.seo ?? 0) >= 90 ? 'good' : (s.speed.scores?.seo ?? 0) >= 70 ? 'warning' : 'bad' },\n    { label: 'Best Practices', value: (s.speed.scores?.bestPractices ?? 0) + '/100', status: (s.speed.scores?.bestPractices ?? 0) >= 90 ? 'good' : (s.speed.scores?.bestPractices ?? 0) >= 70 ? 'warning' : 'bad' },\n    { label: 'Accessibility', value: (s.speed.scores?.accessibility ?? 0) + '/100', status: (s.speed.scores?.accessibility ?? 0) >= 90 ? 'good' : (s.speed.scores?.accessibility ?? 0) >= 70 ? 'warning' : 'bad' },\n    { label: 'Layout-Stabilit\u00e4t (CLS)', value: s.speed.metrics.CLS, status: s.speed.metrics.CLS_val < 0.1 ? 'good' : 'warning' }\n  ],\n  summary: s.speed.mobileScore >= 90 ? 'Schneller als 90% aller Websites. Der Besucher bleibt.' : s.speed.mobileScore >= 50 ? 'Akzeptabel, aber Verbesserungspotenzial.' : 'Zu langsam. Besucher springen ab.',\n  status: s.speed.mobileScore >= 90 ? 'good' : s.speed.mobileScore >= 50 ? 'warning' : 'bad'\n});\n\n// Step 3: Trust\nsteps.push({\n  nr: 3,\n  title: 'Der Besucher sucht einen Grund zu vertrauen',\n  icon: '\\uD83E\\uDD1D',\n  details: [\n    { label: 'Referenzen / Cases', value: s.content.hasCases ? 'Vorhanden' : 'Nicht gefunden', status: s.content.hasCases ? 'good' : 'bad' },\n    { label: 'Kundenstimmen / Testimonials', value: s.content.hasTestimonials ? 'Vorhanden' : 'Nicht gefunden', status: s.content.hasTestimonials ? 'good' : 'bad' },\n    { label: 'SSL / HTTPS', value: s.security.isHttps ? 'Aktiv' : 'Fehlt', status: s.security.isHttps ? 'good' : 'bad' },\n    { label: 'Impressum', value: s.security.hasImpressum ? 'Vorhanden' : 'Nicht gefunden', status: s.security.hasImpressum ? 'good' : 'bad' }\n  ],\n  summary: (s.content.hasCases && s.content.hasTestimonials) ? 'Starker Social Proof. Der Besucher fasst Vertrauen.' : s.content.hasCases ? 'Cases vorhanden, aber keine echten Kundenstimmen. Vertrauen bleibt unvollst\u00e4ndig.' : 'Kein Social Proof. Der Besucher vergleicht und w\u00e4hlt den Anbieter mit Beweisen.',\n  status: (s.content.hasCases && s.content.hasTestimonials) ? 'good' : s.content.hasCases || s.content.hasTestimonials ? 'warning' : 'bad'\n});\n\n// Step 4: Contact/Lead\nsteps.push({\n  nr: 4,\n  title: 'Der Besucher will den n\u00e4chsten Schritt machen',\n  icon: '\\uD83C\\uDFAF',\n  details: [\n    { label: 'Kontaktformular', value: s.content.hasContactForm ? 'Vorhanden' : 'Nicht gefunden', status: s.content.hasContactForm ? 'good' : 'bad' },\n    { label: 'Telefonnummer sichtbar', value: s.content.hasPhone ? 'Ja' : 'Nicht gefunden', status: s.content.hasPhone ? 'good' : 'warning' },\n    { label: 'Lead-Magnet (Guide, Checkliste)', value: s.content.hasLeadMagnet ? 'Vorhanden' : 'Nicht gefunden', status: s.content.hasLeadMagnet ? 'good' : 'bad' },\n    { label: 'Blog / Content Hub', value: s.content.hasBlog ? 'Vorhanden' : 'Nicht gefunden', status: s.content.hasBlog ? 'good' : 'bad' }\n  ],\n  summary: s.content.hasLeadMagnet ? 'Gute Lead-Capture Optionen vorhanden.' : 'Kein Lead-Magnet. 97% der Erstbesucher kommen nie wieder \u2014 ohne Opt-in gehen sie f\u00fcr immer verloren.',\n  status: s.content.hasLeadMagnet ? 'good' : s.content.hasContactForm ? 'warning' : 'bad'\n});\n\n// Build prompt for Story Writer\nlet serpSummary = '';\nserpResults.forEach(r => {\n  const icon = r.status === 'not_found' ? 'NICHT GEFUNDEN' : r.status === 'top3' ? 'TOP 3' : 'Position ' + r.position;\n  serpSummary += '- \"' + r.keyword + '\" (~' + r.volume + '/Monat): ' + icon;\n  if (r.competitors.length > 0 && r.status === 'not_found') serpSummary += ' | Stattdessen: ' + r.competitors.map(c => c.name).join(', ');\n  serpSummary += '\\n';\n});\n\nconst storyPrompt = 'Website: ' + s.domain + ' | Branche: ' + branche + ' | Standort: ' + standort + '\\n\\nGOOGLE-SICHTBARKEIT:\\n' + serpSummary + '\\nPERFORMANCE: Mobile ' + s.speed.mobileScore + '/100, LCP ' + s.speed.metrics.LCP + '\\nTRUST: Cases=' + (s.content.hasCases ? 'Ja' : 'Nein') + ', Testimonials=' + (s.content.hasTestimonials ? 'Ja' : 'Nein') + '\\nLEAD-CAPTURE: LeadMagnet=' + (s.content.hasLeadMagnet ? 'Ja' : 'Nein') + ', Blog=' + (s.content.hasBlog ? 'Ja' : 'Nein') + ', Kontaktformular=' + (s.content.hasContactForm ? 'Ja' : 'Nein') + '\\nTRACKING: GTM=' + (s.tracking.hasGtm ? s.tracking.gtmId : 'Nein') + ', Consent=' + s.tracking.consentTools.join('+') + ', Paid=' + (s.tracking.paidPixels.length > 0 ? s.tracking.paidPixels.join('+') : 'keine') + '\\n\\nREVENUE GAP: ~' + totalLostTraffic + ' verlorene Besucher/Monat, ~' + lostLeadsMonth + ' verlorene Leads/Monat, ~' + lostRevenueMonth + ' EUR/Monat (~' + lostRevenueYear + ' EUR/Jahr)';\n\nreturn [{ json: {\n  story_prompt: storyPrompt,\n  steps, serpResults,\n  revenue: { totalLostTraffic, lostLeadsMonth, lostRevenueMonth, lostRevenueYear, kundenwert, conversionRate: '2%' },\n  branche, standort, kundenwert,\n  url: s.url, email: s.email, domain: s.domain,\n  site: s\n} }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2976,
        -400
      ],
      "id": "97e0867b-94e5-41ab-a525-526f6c89c159",
      "name": "Build Journey"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5.2",
          "mode": "id"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "Du schreibst wie ein Berater der 500 EUR pro Stunde nimmt. Kein Wort ist umsonst. Jeder Satz erzeugt entweder Erkenntnis oder Handlungsdruck.\n\nDu bekommst Daten aus einer Customer-Journey-Simulation. Der Empfaenger hat die Journey-Schritte bereits gesehen. Dein Text ergaenzt \u2014 NICHT wiederholen.\n\nSchreibe GENAU 3 Absaetze (MAXIMAL 180 Woerter total):\n\nABSATZ 1 \u2014 DAS MUSTER (3 Saetze max):\nVerbinde die Schwaechen zu EINEM klaren Bild. Benutze eine praegnante Metapher die haengenbleibt. Nicht beschreiben was fehlt \u2014 sondern was PASSIERT weil es fehlt. Schlecht: \"Ihnen fehlt Content und Testimonials.\" Gut: \"Ihre Website ist ein Schaufenster in einer Seitenstrasse \u2014 schoen dekoriert, aber niemand laeuft vorbei.\"\n\nABSATZ 2 \u2014 WAS DAS KOSTET (2-3 Saetze):\nNimm die Revenue-Gap-Zahl und mach sie greifbar. Nicht \"9.000 EUR pro Monat entgehen Ihnen\" sondern: \"9.000 EUR pro Monat \u2014 das ist ein Vollzeit-Mitarbeiter, der jeden Monat Kunden bringt, den Sie aber nie eingestellt haben.\" Oder: \"In 12 Monaten sind das 108.000 EUR. Genug fuer 3 Entwickler oder 36 Monate Werbebudget.\"\n\nABSATZ 3 \u2014 DER NAECHSTE SCHRITT (2 Saetze):\nEine spezifische Frage die nur mit echten Daten beantwortet werden kann. Nicht generisch. Spezifisch fuer DIESE Website, DIESE Branche, DIESEN Standort.\n\nVERBOTEN: Woerter wie \"grundsaetzlich\", \"durchaus\", \"zweifellos\", \"erheblich\", \"signifikant\", \"massgeblich\". Keine Relativierungen. Keine Konjunktive. Keine Aufzaehlungen. Kein Markdown. Max 180 Woerter."
            },
            {
              "content": "={{ $json.story_prompt }}"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.1,
      "position": [
        3216,
        -400
      ],
      "id": "a4a23e8f-e591-475c-8c35-3c00bdc4b287",
      "name": "Write Story",
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Build clean JSON payload for WordPress frontend display\nconst d = $('Build Journey').first().json;\nconst site = d.site;\nconst steps = d.steps;\nconst serp = d.serpResults;\nconst rev = d.revenue;\n\n// Read story\nconst sj = $('Write Story').first().json;\nlet story = '';\nif (sj?.output?.[0]?.content?.[0]?.text) story = sj.output[0].content[0].text;\nelse if (sj?.content?.[0]?.text) story = sj.content[0].text;\nelse if (typeof sj?.message?.content === 'string') story = sj.message.content;\nelse if (typeof sj?.text === 'string') story = sj.text;\nelse story = '';\nstory = story.replace(/\\*\\*/g, '').replace(/#+\\s/g, '');\n\n// Build frontend-optimized JSON\nconst frontendData = {\n  meta: {\n    domain: d.domain,\n    url: d.url,\n    branche: d.branche,\n    standort: d.standort,\n    generatedAt: new Date().toISOString()\n  },\n  scores: {\n    mobile: site.speed.mobileScore,\n    seo: {\n      hasTitle: !!site.seo.title,\n      hasMetaDesc: !!site.seo.metaDesc,\n      hasCanonical: !!site.seo.canonical,\n      hasOg: site.seo.hasOg,\n      hasSitemap: site.seo.hasSitemap,\n      h1Count: site.seo.h1Count,\n      schemaTypes: site.seo.schemaTypes\n    },\n    tracking: {\n      hasGa4: site.tracking.hasGa4,\n      hasGtm: site.tracking.hasGtm,\n      gtmId: site.tracking.gtmId,\n      hasSgtm: site.tracking.hasSgtm,\n      hasConsent: site.tracking.hasConsent,\n      consentTools: site.tracking.consentTools,\n      paidPixels: site.tracking.paidPixels\n    }\n  },\n  performance: {\n    mobileScore: site.speed.mobileScore,\n    lcp: site.speed.metrics.LCP,\n    lcpMs: site.speed.metrics.LCP_ms,\n    cls: site.speed.metrics.CLS,\n    clsVal: site.speed.metrics.CLS_val,\n    tbt: site.speed.metrics.TBT,\n    tbtMs: site.speed.metrics.TBT_ms,\n    fcp: site.speed.metrics.FCP,\n    ttfb: site.speed.metrics.TTFB\n  },\n  journeySteps: steps,\n  serpResults: serp,\n  revenue: rev,\n  story: story,\n  cta: {\n    url: '#deep-dive-section',\n    label: '360\u00b0 Deep-Dive starten',\n    sublabel: '5 gezielte Fragen \u00b7 Kein Sales-Call \u00b7 Pers\u00f6nliche Analyse in 48h',\n    altUrl: 'https://cal.com/hasim/30min',\n    altLabel: 'Oder direkt: Strategiecall buchen \u2192'\n  }\n};\n\nreturn [{ json: { frontendData, email: d.email, domain: d.domain } }];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3696,
        -400
      ],
      "id": "d218c76f-60ca-4b4d-a9df-4de2a0e0743c",
      "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    email: meta.email || '',\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 [{\n  json: {\n    stored: true,\n    jobId: meta.jobId,\n    email: ($json.email || '').toString(),\n    domain: $json.domain || frontendData?.meta?.domain || meta.domain || null,\n    frontendData,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        3936,
        -400
      ],
      "id": "ec3dfdd9-bdba-4654-a62f-81a03f13e177",
      "name": "Store Results"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ !!($json.email && $json.email.toString().trim()) }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        4176,
        -520
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00003",
      "name": "IF Auto Email Requested"
    },
    {
      "parameters": {
        "jsCode": "return [{\n  json: {\n    mode: 'auto_email',\n    jobId: $json.jobId,\n    email: ($json.email || '').toString().trim(),\n    frontendData: $json.frontendData || {},\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4416,
        -600
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00004",
      "name": "Prepare Auto Email Request"
    },
    {
      "parameters": {
        "jsCode": "const payload = $json.frontendData || {};\nconst meta = payload.meta || {};\nconst steps = Array.isArray(payload.journeySteps) ? payload.journeySteps : [];\nconst serp = Array.isArray(payload.serpResults) ? payload.serpResults : [];\nconst rev = Object.assign({\n  totalLostTraffic: 0,\n  conversionRate: '2%',\n  kundenwert: 0,\n  lostRevenueMonth: 0,\n  lostRevenueYear: 0,\n}, payload.revenue || {});\nconst story = (payload.story || '').toString().trim();\nconst email = ($json.email || '').toString().trim();\nconst mode = ($json.mode || 'auto_email').toString();\nconst siteLabel = meta.domain || meta.url || 'Ihre Website';\nconst fileBase = (meta.domain || 'report').replace(/[^a-zA-Z0-9-]/g, '-');\nconst firstGap = serp.find((item) => item.status === 'not_found') || serp[0] || null;\nconst subjectHook = firstGap ? '\u201e' + firstGap.keyword + '\u201c \u2014 Ihre Kunden suchen, aber finden Sie nicht' : 'Customer Journey Audit';\n\nfunction esc(value) {\n  return String(value ?? '')\n    .replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;')\n    .replace(/\"/g, '&quot;');\n}\n\nfunction money(value) {\n  const num = Number(value || 0);\n  return num.toLocaleString('de-DE');\n}\n\nlet html = '';\nhtml += '<div style=\"font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#f1f5f9;padding:20px;margin:0;\">';\nhtml += '<div style=\"max-width:680px;margin:0 auto;background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 10px 30px rgba(15,23,42,0.08);\">';\nhtml += '<div style=\"background:#0f172a;padding:40px 32px;text-align:center;\">';\nhtml += '<div style=\"font-size:11px;letter-spacing:3px;text-transform:uppercase;color:#ffb020;font-weight:800;margin-bottom:8px;\">Customer Journey Audit</div>';\nhtml += '<h1 style=\"font-size:24px;line-height:1.25;color:#fff;margin:0 0 8px;\">Wir haben die Reise Ihres n\u00e4chsten Kunden simuliert.</h1>';\nhtml += '<p style=\"font-size:14px;color:#cbd5e1;margin:0;\">Website: <strong style=\"color:#fff;\">' + esc(siteLabel) + '</strong></p>';\nhtml += '</div>';\n\nhtml += '<div style=\"padding:28px 32px 8px;\">';\nhtml += '<h2 style=\"font-size:18px;color:#0f172a;margin:0 0 12px;\">Kernaussagen</h2>';\nhtml += '<div style=\"display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;\">';\nhtml += '<div style=\"background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px;\">';\nhtml += '<div style=\"font-size:12px;color:#64748b;\">Potenzial pro Monat</div>';\nhtml += '<div style=\"font-size:22px;font-weight:800;color:#ef4444;\">~' + money(rev.lostRevenueMonth) + ' \u20ac</div>';\nhtml += '</div>';\nhtml += '<div style=\"background:#f8fafc;border:1px solid #e2e8f0;border-radius:10px;padding:14px;\">';\nhtml += '<div style=\"font-size:12px;color:#64748b;\">Potenzial pro Jahr</div>';\nhtml += '<div style=\"font-size:22px;font-weight:800;color:#ef4444;\">~' + money(rev.lostRevenueYear) + ' \u20ac</div>';\nhtml += '</div>';\nhtml += '</div>';\nhtml += '</div>';\n\nhtml += '<div style=\"padding:8px 32px 24px;\">';\nfor (const step of steps) {\n  const status = step.status || 'warning';\n  const color = status === 'good' ? '#16a34a' : status === 'bad' ? '#dc2626' : '#d97706';\n  const bg = status === 'good' ? '#f0fdf4' : status === 'bad' ? '#fef2f2' : '#fffbeb';\n  html += '<div style=\"border:1px solid #e2e8f0;border-radius:10px;padding:16px 18px;margin-bottom:14px;\">';\n  html += '<div style=\"font-size:15px;font-weight:700;color:#0f172a;margin-bottom:10px;\">' + esc(step.title || 'Schritt') + '</div>';\n  if (Array.isArray(step.results)) {\n    for (const result of step.results.slice(0, 6)) {\n      html += '<div style=\"display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:13px;\">';\n      html += '<span style=\"color:#334155;\">' + esc(result.keyword || '') + '</span>';\n      html += '<span style=\"color:' + color + ';font-weight:700;\">' + esc(result.status === 'not_found' ? 'Nicht auf Seite 1' : result.status === 'top3' ? 'Top 3' : 'Position ' + result.position) + '</span>';\n      html += '</div>';\n    }\n  }\n  if (Array.isArray(step.details)) {\n    for (const detail of step.details.slice(0, 6)) {\n      html += '<div style=\"display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px solid #f1f5f9;font-size:13px;\">';\n      html += '<span style=\"color:#475569;\">' + esc(detail.label || '') + '</span>';\n      html += '<span style=\"color:' + color + ';font-weight:700;\">' + esc(detail.value || '') + '</span>';\n      html += '</div>';\n    }\n  }\n  if (step.summary) {\n    html += '<div style=\"margin-top:12px;background:' + bg + ';border-left:3px solid ' + color + ';border-radius:6px;padding:10px 12px;font-size:13px;line-height:1.6;color:#334155;\">' + esc(step.summary) + '</div>';\n  }\n  html += '</div>';\n}\nhtml += '</div>';\n\nif (story) {\n  html += '<div style=\"padding:0 32px 24px;\">';\n  html += '<h2 style=\"font-size:18px;color:#0f172a;margin:0 0 12px;\">Strategische Einordnung</h2>';\n  for (const paragraph of story.split(/\\n\\n+/).map((item) => item.trim()).filter(Boolean)) {\n    html += '<p style=\"font-size:14px;line-height:1.75;color:#334155;margin:0 0 14px;\">' + esc(paragraph) + '</p>';\n  }\n  html += '</div>';\n}\n\nhtml += '<div style=\"background:#0f172a;color:#fff;padding:32px;text-align:center;\">';\nhtml += '<p style=\"font-size:18px;font-weight:800;margin:0 0 12px;\">N\u00e4chster Schritt: 360\u00b0 Deep-Dive</p>';\nhtml += '<p style=\"font-size:14px;line-height:1.7;color:#cbd5e1;margin:0 0 20px;\">Dieser Report zeigt die Oberfl\u00e4che. Der Deep-Dive zeigt die Ursache, Priorit\u00e4t und den sauberen Hebel.</p>';\nhtml += '<a href=\"https://hasimuener.de/customer-journey-audit/#deep-dive-section\" style=\"background:#ffb020;color:#111827;text-decoration:none;padding:14px 28px;border-radius:8px;font-weight:800;display:inline-block;\">360\u00b0 Deep-Dive starten</a>';\nhtml += '<p style=\"font-size:12px;color:#94a3b8;margin:16px 0 0;\">Kein Sales-Call. 5 Fragen. Pers\u00f6nliche Analyse.</p>';\nhtml += '</div>';\nhtml += '</div></div>';\n\nconst attachmentHtml = '<!DOCTYPE html><html lang=\"de\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Customer Journey Audit</title></head><body>' + html + '</body></html>';\nconst attachmentBuffer = Buffer.from(attachmentHtml, 'utf8');\n\nreturn [{\n  json: {\n    email,\n    subject: siteLabel + ': ' + subjectHook,\n    html,\n    mode,\n    respondToWebhook: mode === 'email_capture',\n    jobId: $json.jobId || '',\n  },\n  binary: {\n    attachment_0: {\n      data: attachmentBuffer.toString('base64'),\n      mimeType: 'text/html',\n      fileName: 'Customer-Journey-Audit-' + fileBase + '.html',\n      fileExtension: 'html',\n    },\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        4896,
        -400
      ],
      "id": "822f393a-9d26-4e2b-a84f-f478ae102d3f",
      "name": "Build Email HTML"
    },
    {
      "parameters": {
        "fromEmail": "performance@hasimuener.de",
        "toEmail": "={{ $json.email }}",
        "subject": "={{ $json.subject }}",
        "html": "={{ $json.html }}",
        "options": {
          "attachments": "attachment_0"
        }
      },
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [
        5136,
        -400
      ],
      "id": "c5990da8-af08-422c-bfe6-0a81c1d35319",
      "name": "Send email",
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.respondToWebhook === true }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        5376,
        -400
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00005",
      "name": "IF Email Response Needed?"
    },
    {
      "parameters": {
        "jsCode": "const sd = $getWorkflowStaticData('global');\nconst req = $('URL Validator').first().json;\nconst raw = sd[req.jobId];\n\nif (!raw) {\n  return [{ json: { ok: false, status: 'processing', jobId: req.jobId, message: 'Audit l\u00e4uft noch. Bitte in wenigen Sekunden erneut versuchen.' } }];\n}\n\nlet entry;\ntry {\n  entry = JSON.parse(raw);\n} catch (e) {\n  return [{ json: { ok: false, status: 'error', jobId: req.jobId, error: 'Gespeichertes Audit konnte nicht gelesen werden.' } }];\n}\n\nif (entry.expiresAt && entry.expiresAt < Date.now()) {\n  delete sd[req.jobId];\n  return [{ json: { ok: false, status: 'expired', jobId: req.jobId, error: 'Das Audit-Ergebnis ist abgelaufen.' } }];\n}\n\nif (entry.status !== 'done' || !entry.data) {\n  return [{ json: { ok: false, status: entry.status || 'processing', jobId: req.jobId, message: entry.message || 'Audit l\u00e4uft noch...' } }];\n}\n\nreturn [{\n  json: {\n    ok: true,\n    mode: 'email_capture',\n    status: 'done',\n    jobId: req.jobId,\n    email: req.email,\n    frontendData: entry.data,\n  },\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        432,
        -240
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00006",
      "name": "Load Stored Audit For Email"
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{ $json.ok === true }}",
              "operation": "isTrue"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [
        672,
        -240
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00007",
      "name": "IF Stored Audit Ready?"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        912,
        -120
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00008",
      "name": "Respond Email Capture State"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ { ok: true, status: \"sent\", jobId: $json.jobId || \"\", message: \"Report wird zugestellt. Bitte Posteingang pr\u00fcfen.\" } }}",
        "options": {
          "responseHeaders": {
            "entries": [
              {
                "name": "Access-Control-Allow-Origin",
                "value": "*"
              }
            ]
          }
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        5616,
        -320
      ],
      "id": "1a7c1a10-4b58-4f80-840b-5d1e9fa00009",
      "name": "Respond Email Capture OK"
    },
    {
      "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": [
        -384,
        80
      ],
      "id": "bc8ec8ab-0fda-4426-a2a7-82eeb9511492",
      "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: 'Ung\u00fcltige Job-ID.' } }];\n}\n\nconst raw = sd[jobId];\nif (!raw) {\n  return [{ json: { ok: true, status: 'processing', jobId, message: 'Audit l\u00e4uft 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 || 'Audit fehlgeschlagen.' } }];\n}\n\nreturn [{ json: { ok: true, status: entry.status || 'processing', jobId, message: entry.message || 'Audit l\u00e4uft noch...' } }];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -144,
        80
      ],
      "id": "ad448cf2-625f-46c2-a220-b9982f33b061",
      "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": [
        96,
        80
      ],
      "id": "126606ab-8da2-43be-901f-76ae593d201c",
      "name": "Respond Status"
    }
  ],
  "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": "IF Email Capture?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Email Capture?": {
      "main": [
        [
          {
            "node": "Load Stored Audit For Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond OK",
            "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": "Sitemap Fetch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sitemap Fetch": {
      "main": [
        [
          {
            "node": "Sitemap Fetch Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sitemap Fetch Fallback": {
      "main": [
        [
          {
            "node": "Sitemap Final",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sitemap Final": {
      "main": [
        [
          {
            "node": "Analyze Site",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Site": {
      "main": [
        [
          {
            "node": "Keyword Generator",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keyword Generator": {
      "main": [
        [
          {
            "node": "Extract Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Keywords": {
      "main": [
        [
          {
            "node": "SERP Check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SERP Check": {
      "main": [
        [
          {
            "node": "Build Journey",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Journey": {
      "main": [
        [
          {
            "node": "Write Story",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Write Story": {
      "main": [
        [
          {
            "node": "Build Frontend Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Frontend Data": {
      "main": [
        [
          {
            "node": "Store Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Results": {
      "main": [
        [
          {
            "node": "IF Auto Email Requested",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Auto Email Requested": {
      "main": [
        [
          {
            "node": "Prepare Auto Email Request",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Prepare Auto Email Request": {
      "main": [
        [
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Load Stored Audit For Email": {
      "main": [
        [
          {
            "node": "IF Stored Audit Ready?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Stored Audit Ready?": {
      "main": [
        [
          {
            "node": "Build Email HTML",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond Email Capture State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email HTML": {
      "main": [
        [
          {
            "node": "Send email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send email": {
      "main": [
        [
          {
            "node": "IF Email Response Needed?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF Email Response Needed?": {
      "main": [
        [
          {
            "node": "Respond Email Capture OK",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Webhook Status": {
      "main": [
        [
          {
            "node": "Lookup Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Lookup Results": {
      "main": [
        [
          {
            "node": "Respond Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {},
  "meta": {
    "templateCredsSetupCompleted": true
  }
}