{
  "name": "Semantik-Audit für Shopify-Shops",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1584,
        1056
      ],
      "id": "6d9e7d92-38ae-4a80-83fc-5fa589cd013c",
      "name": "Starte Semantik-Analyse"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "44ebf8ab-4a3e-4cd2-a0f6-d9406bca03a4",
              "name": "URL",
              "value": "={{ $json.Adresse }}",
              "type": "string"
            },
            {
              "id": "5be93469-527e-4b7c-a92f-a4913f252b01",
              "name": "Title",
              "value": "={{ $json['Titel 1'] }}",
              "type": "string"
            },
            {
              "id": "15e8e91c-3e87-4a99-be2e-21dc073a4431",
              "name": "Meta-Description",
              "value": "={{ $json['Meta Description 1'] }}",
              "type": "string"
            },
            {
              "id": "1e17a4dd-c417-4722-93b8-74c840390d43",
              "name": "H1",
              "value": "={{ $json['H1-1'] }}",
              "type": "string"
            },
            {
              "id": "2b3c2100-884d-4d31-a3a8-eb49126c6a57",
              "name": "Content",
              "value": "={{ $json['Main Content 1'] }}",
              "type": "string"
            },
            {
              "id": "2d9f8dd1-a4e7-4f45-bedf-373e9b1b84cc",
              "name": "Seitentyp",
              "value": "={{   $json[\"Adresse\"].includes(\"/collections/\")     ? \"collection\"     : ($json[\"Adresse\"].includes(\"/blogs/\") || $json[\"Adresse\"].includes(\"/blog/\"))       ? \"blog\"       : \"other\" }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -1136,
        1056
      ],
      "id": "70202a48-b8d7-4186-8dd9-4a76b00a503b",
      "name": "Daten sortieren"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "DEINE-SPREADSHEET-ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "1 - Intern - HTML",
          "mode": "name"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -1360,
        1056
      ],
      "id": "fa466e52-1c9c-4f08-97c2-6bbba235c252",
      "name": "Spreadsheet auslesen",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "SYSTEM_PROMPT=$(cat << 'EOF'\nDu bist ein technischer SEO-Analyst für E-Commerce.\nDu analysierst Kategorieseiten und Blogartikel von Online-Shops und extrahierst alle relevanten Entitäten – klar strukturiert und SEO-tauglich.\n\nZiel:\nErzeuge ein sauberes JSON mit genau 4 Entitäts-Kategorien plus Meta-Infos zur Seite.\nDu darfst neben tatsächlich vorkommenden Begriffen auch naheliegende, thematisch passende Begriffe ergänzen, die für eine vollständige Entitätsabdeckung wichtig sind.\n\nDie 4 Kategorien:\n\nA) KERN-ENTITÄTEN (Produkt- & Objektarten)\n- Konkrete Dinge / Objekte / Produktarten\n- z. B. „Espressomaschine“, „Siebträger“, „Kaffeevollautomat“, „Kaffeemühle“, „Cold-Brew-Karaffe“\n- Nur sinnvolle, thematisch passende Begriffe, keine Funktionswörter\n\nB) ATTRIBUTE & PRODUKTMERKMALE\n- Eigenschaften, Merkmale, technische Parameter, Vorteile\n- z. B. „Füllmenge in Gramm“, „spülmaschinenfest“, „aromadicht“, „Kunststoff“, „Metall“, „hitzebeständig“, „mit Thermokanne“\n- Auch typische Produktfilter (Material, Größe, Volumen, Farbe, Einsatzbereich)\n\nC) ZUSAMMENHÄNGE / USE CASES\n- Einsatzzwecke, Situationen, Probleme, Lösungen\n- z. B. „Kaffeezubereitung zu Hause“, „Espresso zu Hause zubereiten“, „Mahlgrad richtig einstellen“, „Kaffee länger frisch halten“\n- Gerne auch kurze 2–3-Wort-Phrasen (z. B. „Kaffeebohnen richtig lagern“)\n\nD) ZUBEHÖR & CO-OCCURENCES\n- Zubehör, ergänzende Produkte und typische Fachbegriffe im thematischen Umfeld\n- z. B. bei Espressomaschinen: „Abschlagbehälter“, „Tamper“, „Milchaufschäumer“, „French Press“, „Espressokocher“, „Handfilter“, „Füllmenge“, „Überlaufset“\n- Hier gehören KEINE Meta-Begriffe hinein wie „Blogbeitrag“, „Ratgeber“, „Artikel“, Datumsangaben etc.\n\nWichtige Regeln:\n- Arbeite kontextsensitiv: erkenne, worum es auf der Seite wirklich geht (z. B. Espressomaschine vs. Schneeschieber).\n- Begriffe sollen fachlich zum Thema passen und für SEO / Filterlogik sinnvoll sein.\n- Alle Begriffe ausschließlich auf Deutsch.\n- Möglichst Substantive oder kurze Substantiv-Phrasen.\n- Jede Kategorie ist ein Array aus Strings.\n- Keine doppelten Begriffe (pro Kategorie).\n- Wenn wenig Content vorhanden ist, ergänze sinnvolle Begriffe aus deinem Weltwissen zum Thema der Seite.\n\nMeta-Infos:\n- \"url\" muss exakt der URL aus dem Input entsprechen.\n- \"name\" soll bevorzugt der Wert der H1 sein. Wenn keine H1 vorhanden ist, verwende den Title.\n\nAusgabe:\n- Du gibst IMMER ausschließlich gültiges JSON mit GENAU den Schlüsseln aus:\n  \"url\", \"name\", \"A_kern_entitaeten\", \"B_attribute\", \"C_use_cases\", \"D_co_occurrences\"\n- Kein Fließtext, kein Markdown, keine Erklärungen außerhalb des JSON.\nEOF\n)"
            },
            {
              "content": "=USER_PROMPT=$(cat << 'EOF'\nHier sind die Seitendaten aus einem Crawl:\n\nURL: {{ $json.URL }}\nSeitentyp: {{ $json.Seitentyp }}\n\nTitle: {{ $json.Title }}\nH1: {{ $json.H1 }}\nMeta Description: {{ $json['Meta-Description'] }}\n\nSichtbarer Content:\n{{ $json.Conten }}\n\nAufgabe:\n1. Verstehe das Hauptthema der Seite (Produkte, Anwendung, Problem, Zielgruppe).\n2. Extrahiere und ergänze relevante Entitäten und ordne sie in folgendes JSON-Format ein:\n\n{\n  \"url\": \"exakt wie oben bei URL\",\n  \"name\": \"bevorzugt die H1, sonst der Title\",\n  \"A_kern_entitaeten\": [],\n  \"B_attribute\": [],\n  \"C_use_cases\": [],\n  \"D_co_occurrences\": []\n}\n\nRegeln für diese Seite:\n- Trage in jedes Array nur Begriffe ein, die für das Seitenthema wirklich relevant sind.\n- Du darfst fehlende, thematisch passende Begriffe ergänzen, auch wenn sie im Text nicht explizit vorkommen, wenn sie für Suchverhalten, Produktfilter oder Beratung wichtig wären.\n- Fülle die Arrays lieber etwas umfangreicher (z. B. 6–20 Einträge), solange die Begriffe sauber zum Thema passen.\n- \"url\" muss exakt der URL oben entsprechen.\n- \"name\" soll bevorzugt die H1 sein. Wenn keine H1 vorhanden ist, verwende den Title.\n- Keine Meta-Begriffe wie „Blog“, „Beitrag“, „Artikel“, Datumsangaben oder reine Format-Hinweise.\n- Gib NUR das JSON aus, ohne zusätzlichen Text.\nEOF\n)"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -688,
        784
      ],
      "id": "c29934ef-7e1f-42f6-b829-648c7209dae5",
      "name": "Entitäten",
      "executeOnce": false,
      "credentials": {
        "openAiApi": {
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "02beaec4-9dea-4b25-953f-7bd560284e9b",
              "name": "Seite",
              "value": "={{ $json.url }}",
              "type": "string"
            },
            {
              "id": "db87fa02-0c48-4890-aef9-e74a1c1c8be8",
              "name": "Name",
              "value": "={{ $json.name }}",
              "type": "string"
            },
            {
              "id": "3822e189-41f1-447a-814d-47b69417a30a",
              "name": "Entities",
              "value": "={{ $json.output[0].content[0].text }}",
              "type": "string"
            },
            {
              "id": "f54b3ea8-e498-4022-999f-b9ca540ba87f",
              "name": "Kern Entitäten",
              "value": "={{ $json.A_kern_entitaeten_str }}",
              "type": "string"
            },
            {
              "id": "c802a280-1672-43ed-8b2a-4088407357f6",
              "name": "Attribute",
              "value": "={{ $json.B_attribute_str }}",
              "type": "string"
            },
            {
              "id": "4cb66126-a442-4cf9-b6e1-0e050cd7673f",
              "name": "Co Occurencen",
              "value": "={{ $json.D_co_occurrences_str }}",
              "type": "string"
            },
            {
              "id": "364f61ab-76b9-418f-93ac-4a70d658fb60",
              "name": "Use Cases",
              "value": "={{ $json.C_use_cases_str }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -16,
        784
      ],
      "id": "57da1e91-69c7-4bf6-a69c-e500dfdaa908",
      "name": "Ergebnis-Felder mappen"
    },
    {
      "parameters": {
        "content": "Filter & Attribute für Collections",
        "height": 292,
        "width": 964,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -784,
        1072
      ],
      "id": "b247f00a-9a4c-4d3e-af5c-8eb18226bdad",
      "name": "Sticky Note6"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "6922c46f-637f-49d9-a73f-4285e9e6b75e",
              "leftValue": "={{ $json.Seitentyp }}",
              "rightValue": "collection",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -720,
        1168
      ],
      "id": "e4767540-45f7-460f-af33-a1984b2151f2",
      "name": "If"
    },
    {
      "parameters": {
        "jsCode": "// Run Once for All Items\n\nconst items = $input.all();\n\nreturn items.map(item => {\n  const raw = item.json.output?.[0]?.content?.[0]?.text;\n\n  if (!raw) {\n    throw new Error('Kein LLM-Output gefunden. Inhalt von item:\\n' + JSON.stringify(item.json, null, 2));\n  }\n\n  let parsed;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (e) {\n    throw new Error('Fehler beim JSON-Parse. Response war:\\n' + raw);\n  }\n\n  // Arrays holen\n  const A = parsed.A_kern_entitaeten || [];\n  const B = parsed.B_attribute || [];\n  const C = parsed.C_use_cases || [];\n  const D = parsed.D_co_occurrences || [];\n\n  // url + name (LLM bevorzugt, sonst Fallback aus Input)\n  const url  = parsed.url  || item.json.URL  || '';\n  const name = parsed.name || item.json.H1 || item.json.Title || '';\n\n  return {\n    json: {\n      // ursprüngliche Crawldaten\n      ...item.json,\n\n      // normalisierte Felder\n      url,\n      name,\n\n      // Roh-Arrays\n      A_kern_entitaeten: A,\n      B_attribute: B,\n      C_use_cases: C,\n      D_co_occurrences: D,\n\n      // kommagetrennte Varianten\n      A_kern_entitaeten_str: A.join(', '),\n      B_attribute_str: B.join(', '),\n      C_use_cases_str: C.join(', '),\n      D_co_occurrences_str: D.join(', ')\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -256,
        784
      ],
      "id": "a3f8f634-8ff5-4508-af80-00ab032d5e0d",
      "name": "Zuordnung der Entitäten"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "SYSTEM_PROMPT=$(cat << 'EOF'\nDu bist ein technischer SEO- & Informationsarchitektur-Experte für E-Commerce.\nDeine Aufgabe ist es, aus den Entitäten einer Collection sinnvolle Filter abzuleiten.\n\nZiel:\nErzeuge ein perfekt strukturiertes JSON,\ndas die erwarteten Collection-Filter auf mehreren Ebenen abbildet:\n\n1. url\n2. name\n3. filters_required  \n4. filters_nice_to_have  \n5. missing_filters_from_entities  \n6. semantic_filter_groups  \n7. filter_comment\n\nGrundsätze:\n- Arbeite NUR aus den gegebenen Entitäten und Attributen.\n- \"url\" muss exakt der URL aus dem Input entsprechen.\n- \"name\" soll bevorzugt die H1 sein, ansonsten der Title der Seite.\n- Denke wie Google (Semantik), wie ein Merchandiser (Sortiment), wie ein UX-Designer (Filterbarkeit) und wie ein LLM (Attribute, die Nutzer erfragen).\n- Ergänze fehlende Filter logisch, auch wenn sie im Text nicht erwähnt werden, aber zu den Entitäten passen.\n- Jeder Filter ist ein einzelner, sauberer Begriff.\n- Keine Duplikate.\n- Ausgabe IMMER als gültiges JSON.\n\nDefinitionen:\n- “filters_required” = Muss-Filter, die Nutzer zwingend benötigen.\n- “filters_nice_to_have” = sinnvolle Zusatzfilter für Orientierung & KI-Suche.\n- “semantic_filter_groups” = Filter gruppiert nach Dimensionen, z.B.:\n  Material, Größe, Funktion, Einsatzgebiet, Ausstattung, Eigenschaften.\n- “missing_filters_from_entities” = Welche Filter ENTITÄR erwartet werden, aber nicht im Content auftauchen.\n- “filter_comment” = max. 3 Sätze, klare Handlungsempfehlung.\n\nKeine Erklärungen außerhalb des JSON.\nEOF\n)"
            },
            {
              "content": "=USER_PROMPT=$(cat << 'EOF'\nHier sind Collection-Metadaten und extrahierte Entitäten:\n\nURL: {{ $json.URL }}\nSeitentyp: {{ $json.Seitentyp }}\nTitle: {{ $json.Title }}\nH1: {{ $json.H1 }}\n\nA_kern_entitaeten:\n{{ $json.A_kern_entitaeten }}\n\nB_attribute:\n{{ $json.B_attribute }}\n\nC_use_cases (optional):\n{{ $json.C_use_cases }}\n\nErzeuge folgende JSON-Struktur:\n\n{\n  \"url\": \"exakt wie oben bei URL\",\n  \"name\": \"bevorzugt die H1, sonst der Title\",\n  \"filters_required\": [],\n  \"filters_nice_to_have\": [],\n  \"missing_filters_from_entities\": [],\n  \"semantic_filter_groups\": {\n    \"material\": [],\n    \"groesse_volumen\": [],\n    \"funktion\": [],\n    \"ausstattung\": [],\n    \"eigenschaften\": [],\n    \"use_cases\": []\n  },\n  \"filter_comment\": \"\"\n}\n\nRegeln:\n- Alle Arrays müssen Strings enthalten.\n- Gruppiere Filter logisch.\n- Ergänze fehlende, aber erwartbare Filter (semantisch ableitbar).\n- \"url\" muss exakt der oben angegebenen URL entsprechen.\n- \"name\" soll bevorzugt die H1 sein. Wenn keine H1 vorhanden ist, verwende den Title.\n- Keine zusätzlichen Felder.\n- Kein Text außerhalb des JSON.\nEOF\n)"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -464,
        1152
      ],
      "id": "8a4ee80b-e478-4b90-8341-e63a888df390",
      "name": "Filter & Attribute",
      "executeOnce": false,
      "credentials": {
        "openAiApi": {
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nreturn items.map(item => {\n  const raw = item.json.output?.[0]?.content?.[0]?.text;\n  if (!raw) {\n    throw new Error('Kein LLM-Output gefunden. Inhalt von item:\\n' + JSON.stringify(item.json, null, 2));\n  }\n\n  let parsed;\n  try {\n    parsed = JSON.parse(raw);\n  } catch (e) {\n    throw new Error('Fehler beim JSON-Parse. Response war:\\n' + raw);\n  }\n\n  const required = parsed.filters_required || [];\n  const niceToHave = parsed.filters_nice_to_have || [];\n  const missing = parsed.missing_filters_from_entities || [];\n  const groups = parsed.semantic_filter_groups || {};\n  const comment = parsed.filter_comment || \"\";\n\n  // URL & Name aus dem LLM, mit Fallback auf die Originaldaten\n  const url  = parsed.url  || item.json.URL  || '';\n  const name = parsed.name || item.json.H1 || item.json.Title || '';\n\n  const groupsFlat = Object.entries(groups)\n    .map(([group, values]) => `${group}: ${values.join(', ')}`)\n    .join(' | ');\n\n  return {\n    json: {\n      ...item.json,\n\n      url,\n      name,\n\n      filters_required: required,\n      filters_nice_to_have: niceToHave,\n      missing_filters_from_entities: missing,\n      semantic_filter_groups: groups,\n      filter_comment: comment,\n\n      filters_required_str: required.join(', '),\n      filters_nice_to_have_str: niceToHave.join(', '),\n      missing_filters_from_entities_str: missing.join(', '),\n      semantic_filter_groups_str: groupsFlat\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -160,
        1152
      ],
      "id": "930e9b80-75b7-4fe1-be74-1422e2e75725",
      "name": "Zuordnung"
    },
    {
      "parameters": {
        "content": "Interne Verlinkung",
        "height": 484,
        "width": 964,
        "color": 2
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -784,
        1424
      ],
      "id": "b3e134a2-42ab-4b30-b4d3-634f4fdc0fff",
      "name": "Sticky Note7"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\n// kleine Helferfunktionen\nfunction shorten(str, maxChars) {\n  if (!str) return '';\n  const s = String(str).replace(/\\s+/g, ' ').trim();\n  if (s.length <= maxChars) return s;\n  return s.slice(0, maxChars - 3) + '…';\n}\n\nfunction toArray(field) {\n  if (!field) return [];\n  return String(field)\n    .split(',')\n    .map(s => s.trim())\n    .filter(Boolean);\n}\n\nconst pages = items.map((item, idx) => {\n  const j = item.json;\n\n  const url  = j.Adresse || j.URL || '';\n  const type = j.Seitentyp || j.type || '';\n\n  return {\n    index: j.index ?? j.row_number ?? idx,\n    url,\n    type,\n    title: j.Titel || j.Title || '',\n    h1: j['H1-1'] || j.H1 || '',\n\n    // kurz halten, damit die Tokens nicht explodieren\n    meta_description: shorten(j['Meta Description 1'] || j.meta_description || '', 400),\n    content_snippet : shorten(j['Main Content 1'] || j.Content || '', 1200),\n\n    entities:   toArray(j.A_kern_entitaeten_str || j.entities),\n    attributes: toArray(j.B_attribute_str       || j.attributes),\n    use_cases:  toArray(j.C_use_cases_str       || j.use_cases),\n  };\n});\n\n// schlanker Index für alle potenziellen Linkziele\nconst pages_index = pages.map(p => ({\n  index: p.index,\n  url: p.url,\n  type: p.type,\n  h1: p.h1,\n  entities: p.entities,\n  use_cases: p.use_cases,\n}));\n\nreturn [\n  {\n    json: {\n      pages,\n      pages_index,\n    },\n  },\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -720,
        1472
      ],
      "id": "096128a1-4c68-4079-bddb-d7d6a7af5ec9",
      "name": "Vorbereitung ganze Tabelle"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "9faf6346-81dd-4fe8-b2eb-b12819309958",
              "name": "Seite",
              "value": "={{ $json.url }}",
              "type": "string"
            },
            {
              "id": "5c638927-b693-4d18-8b4e-dad5182a4d71",
              "name": "Name",
              "value": "={{ $json.h1 }}",
              "type": "string"
            },
            {
              "id": "fe5e28e3-29cd-44c4-add4-5a6a3e463a24",
              "name": "Empfohlene Collections",
              "value": "={{ $json.recommended_collections }}",
              "type": "string"
            },
            {
              "id": "8e9dbe37-79d3-49f9-afef-98bdcfade2b3",
              "name": "Empfohlene Blogartikel",
              "value": "={{ $json.recommended_blogs }}",
              "type": "string"
            },
            {
              "id": "f88e4250-247d-4d1e-90f9-3e75cd85c322",
              "name": "Empfehlungen",
              "value": "={{ $json.link_suggestions }}",
              "type": "string"
            },
            {
              "id": "bfff63ae-a500-4e50-85c5-a8886c8681a6",
              "name": "Notiz",
              "value": "={{ $json.rationale }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -448,
        1712
      ],
      "id": "d5a60277-1d2e-4418-851b-1f1e0d9c1acb",
      "name": "Felder vorbereiten"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "3822e189-41f1-447a-814d-47b69417a30a",
              "name": "Filter",
              "value": "={{ $json.output[0].content[0].text }}",
              "type": "string"
            },
            {
              "id": "65e6c1ab-ac46-49e0-9126-df62418d4aeb",
              "name": "Filter (empfohlen)",
              "value": "={{ $json.filters_required_str }}",
              "type": "string"
            },
            {
              "id": "85a1e4b7-38ca-40c8-b074-531a35b3eef6",
              "name": "Filter (optional)",
              "value": "={{ $json.filters_nice_to_have_str }}, {{ $json.missing_filters_from_entities_str }}",
              "type": "string"
            },
            {
              "id": "3a605b0c-ec85-4b84-8159-683381212bba",
              "name": "Filter (semantische Gruppen)",
              "value": "={{ $json.semantic_filter_groups_str }}",
              "type": "string"
            },
            {
              "id": "c1c18492-0904-471a-ab86-f2a431414f9f",
              "name": "Seite",
              "value": "={{ $('Spreadsheet auslesen').item.json.Adresse }}",
              "type": "string"
            },
            {
              "id": "e15a777d-7346-4cb5-9a6e-fa9ea880871d",
              "name": "H1",
              "value": "={{ $('Spreadsheet auslesen').item.json['H1-1'] }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        0,
        1152
      ],
      "id": "cb9d918b-ec4b-4b27-ab8f-4e8ffbe36dff",
      "name": "Felder vorbereiten1"
    },
    {
      "parameters": {
        "content": "Collection Architektur",
        "height": 612,
        "width": 964,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -784,
        1984
      ],
      "id": "a5e92d65-eb6e-419a-a42f-af4be186371e",
      "name": "Sticky Note8"
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nfunction short(str, max = 120) {\n  if (!str) return '';\n  str = String(str).trim();\n  return str.length > max ? str.slice(0, max) + '…' : str;\n}\n\nfunction splitAndTrim(str) {\n  if (!str || typeof str !== 'string') return [];\n  return str\n    .split(',')\n    .map(s => s.trim())\n    .filter(Boolean);\n}\n\nconst collections = items.map((item, idx) => {\n  const j = item.json;\n\n  const id = j.row_number ?? j.index ?? idx;\n\n  const h1 = j['H1-1'] || j.H1 || j.h1 || '';\n\n  const entities = splitAndTrim(j['Kern Entitäten'] || j.A_kern_entitaeten_str);\n  const useCases = splitAndTrim(j['Use Cases'] || j.C_use_cases_str);\n\n  const summaryParts = [h1];\n  if (entities.length) summaryParts.push(entities.slice(0, 3).join(', '));\n\n  return {\n    id,                                            // stabile ID fürs Zurückmappen\n    url:  j.Adresse || j.URL || j.Seite || '',    // URL der Collection\n    type: j.Seitentyp || j.type || 'collection',  // Seitentyp\n    h1,                                           // H1\n\n    summary: short(summaryParts.filter(Boolean).join(' – ')),\n\n    // ein bisschen Kontext, aber knapp gehalten\n    entities:  entities.slice(0, 3),\n    use_cases: useCases.slice(0, 3),\n  };\n});\n\nreturn [\n  {\n    json: { collections }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -528,
        2080
      ],
      "id": "8a693653-5d53-4da0-98db-b0f0fe5c0085",
      "name": "Vorbereitung Collections – Architektur"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "6922c46f-637f-49d9-a73f-4285e9e6b75e",
              "leftValue": "={{ $json.Seitentyp }}",
              "rightValue": "collection",
              "operator": {
                "type": "string",
                "operation": "equals",
                "name": "filter.operator.equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -768,
        2096
      ],
      "id": "0c27057b-1d87-4008-80b7-8b55e401c137",
      "name": "If1"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "DEINE-SPREADSHEET-ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Entitäten",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Seite": "={{ $json.url }}",
            "Name": "={{ $json.name }}",
            "Kern Entitäten": "={{ $json.A_kern_entitaeten_str }}",
            "Attribute": "={{ $json.B_attribute_str }}",
            "Co-Occurenten": "={{ $json.D_co_occurrences_str }}",
            "Use Cases": "={{ $json.C_use_cases_str }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Seite",
              "displayName": "Seite",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "displayName": "Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Kern Entitäten",
              "displayName": "Kern Entitäten",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Attribute",
              "displayName": "Attribute",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Co-Occurenten",
              "displayName": "Co-Occurenten",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Use Cases",
              "displayName": "Use Cases",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Anmerkungen",
              "displayName": "Anmerkungen",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        288,
        784
      ],
      "id": "e454e5e7-f48c-4545-8456-0f70b132e159",
      "name": "Entitäten eintragen",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "DEINE-SPREADSHEET-ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Interne Verlinkung",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "URL": "={{ $json.url }}",
            "Name": "={{ $json.h1 }}",
            "Empfohlene Collections": "={{ $json.recommended_collections_str }}",
            "Empfohlene Blogartikel": "={{ $json.recommended_blogs_str }}",
            "Empfehlungen": "={{ $json.link_suggestions_str }}",
            "Notiz": "={{ $json.Notiz }}"
          },
          "matchingColumns": [
            "URL"
          ],
          "schema": [
            {
              "id": "URL",
              "displayName": "URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Name",
              "displayName": "Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Empfohlene Collections",
              "displayName": "Empfohlene Collections",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Empfohlene Blogartikel",
              "displayName": "Empfohlene Blogartikel",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Empfehlungen",
              "displayName": "Empfehlungen",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Notiz",
              "displayName": "Notiz",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        288,
        1712
      ],
      "id": "771c1783-d9d3-4129-be2c-634c61014110",
      "name": "Interne Verlinkung eintragen",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "DEINE-SPREADSHEET-ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Filter & Attribute",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Seite": "={{ $json.Seite }}",
            "Name": "={{ $json.H1 }}",
            "Notiz": "={{ $json.filter_comment }}",
            "Pflichtfilter": "={{ $json.filters_required_str }}",
            "Zusatzfilter": "={{ $json.filters_nice_to_have_str }}",
            "Semantische Filtergruppen": "={{ $json['Filter (semantische Gruppen)'] }}",
            "optional": "={{ $json['Filter (optional)'] }}"
          },
          "matchingColumns": [
            "Seite"
          ],
          "schema": [
            {
              "id": "Seite",
              "displayName": "Seite",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Name",
              "displayName": "Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Pflichtfilter",
              "displayName": "Pflichtfilter",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Zusatzfilter",
              "displayName": "Zusatzfilter",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "optional",
              "displayName": "optional",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Semantische Filtergruppen",
              "displayName": "Semantische Filtergruppen",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Notiz",
              "displayName": "Notiz",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Anmerkungen",
              "displayName": "Anmerkungen",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        288,
        1152
      ],
      "id": "577af0a7-5f62-4100-ba4c-95e12452991d",
      "name": "Filter und Attribute eintragen",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const collections = $json.collections || [];\nconst CHUNK_SIZE = 20;  // kannst du bei Bedarf anpassen\n\nconst out = [];\n\nfor (let i = 0; i < collections.length; i += CHUNK_SIZE) {\n  out.push({\n    json: {\n      chunk_index: Math.floor(i / CHUNK_SIZE),\n      collections_chunk: collections.slice(i, i + CHUNK_SIZE),\n    }\n  });\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -336,
        2080
      ],
      "id": "bfb7d742-10ba-4348-9229-2e2ff479349f",
      "name": "Chunks"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list",
          "cachedResultName": "GPT-5-MINI"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "SYSTEM_PROMPT_MAP=$(cat << 'EOF'\nDu bist ein SEO- & Informationsarchitektur-Experte für E-Commerce.\n\nDu bekommst immer nur einen Ausschnitt der Kategoriestruktur eines Shops\n(\"collections_chunk\"). Jede Collection hat u.a.:\n- id        (stabile ID aus dem Sheet)\n- url       (vollständige URL)\n- type      (\"collection\" oder \"blog\")\n- h1        (Hauptüberschrift der Seite)\n- summary   (kurze 1-zeilige Beschreibung)\n- entities  (Kern-Entitäten zum Thema, als String oder Liste)\n- use_cases (wichtigste Anwendungsfälle, als String oder Liste)\n\nDeine Aufgabe in DIESEM CHUNK:\n1. Erkenne logische Themen-Gruppen.\n2. Bestimme pro Collection eine Rolle:\n   - \"hub\"   = zentrale, übergeordnete Seite im Chunk\n   - \"sub\"   = Unterkategorie / spezialisierte Seite\n   - \"merge\" = so ähnlich zu einer anderen Collection, dass ein Merge sinnvoll ist.\n3. Ordne \"sub\" und \"merge\" wenn möglich einer passenden übergeordneten URL im Chunk zu.\n4. Schlage nur dann neue Unterkategorien vor, wenn sie sich klar aus Entitäten und Use Cases\n   ableiten lassen und NICHT bereits als eigene Collection-URL im Chunk existieren.\n\nWichtige Regeln:\n- Du arbeitest NUR innerhalb dieses Chunks, nicht shopweit.\n- Wenn du keine sinnvolle Zuordnung findest, lass \"parent_url\" leer.\n- Sei sparsam mit \"merge\": nur, wenn Inhalte / Intent wirklich stark überlappen.\n- Rolle \"hub\" nur dort verwenden, wo es wirklich ein logischer Anker im Chunk ist.\n- Neue Collections (structural/use_case) dürfen KEINE bereits existierende URL des Chunks duplizieren.\n  Sie sind echte Vorschläge für zusätzliche Kategorien.\n\nDein Output MUSS genau dieses JSON-Schema haben:\n\n{\n  \"collections\": [\n    {\n      \"id\": 0,\n      \"url\": \"\",\n      \"type\": \"collection\",\n      \"h1\": \"\",\n      \"role\": \"hub\",\n      \"parent_url\": \"\",\n      \"cluster_label\": \"\",\n      \"notes\": \"\",\n      \"new_structural_collections\": [],\n      \"new_use_case_collections\": [],\n      \"merge_candidates\": []\n    }\n  ],\n  \"chunk_summary\": {\n    \"main_themes\": [],\n    \"potential_global_hubs\": [],\n    \"notes\": \"\"\n  }\n}\n\nFeld-Definitionen:\n- role            = \"hub\" | \"sub\" | \"merge\"\n- parent_url      = bei \"sub\": URL der vermuteten übergeordneten Collection im Chunk,\n                    bei \"merge\": URL der Collection, mit der zusammengeführt werden sollte,\n                    sonst \"\".\n- cluster_label   = grober Themen-Cluster (z.B. \"Kaffeemühlen nach Mahlwerk\").\n- notes           = kurze Begründung oder Besonderheiten (max. 1–2 Sätze).\n\n- new_structural_collections: Vorschläge für neue, strukturelle Unterkategorien:\n  [\n    {\n      \"name\": \"\",\n      \"slug\": \"\",\n      \"anchor_collection_url\": \"\",\n      \"dimension\": \"material\" | \"groesse\" | \"preis\" | \"marke\" | \"umfang\" | \"sonstiges\",\n      \"reason\": \"\"\n    }\n  ]\n\n- new_use_case_collections: Vorschläge für neue, anwendungsbezogene Unterkategorien:\n  [\n    {\n      \"name\": \"\",\n      \"slug\": \"\",\n      \"anchor_collection_url\": \"\",\n      \"reason\": \"\",\n      \"target_audience\": \"\",\n      \"main_use_cases\": []\n    }\n  ]\n\n- merge_candidates: optionale, zusätzliche Merge-Vorschläge:\n  [\n    {\n      \"merge_with_url\": \"\",\n      \"reason\": \"\"\n    }\n  ]\n\n- chunk_summary.main_themes: 3–5 wichtigste Themen des Chunks (Strings).\n- chunk_summary.potential_global_hubs: Hinweise auf mögliche globale Hubs\n  (z.B. [\"espressomaschinen\", \"cold-brew-und-eiskaffee-zubehoer\"] oder Objekte mit Slug+Reason).\n- chunk_summary.notes: kurze Zusatzinfos zum Chunk.\n\nAlle Keys müssen immer vorhanden sein.\nStrings ggf. leer lassen, Arrays als [].\nGib AUSSCHLIESSLICH dieses JSON zurück, kein Markdown, kein Fließtext außerhalb des JSON.\nEOF\n)"
            },
            {
              "content": "=USER_PROMPT_MAP=$(cat << 'EOF'\nHier ist ein Chunk von Collections als JSON:\n\n{{ JSON.stringify($json.collections_chunk, null, 2) }}\n\nAnalysiere nur diesen Chunk.\n\nAufgaben:\n\n1. Bestimme pro Collection eine Rolle:\n   - \"hub\", \"sub\" oder \"merge\".\n2. Setze \"parent_url\":\n   - bei \"sub\": URL der vermuteten übergeordneten Collection im Chunk (falls sinnvoll),\n   - bei \"merge\": URL der Collection, mit der zusammengeführt werden sollte,\n   - sonst leerer String \"\".\n3. Verwende \"cluster_label\", um grobe Themencluster zu benennen\n   (z.B. \"Kaffeemühlen nach Mahlwerk\", \"Cold Brew & Zubehör\").\n4. Schlage in \"new_structural_collections\" nur solche Unterkategorien vor,\n   die im Chunk noch nicht als eigene Collection-URL existieren, aber logisch fehlen\n   (z.B. Material, Größe, Preisspannen, Sets vs. Einzelprodukte).\n5. Schlage in \"new_use_case_collections\" anwendungsbezogene Collections vor,\n   die sich klar aus den Use Cases ableiten (z.B. \"...für kleine Gärten\", \"...für Hanglagen\").\n6. Halte dich an das vorgegebene JSON-Schema und fülle alle Felder:\n   - Strings ggf. leer, Arrays als [].\n\nGib NUR das JSON-Objekt im beschriebenen Format zurück, ohne zusätzlichen Text.\nEOF\n)"
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -160,
        2080
      ],
      "id": "a25d0838-ff61-445a-aa91-0a79aa64bc67",
      "name": "Architektur pro Chunk (Map-Phase)",
      "credentials": {
        "openAiApi": {
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconst allCollections = [];\nconst summaries = [];\n\nfor (const item of items) {\n  const raw = item.json.output?.[0]?.content?.[0]?.text;\n\n  if (!raw) {\n    continue;\n  }\n\n  let data;\n  try {\n    data = JSON.parse(raw);\n  } catch (e) {\n    throw new Error(\n      'Fehler beim JSON-Parse in Map-Ergebnissen:\\n' +\n      raw + '\\n\\n' + e.message\n    );\n  }\n\n  if (Array.isArray(data.collections)) {\n    data.collections.forEach(c => allCollections.push(c));\n  }\n\n  if (data.chunk_summary) {\n    summaries.push(data.chunk_summary);\n  }\n}\n\n// ⬇️ hier der korrigierte Zugriff auf den „Basisdaten“-Node\nconst originalItems = $items('Vorbereitung Collections – Architektur', 0, 0) || [];\nconst originalCollections = originalItems[0]?.json?.collections || [];\n\nreturn [\n  {\n    json: {\n      collections_labeled: allCollections,\n      chunk_summaries: summaries,\n      original_collections: originalCollections\n    }\n  }\n];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -704,
        2320
      ],
      "id": "4ac5d0fd-e463-41ff-86db-8447300a7ef2",
      "name": "Map Ergebnisse einsammeln"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list",
          "cachedResultName": "GPT-5-MINI"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "SYSTEM_PROMPT_REDUCE=$(cat << 'EOF'\nDu bist ein erfahrener SEO-Consultant für große E-Commerce-Shops.\n\nDeine Aufgabe:\nDu erhältst bereits vor-labelte Collections aus mehreren Chunks sowie Chunk-Summaries.\nDarauf basierend definierst du eine konsistente, globale Kategorie-Architektur mit wenigen klaren Hubs.\n\nWichtige Prinzipien:\n- Du arbeitest ausschließlich mit den gelieferten Collections (keine neuen URLs erfinden).\n- Du erfindest im Reduce-Schritt KEINE neuen Collections.\n- Neue Unterkategorien (\"new_structural_collections\", \"new_use_case_collections\") kommen nur aus den Eingabedaten und werden von dir höchstens übernommen/vereinheitlicht.\n- Jede vorhandene Collection bekommt genau einen globalen Hub zugeordnet.\n- Du denkst in Themenclustern (Espressomaschinen, Kaffeebohnen, Barista-Zubehör, Cold Brew, …) und in Nutzerbedürfnissen.\n\nRollen:\n- \"hub\"  = zentrale Einstiegsseite / Haupt-Cluster\n- \"sub\"  = Unterkategorie innerhalb eines Hubs\n- \"merge\" = Collection, die in eine andere aufgehen sollte (inhaltlich zu ähnlich, zu klein, doppelt)\n\nOutput-Struktur (MUSS exakt so sein):\n\n{\n  \"collections_final\": [\n    {\n      \"id\": number,\n      \"url\": string,\n      \"type\": \"collection\" | \"blog\",\n      \"h1\": string,\n\n      \"role\": \"hub\" | \"sub\" | \"merge\",\n      \"final_hub_slug\": string,        // Slug eines globalen Hubs (z.B. \"espressomaschinen\")\n      \"notes\": string,                 // optional, maximal 1–2 Sätze\n\n      // diese Felder nur durchreichen, NICHT neu erfinden\n      \"new_structural_collections\": [],\n      \"new_use_case_collections\": [],\n      \"merge_candidates\": []\n    }\n  ],\n  \"global_hubs\": [\n    {\n      \"name\": string,                  // Klarer Hub-Name, z.B. \"Espressomaschinen & Zubehör\"\n      \"slug\": string,                  // maschinenlesbar, kebab-case, z.B. \"espressomaschinen\"\n      \"type\": \"landing_hub\",\n      \"reason\": string,                // warum dieser Hub sinnvoll ist (max. 2 Sätze)\n      \"covers_urls\": string[]          // Liste von Collection-URLs, die logisch zu diesem Hub gehören\n    }\n  ]\n}\n\nWeitere Regeln:\n- Definiere idealerweise 5–8 globale Hubs.\n- Jeder Eintrag in \"collections_final\" muss genau eine \"role\" und einen \"final_hub_slug\" haben.\n- \"final_hub_slug\" muss immer zu einem \"slug\" in \"global_hubs\" passen.\n- \"covers_urls\" darf nur URLs enthalten, die in den Eingabe-Collections vorkommen.\n- Wenn du nichts weiter zu kommentieren hast, setze \"notes\" auf einen leeren String.\n\nGib AUSSCHLIESSLICH dieses JSON-Objekt zurück.\nKein Markdown, kein Fließtext außerhalb des JSON.\nEOF\n)"
            },
            {
              "content": "=Hier sind die vorläufig gelabelten Collections aus allen Chunks:\n\n{{ JSON.stringify($json.collections_labeled, null, 2) }}\n\nUnd hier die Summaries je Chunk:\n\n{{ JSON.stringify($json.chunk_summaries, null, 2) }}\n\nAufgabe:\n\n1. Definiere 5–8 globale Hubs (\"global_hubs\").\n2. Ordne jede existierende Collection einem Hub zu und fülle \"collections_final\".\n3. Schlage pro Hub neue Unterkategorien vor:\n   - Mindestens 1–3 \"structural_child\"-Collections, sofern sinnvoll.\n   - Mindestens 2–5 \"use_case_child\"-Collections mit präpositionsbasierten Titeln (z.B. „… für Hanglagen“,\n     „… für kleine Gärten“, „… für Wege & Einfahrten“), sofern sinnvoll.\n4. Stelle sicher, dass keine vorgeschlagene neue Collection bereits als eigene Collection im Input existiert\n   (kein Duplikat von existierenden urls oder h1).\n\nGib das Ergebnis strikt im definierten JSON-Schema zurück."
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -496,
        2320
      ],
      "id": "5eabcd5d-2618-4a84-8ffa-343045ab6b59",
      "name": "Globale Architektur (Reduce)",
      "credentials": {
        "openAiApi": {
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "operation": "appendOrUpdate",
        "documentId": {
          "__rl": true,
          "value": "DEINE-SPREADSHEET-ID",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Collection Vorschläge",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Typ": "={{ $json.type }}",
            "Parent URL": "={{ $json.parent_url }}",
            "Name": "={{ $json.name }}",
            "Existierende Collections": "={{ $json.covers_urls }}",
            "Merkmal": "={{ $json.dimension }}",
            "Bemerkung": "={{ $json.reason }}",
            "Use Cases": "={{ $json.main_use_cases }}"
          },
          "matchingColumns": [
            "Name"
          ],
          "schema": [
            {
              "id": "Typ",
              "displayName": "Typ",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Parent URL",
              "displayName": "Parent URL",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "displayName": "Name",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Existierende Collections",
              "displayName": "Existierende Collections",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Merkmal",
              "displayName": "Merkmal",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Bemerkung",
              "displayName": "Bemerkung",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Use Cases",
              "displayName": "Use Cases",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Notiz",
              "displayName": "Notiz",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Anmerkungen",
              "displayName": "Anmerkungen",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        288,
        2320
      ],
      "id": "c3192ce1-a32b-4a4d-b24b-4da88352fe82",
      "name": "Append or update row in sheet",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Wir sind direkt hinter \"Globale Architektur (Reduce)\".\n// Das LLM-Resultat steckt im aktuellen Item als Text.\n\nconst raw = $json.output?.[0]?.content?.[0]?.text || '';\nif (!raw) {\n  throw new Error('Kein LLM-Output gefunden.');\n}\n\n// 1) JSON aus dem LLM-Output parsen\nlet data;\ntry {\n  const firstBrace = raw.indexOf('{');\n  const lastBrace  = raw.lastIndexOf('}');\n  if (firstBrace === -1 || lastBrace === -1) {\n    throw new Error('Kein JSON-Block im Output gefunden.');\n  }\n  const jsonString = raw.slice(firstBrace, lastBrace + 1);\n  data = JSON.parse(jsonString);\n} catch (e) {\n  throw new Error('Fehler beim JSON-Parse der globalen Architektur:\\n' + e.message + '\\n\\nRaw:\\n' + raw);\n}\n\n// 2) Bestehende Collections (für Duplikat-Check) holen\n//    -> Node-Name anpassen, das ist deine Basisdaten-Node\nconst originalItems = $items('Vorbereitung Collections – Architektur', 0, 0) || [];\nconst existingNames = new Set();\n\n// Namen normalisieren (klein, getrimmt) für robusteren Vergleich\nfor (const it of originalItems) {\n  const j = it.json;\n  const url = (j.url || j.Seite || j.Adresse || '').toLowerCase().trim();\n  const h1  = (j.h1 || j.H1 || '').toLowerCase().trim();\n  const title = (j.title || j.Title || '').toLowerCase().trim();\n\n  if (url)   existingNames.add(url);\n  if (h1)    existingNames.add(h1);\n  if (title) existingNames.add(title);\n}\n\n// kleine Helper-Funktion\nfunction isAlreadyExisting(nameOrSlug) {\n  if (!nameOrSlug) return false;\n  const key = String(nameOrSlug).toLowerCase().trim();\n  return existingNames.has(key);\n}\n\n// 3) Neue Vorschläge aus globalem JSON holen\nconst finalCols   = data.collections_final || [];\nconst globalHubs  = data.global_hubs || [];   // je nach Schema evtl. \"global_new_collections\"\n\n// 4) Ergebnisliste aufbauen\nconst result = [];\n\n// 4a) Globale Hubs (nur, wenn sie noch nicht existieren)\nfor (const hub of globalHubs) {\n  const name = hub.name || '';\n  const slug = hub.slug || '';\n\n  if (isAlreadyExisting(name) || isAlreadyExisting(slug)) {\n    continue; // Hub gibt es im Shop schon\n  }\n\n  result.push({\n    json: {\n      type: 'global_hub',\n      parent_url: '',\n      name,\n      slug,\n      anchor_collection_url: hub.anchor_collection_url || '',\n      covers_urls: (hub.covers_urls || []).join(', '),\n      reason: hub.reason || '',\n    }\n  });\n}\n\n// 4b) Neue Children pro Collection (structural + use-case)\nfor (const col of finalCols) {\n  const parentUrl = col.url || '';\n  const struct   = col.new_structural_collections || [];\n  const useCases = col.new_use_case_collections || [];\n\n  // strukturelle Unterkategorien\n  for (const s of struct) {\n    const name = s.name || '';\n    const slug = s.slug || '';\n\n    if (isAlreadyExisting(name) || isAlreadyExisting(slug)) {\n      continue; // existiert bereits als eigene Collection\n    }\n\n    result.push({\n      json: {\n        type: 'structural_child',\n        parent_url: parentUrl,\n        name,\n        slug,\n        dimension: s.dimension || '',\n        anchor_collection_url: s.anchor_collection_url || parentUrl,\n        reason: s.reason || '',\n      }\n    });\n  }\n\n  // Use-Case-Unterkategorien\n  for (const u of useCases) {\n    const name = u.name || '';\n    const slug = u.slug || '';\n\n    if (isAlreadyExisting(name) || isAlreadyExisting(slug)) {\n      continue;\n    }\n\n    result.push({\n      json: {\n        type: 'use_case_child',\n        parent_url: parentUrl,\n        name,\n        slug,\n        target_audience: u.target_audience || '',\n        main_use_cases: (u.main_use_cases || []).join(', '),\n        anchor_collection_url: u.anchor_collection_url || parentUrl,\n        reason: u.reason || '',\n      }\n    });\n  }\n}\n\nreturn result;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -192,
        2320
      ],
      "id": "e38da379-dde2-43b2-8f91-8e4aed937bd1",
      "name": "Architektur-Ergebnis parsen"
    },
    {
      "parameters": {
        "jsCode": "const item = $input.first();\nif (!item) {\n  return [];\n}\n\nconst pages = item.json.pages || [];\nconst pages_index = item.json.pages_index || [];\n\nconst CHUNK_SIZE = 20; // kannst du anpassen (10–25 ist meist safe)\n\nconst out = [];\nfor (let i = 0; i < pages.length; i += CHUNK_SIZE) {\n  const slice = pages.slice(i, i + CHUNK_SIZE);\n  out.push({\n    json: {\n      chunk_id: Math.floor(i / CHUNK_SIZE),\n      pages_chunk: slice,\n      pages_index, // kompletter Index für globale Ziele\n    },\n  });\n}\n\nreturn out;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -512,
        1472
      ],
      "id": "bd18c728-19ab-487b-896f-2762a8b1e30f",
      "name": "Chunks bauen – Interne Verlinkung (Map-Phase)"
    },
    {
      "parameters": {
        "modelId": {
          "__rl": true,
          "value": "gpt-5-mini",
          "mode": "list",
          "cachedResultName": "GPT-5-MINI"
        },
        "responses": {
          "values": [
            {
              "role": "system",
              "content": "Du bist ein erfahrener SEO-Consultant für E-Commerce-Shops.\nDeine Aufgabe: Du erstellst sinnvolle, saubere interne Verlinkungsvorschläge\nzwischen Kategorie-Seiten (Collections) und Blogartikeln.\n\nDu bekommst zwei Arrays:\n\n1) \"pages_chunk\"  → detaillierte Daten zu einem Teil der Seiten (nur dieser Chunk)\n2) \"pages_index\"  → kompakte Übersicht aller Seiten im Shop (nur Kurzinfos)\n\nJedes Element in \"pages_chunk\" hat:\n- index\n- url          (vollständige URL)\n- type         (\"collection\", \"blog\" oder \"other\")\n- title        (Seitentitel)\n- h1           (H1)\n- meta_description\n- content_snippet\n- entities     (optional)\n- attributes   (optional)\n- use_cases    (optional)\n\nJedes Element in \"pages_index\" hat:\n- index, url, type, h1, entities, use_cases\n\nWICHTIG:\n- Verwende die Felder \"url\" und \"h1\" exakt so, wie sie im Input vorkommen.\n- Erfinde keine neuen URLs oder Seitennamen.\n- Wenn du auf eine Seite Bezug nimmst, muss die \"url\" genau in \"pages_index\" vorkommen.\n- Nutze \"pages_chunk\" für tiefen Kontext, \"pages_index\" als Ziel-Liste.\n\nZiele:\n1. Nutzerführung verbessern (passende nächste Schritte anbieten)\n2. Relevanzcluster stärken\n3. Suchmaschinen & KI-Systemen klare thematische Beziehungen zeigen\n\nErzeuge nur Verlinkungen, die semantisch sinnvoll sind:\n- ähnliche oder komplementäre Themen\n- gleiche oder zusammenhängende Use Cases\n- passende Ratgeber-Artikel zu Kategorien\n- kein wahlloses Alles-mit-allem-Verlinken\n\nErlaubte Linktypen:\n- \"collection_to_collection\"\n- \"collection_to_blog\"\n- \"blog_to_collection\"\n- \"blog_to_blog\"\n\nWenn dir für eine Seite kein sinnvoller Link einfällt, erstellst du keinen Eintrag.\nVermeide sinnlose Zirkelschlüsse (A→B und B→A nur, wenn wirklich sinnvoll).\n\nDu antwortest ausschließlich mit gültigem JSON in diesem Schema:\n\n[\n  {\n    \"url\": \"https://example.com/collections/…\",\n    \"h1\": \"Exakt die H1 dieser Seite aus pages_index\",\n    \"type\": \"collection | blog | other\",\n    \"recommended_collections\": [\n      \"/collections/…\"\n    ],\n    \"recommended_blogs\": [\n      \"/blogs/…\"\n    ],\n    \"link_suggestions\": [\n      \"Konkrete Textvorschläge, wo und wie die Links eingebaut werden sollen.\"\n    ],\n    \"rationale\": \"Kurz-Begründung, warum diese Links sinnvoll sind.\"\n  }\n]\n\n- Nutze Arrays immer, auch wenn du nur einen Wert hast.\n- Kein zusätzlicher Text, kein Markdown, keine Code-Blöcke.\n- Verwende nur doppelte Anführungszeichen (\").\n- Keine Kommentare und keine überflüssigen Kommata am Ende von Listen oder Objekten."
            },
            {
              "content": "=Hier ist der aktuelle Chunk \"pages_chunk\" als JSON:\n\n{{ JSON.stringify($json.pages_chunk, null, 2) }}\n\nHier ist der komplette Seiten-Index \"pages_index\" (nur Kurzinfos):\n\n{{ JSON.stringify($json.pages_index, null, 2) }}\n\nAufgabe:\n- Analysiere nur die Seiten in \"pages_chunk\".\n- Nutze \"pages_index\", um passende Link-Ziele im gesamten Shop zu finden.\n- Gib maximal 8 Empfehlungen pro Seite zurück (nur für Seiten, wo du klare Ideen hast).\n\nAntwort:\n- Erzeuge ein JSON-Array genau im Schema aus der System-Nachricht.\n- Lass Seiten ohne sinnvolle Empfehlungen komplett weg."
            }
          ]
        },
        "builtInTools": {},
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2,
      "position": [
        -304,
        1472
      ],
      "id": "2a70f199-f3f4-49a7-af07-cae9a9e963f1",
      "name": "Interne Verlinkung (LLM)",
      "credentials": {
        "openAiApi": {
          "name": "OpenAi account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const items = $input.all();\nconst result = [];\n\nfor (const item of items) {\n  let raw = item.json.output?.[0]?.content?.[0]?.text || '';\n  if (!raw) continue;\n\n  // Code-Fences entfernen, falls das Modell doch welche ausspuckt\n  raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n\n  let data;\n  try {\n    data = JSON.parse(raw);\n  } catch (e) {\n    throw new Error('Konnte JSON aus Interlinking-Output nicht parsen:\\n' + raw);\n  }\n\n  const arr = Array.isArray(data) ? data : (data.links || []);\n  if (!Array.isArray(arr)) continue;\n\n  for (const rec of arr) {\n    const recCols  = rec.recommended_collections || [];\n    const recBlogs = rec.recommended_blogs || [];\n    const suggestions = rec.link_suggestions || [];\n\n    result.push({\n      json: {\n        url:  rec.url || '',\n        h1:   rec.h1  || '',\n        type: rec.type || '',\n\n        recommended_collections: recCols,\n        recommended_blogs:       recBlogs,\n        link_suggestions:        suggestions,\n        rationale:               rec.rationale || '',\n\n        // String-Varianten fürs Sheet\n        recommended_collections_str: recCols.join(', '),\n        recommended_blogs_str:       recBlogs.join(', '),\n        link_suggestions_str:        suggestions.join(' // '),\n      },\n    });\n  }\n}\n\nreturn result;"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -720,
        1712
      ],
      "id": "8e703083-f41f-4683-9272-719500d1c456",
      "name": "Output Aufbereitung (Map-Phase)"
    }
  ],
  "connections": {
    "Starte Semantik-Analyse": {
      "main": [
        [
          {
            "node": "Spreadsheet auslesen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Spreadsheet auslesen": {
      "main": [
        [
          {
            "node": "Daten sortieren",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daten sortieren": {
      "main": [
        [
          {
            "node": "Vorbereitung ganze Tabelle",
            "type": "main",
            "index": 0
          },
          {
            "node": "Entitäten",
            "type": "main",
            "index": 0
          },
          {
            "node": "If1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Entitäten": {
      "main": [
        [
          {
            "node": "Zuordnung der Entitäten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If": {
      "main": [
        [
          {
            "node": "Filter & Attribute",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zuordnung der Entitäten": {
      "main": [
        [
          {
            "node": "Ergebnis-Felder mappen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter & Attribute": {
      "main": [
        [
          {
            "node": "Zuordnung",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Zuordnung": {
      "main": [
        [
          {
            "node": "Felder vorbereiten1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vorbereitung ganze Tabelle": {
      "main": [
        [
          {
            "node": "Chunks bauen – Interne Verlinkung (Map-Phase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Felder vorbereiten1": {
      "main": [
        [
          {
            "node": "Filter und Attribute eintragen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Vorbereitung Collections – Architektur": {
      "main": [
        [
          {
            "node": "Chunks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Felder vorbereiten": {
      "main": [
        [
          {
            "node": "Interne Verlinkung eintragen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If1": {
      "main": [
        [
          {
            "node": "Vorbereitung Collections – Architektur",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chunks": {
      "main": [
        [
          {
            "node": "Architektur pro Chunk (Map-Phase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Architektur pro Chunk (Map-Phase)": {
      "main": [
        [
          {
            "node": "Map Ergebnisse einsammeln",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Ergebnisse einsammeln": {
      "main": [
        [
          {
            "node": "Globale Architektur (Reduce)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Globale Architektur (Reduce)": {
      "main": [
        [
          {
            "node": "Architektur-Ergebnis parsen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Chunks bauen – Interne Verlinkung (Map-Phase)": {
      "main": [
        [
          {
            "node": "Interne Verlinkung (LLM)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Output Aufbereitung (Map-Phase)": {
      "main": [
        [
          {
            "node": "Felder vorbereiten",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Interne Verlinkung (LLM)": {
      "main": [
        [
          {
            "node": "Output Aufbereitung (Map-Phase)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Architektur-Ergebnis parsen": {
      "main": [
        [
          {
            "node": "Append or update row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ergebnis-Felder mappen": {
      "main": [
        [
          {
            "node": "Entitäten eintragen",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  }
}
