{
  "name": "SEO-Content-Briefing aus SERP-Analyse erstellen",
  "meta": {
    "beschreibung": "Die Redaktion traegt ein Keyword in ein Formular ein, der Workflow holt die aktuellen Suchergebnisse ueber eine SERP-API (Platzhalter fuer DataForSEO oder SerpAPI), wertet die Top-Treffer aus und laesst die KI daraus ein Content-Briefing bauen: Suchintention, Gliederung mit H2/H3, Fragen die der Text beantworten muss, empfohlene Wortzahl und interne Verlinkungsideen. Das Briefing landet als Zeile im Redaktions-Sheet und geht per Mail an die anfragende Person. Hinweis: Das Briefing ist ein Vorschlag auf Basis fremder Seiten — Inhalte nicht uebernehmen, sondern eigenstaendig schreiben. In das Formular gehoeren keine personenbezogenen Daten.",
    "kategorie": "Marketing & Content",
    "schwierigkeit": "mittel",
    "benoetigt": ["SERP-API (DataForSEO oder SerpAPI)", "OpenAI-API (oder Ollama)", "Google Sheets", "SMTP-Postfach"],
    "quelle": "https://die-ki-agentur.de/n8n-agentur/"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## SEO-Content-Briefing per Formular\n\n**Ablauf:** Formular mit Keyword → Eingabe normalisieren und pruefen → SERP-API abfragen → Top-10 auswerten (Titel, Snippets, Wortarten, People-also-ask) → KI baut Briefing (Suchintention, H2/H3-Gliederung, Pflichtfragen, Wortzahl) → Markdown erzeugen → Redaktions-Sheet + Mail an die anfragende Person.\n\n**Vor dem Start:**\n1. Im Node `SERP-Daten abrufen` die URL des Anbieters eintragen:\n   - DataForSEO: https://api.dataforseo.com/v3/serp/google/organic/live/advanced (Basic Auth)\n   - SerpAPI: https://serpapi.com/search (Query-Parameter api_key)\n   Zugangsdaten beim Import als Credential hinterlegen, nicht ins JSON schreiben.\n2. Google-Sheet `Content-Briefings` anlegen, Spalten: Angefragt am, Keyword, Zielmarkt, Seitentyp, Suchintention, Wortzahl, Gliederung, Pflichtfragen, Wettbewerber, Briefing (Markdown), Bearbeiter\n3. Sheet-ID und Absenderadresse (HIER_...) ersetzen\n4. Formular-URL im Team teilen — sie ist oeffentlich erreichbar, deshalb keine Kundendaten eintragen lassen\n\n**Grenze:** Der Workflow ersetzt keine Keyword-Recherche. Er beschreibt, was fuer dieses eine Keyword bereits rankt.\n\n**Betreuung:** https://die-ki-agentur.de/n8n-agentur/",
        "height": 660,
        "width": 500
      },
      "id": "note-doku",
      "name": "Kurzanleitung",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-180, -100]
    },
    {
      "parameters": {
        "path": "seo-briefing",
        "formTitle": "SEO-Content-Briefing anfordern",
        "formDescription": "Keyword eintragen, das Briefing kommt in wenigen Minuten per E-Mail. Bitte keine Kunden- oder Personendaten eintragen.",
        "formFields": {"values": [
          {"fieldLabel": "Haupt-Keyword", "placeholder": "z. B. waermepumpe foerderung 2026", "requiredField": true},
          {"fieldLabel": "Zielgruppe", "placeholder": "z. B. Hausbesitzer mit Sanierungsabsicht", "requiredField": true},
          {"fieldLabel": "Zielmarkt", "fieldType": "dropdown", "fieldOptions": {"values": [{"option": "Deutschland (de)"}, {"option": "Oesterreich (de)"}, {"option": "Schweiz (de)"}]}, "requiredField": true},
          {"fieldLabel": "Seitentyp", "fieldType": "dropdown", "fieldOptions": {"values": [{"option": "Ratgeber / Blogartikel"}, {"option": "Leistungsseite"}, {"option": "Produktseite"}, {"option": "Glossar / Definition"}, {"option": "Vergleich / Kaufberatung"}]}, "requiredField": true},
          {"fieldLabel": "E-Mail fuer das Briefing", "fieldType": "email", "requiredField": true}
        ]},
        "options": {"appendAttribution": false}
      },
      "id": "trigger-formular",
      "name": "Keyword-Formular",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.2,
      "position": [400, 220]
    },
    {
      "parameters": {
        "jsCode": "// Freitext aus einem Formular ist selten sauber: Grossschreibung, doppelte\n// Leerzeichen, versehentlich ganze Fragen statt eines Keywords. Ohne\n// Normalisierung liefert die SERP-API entweder Muell oder gar nichts.\nconst raus = [];\n\nfor (const eintrag of $input.all()) {\n  const f = eintrag.json || {};\n  const keyword = String(f['Haupt-Keyword'] || '').trim().replace(/\\s+/g, ' ').toLowerCase();\n  const zielmarkt = String(f['Zielmarkt'] || 'Deutschland (de)');\n\n  // Die SERP-APIs erwarten englische Laendernamen, das Formular zeigt deutsche.\n  let land = 'Germany';\n  if (zielmarkt.includes('Oesterreich')) land = 'Austria';\n  if (zielmarkt.includes('Schweiz')) land = 'Switzerland';\n\n  const woerter = keyword ? keyword.split(' ').length : 0;\n  const maengel = [];\n  if (keyword.length < 3) maengel.push('Keyword ist zu kurz oder leer');\n  if (woerter > 8) maengel.push('Das sieht nach einem ganzen Satz aus, nicht nach einem Keyword');\n  if (/https?:\\/\\//.test(keyword)) maengel.push('Bitte ein Keyword eintragen, keine URL');\n\n  raus.push({\n    json: {\n      keyword,\n      wortanzahl_keyword: woerter,\n      zielgruppe: String(f['Zielgruppe'] || '').trim(),\n      zielmarkt,\n      land,\n      sprachcode: 'de',\n      seitentyp: String(f['Seitentyp'] || 'Ratgeber / Blogartikel'),\n      empfaenger: String(f['E-Mail fuer das Briefing'] || '').trim(),\n      angefragt_am: new Date().toLocaleDateString('de-DE'),\n      eingabe_brauchbar: maengel.length === 0,\n      eingabe_maengel: maengel.join('; ')\n    }\n  });\n}\nreturn raus;"
      },
      "id": "eingabe-aufbereiten",
      "name": "Anfrage aufbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [620, 220]
    },
    {
      "parameters": {
        "conditions": {
          "options": {"caseSensitive": true, "version": 2},
          "conditions": [{
            "id": "brauchbar",
            "leftValue": "={{ $json.eingabe_brauchbar }}",
            "rightValue": true,
            "operator": {"type": "boolean", "operation": "true", "singleValue": true}
          }],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "weiche-eingabe",
      "name": "Keyword brauchbar?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [840, 220]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "HIER_SERP_API_URL_EINTRAGEN",
        "sendHeaders": true,
        "headerParameters": {"parameters": [{"name": "Content-Type", "value": "application/json"}]},
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify([{ keyword: $json.keyword, location_name: $json.land, language_code: $json.sprachcode, device: 'desktop', depth: 20 }]) }}",
        "options": {"timeout": 30000, "response": {"response": {"neverError": true}}}
      },
      "id": "serp-abrufen",
      "name": "SERP-Daten abrufen",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [1060, 120],
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 2
    },
    {
      "parameters": {
        "jsCode": "// Die beiden gaengigen Anbieter liefern voellig unterschiedliche Strukturen.\n// Statt den Nutzer auf einen festzunageln, lesen wir beide Formen — und liefern\n// notfalls ein leeres, aber gueltiges Ergebnis, damit die KI trotzdem ein\n// Briefing auf Basis von Keyword und Seitentyp bauen kann.\nconst anfrage = $('Anfrage aufbereiten').item.json;\nconst antwort = $input.first().json || {};\n\nlet treffer = [];\nlet fragen = [];\n\n// Variante DataForSEO\nconst dfs = antwort.tasks?.[0]?.result?.[0]?.items;\nif (Array.isArray(dfs)) {\n  treffer = dfs.filter(i => i.type === 'organic').map(i => ({ position: i.rank_group, titel: i.title, beschreibung: i.description, url: i.url, domain: i.domain }));\n  const paa = dfs.find(i => i.type === 'people_also_ask');\n  fragen = (paa?.items || []).map(i => i.title).filter(Boolean);\n}\n\n// Variante SerpAPI\nif (treffer.length === 0 && Array.isArray(antwort.organic_results)) {\n  treffer = antwort.organic_results.map(i => ({ position: i.position, titel: i.title, beschreibung: i.snippet, url: i.link, domain: (i.displayed_link || '').split('/')[0] }));\n  fragen = (antwort.related_questions || []).map(i => i.question).filter(Boolean);\n}\n\nconst top = treffer.slice(0, 10);\n\n// Grobe Laengenindikation: Wie ausfuehrlich sind die Snippets der Top-Treffer?\n// Ersetzt keine echte Wortzahlmessung, gibt der KI aber einen Anhaltspunkt.\nconst snippetLaenge = top.length\n  ? Math.round(top.reduce((s, t) => s + String(t.beschreibung || '').length, 0) / top.length)\n  : 0;\n\n// Verzeichnisse und Portale zeigen an, ob das Keyword ueberhaupt fuer eine\n// eigene Unternehmensseite erreichbar ist oder von Aggregatoren besetzt wird.\nconst portale = ['wikipedia.org', 'amazon.de', 'idealo.de', 'gelbeseiten.de', 'youtube.com', 'reddit.com', 'gutefrage.net'];\nconst portalanteil = top.length ? top.filter(t => portale.some(p => String(t.domain || '').includes(p))).length / top.length : 0;\n\nreturn [{\n  json: {\n    ...anfrage,\n    serp_verfuegbar: top.length > 0,\n    anzahl_treffer: top.length,\n    top_ergebnisse: top,\n    verwandte_fragen: fragen.slice(0, 12),\n    durchschnittliche_snippet_laenge: snippetLaenge,\n    portalanteil_prozent: Math.round(portalanteil * 100),\n    serp_hinweis: top.length === 0 ? 'Keine SERP-Daten erhalten - Briefing basiert nur auf Keyword und Seitentyp. API-Zugang und URL pruefen.' : ''\n  }\n}];"
      },
      "id": "serp-analysieren",
      "name": "Top-Ergebnisse analysieren",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1280, 120]
    },
    {
      "parameters": {
        "modelId": {"__rl": true, "value": "gpt-4.1-mini", "mode": "list"},
        "messages": {"values": [
          {"role": "system", "content": "Du bist SEO-Redaktionsleiter und erstellst Content-Briefings fuer deutschsprachige Texte. Antworte AUSSCHLIESSLICH mit einem JSON-Objekt, keine Erklaerung.\n\nFelder:\n- suchintention (string): informational, kommerziell, transaktional oder navigational, mit einem Satz Begruendung aus den Suchergebnissen\n- nutzerfrage (string): die eine Frage, die der Suchende wirklich beantwortet haben will\n- empfohlene_wortzahl (number): realistische Zielwortzahl fuer den deutschen Text\n- wortzahl_begruendung (string)\n- gliederung (array von Objekten mit h2 (string) und h3 (array aus strings)): logischer Aufbau, 5 bis 9 H2-Abschnitte, deutsche Zwischenueberschriften, keine generischen Floskeln wie Fazit ohne Inhalt\n- pflichtfragen (array aus strings): Fragen, die der Text beantworten muss, gespeist aus den verwandten Fragen und den Luecken der Top-Treffer\n- pflichtbegriffe (array aus strings): Fachbegriffe und Entitaeten, die im Text vorkommen sollten\n- luecken (array aus strings): was die aktuellen Top-Treffer nicht liefern und wo eigener Mehrwert entsteht\n- einstiegsabsatz (string): Vorschlag fuer die ersten zwei bis drei Saetze\n- meta_title (string, maximal 60 Zeichen)\n- meta_description (string, maximal 155 Zeichen)\n- interne_verlinkung (array aus strings): Themen, auf die intern verlinkt werden sollte\n- warnung (string oder null): etwa wenn das Keyword von Portalen besetzt ist oder keine SERP-Daten vorlagen\n\nRegeln: Uebernimm keine Formulierungen aus den Suchergebnissen, sie dienen nur der Analyse. Erfinde keine Zahlen, Preise oder Foerdersaetze. Wenn keine SERP-Daten vorliegen, baue das Briefing aus Keyword, Zielgruppe und Seitentyp und setze eine entsprechende Warnung."},
          {"role": "user", "content": "=Keyword: {{ $json.keyword }}\nZielgruppe: {{ $json.zielgruppe }}\nZielmarkt: {{ $json.zielmarkt }}\nGeplanter Seitentyp: {{ $json.seitentyp }}\nSERP-Daten vorhanden: {{ $json.serp_verfuegbar }}\nAnteil Portale/Verzeichnisse in den Top-Treffern: {{ $json.portalanteil_prozent }} Prozent\nDurchschnittliche Snippet-Laenge: {{ $json.durchschnittliche_snippet_laenge }} Zeichen\n\nTop-Ergebnisse:\n{{ JSON.stringify($json.top_ergebnisse, null, 1) }}\n\nVerwandte Fragen aus der Suche:\n{{ JSON.stringify($json.verwandte_fragen) }}"}
        ]},
        "jsonOutput": true,
        "options": {"temperature": 0}
      },
      "id": "ki-briefing",
      "name": "Content-Briefing erstellen",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [1500, 120]
    },
    {
      "parameters": {
        "jsCode": "// Die KI liefert Struktur, kein lesbares Dokument. Redakteure arbeiten aber\n// nicht mit JSON — deshalb hier ein Markdown-Briefing, das sich direkt in\n// Google Docs, Notion oder das CMS kopieren laesst.\nconst analyse = $('Top-Ergebnisse analysieren').item.json;\nconst b = $input.first().json.message?.content ?? $input.first().json;\n\nconst zeilen = [];\nzeilen.push('# Content-Briefing: ' + analyse.keyword);\nzeilen.push('');\nzeilen.push('- Angefragt am: ' + analyse.angefragt_am);\nzeilen.push('- Zielgruppe: ' + analyse.zielgruppe);\nzeilen.push('- Zielmarkt: ' + analyse.zielmarkt);\nzeilen.push('- Seitentyp: ' + analyse.seitentyp);\nzeilen.push('- Suchintention: ' + (b.suchintention || 'unklar'));\nzeilen.push('- Kernfrage der Nutzer: ' + (b.nutzerfrage || '-'));\nzeilen.push('- Empfohlene Wortzahl: ' + (b.empfohlene_wortzahl || '-') + ' (' + (b.wortzahl_begruendung || '') + ')');\nzeilen.push('');\nzeilen.push('## Meta-Angaben');\nzeilen.push('- Title: ' + (b.meta_title || '-'));\nzeilen.push('- Description: ' + (b.meta_description || '-'));\nzeilen.push('');\nzeilen.push('## Einstieg');\nzeilen.push(b.einstiegsabsatz || '-');\nzeilen.push('');\nzeilen.push('## Gliederung');\nconst gliederung = Array.isArray(b.gliederung) ? b.gliederung : [];\nfor (const abschnitt of gliederung) {\n  zeilen.push('### ' + (abschnitt.h2 || '-'));\n  for (const h3 of (abschnitt.h3 || [])) zeilen.push('- ' + h3);\n  zeilen.push('');\n}\nconst liste = (titel, werte) => {\n  zeilen.push('## ' + titel);\n  const w = Array.isArray(werte) ? werte : [];\n  if (w.length === 0) zeilen.push('- (keine Angabe)');\n  for (const x of w) zeilen.push('- ' + x);\n  zeilen.push('');\n};\nliste('Fragen, die beantwortet werden muessen', b.pflichtfragen);\nliste('Pflichtbegriffe und Entitaeten', b.pflichtbegriffe);\nliste('Luecken der aktuellen Top-Treffer', b.luecken);\nliste('Interne Verlinkung pruefen', b.interne_verlinkung);\n\nzeilen.push('## Aktuelle Top-Treffer');\nfor (const t of (analyse.top_ergebnisse || []).slice(0, 10)) {\n  zeilen.push('- ' + t.position + '. ' + t.titel + ' (' + t.domain + ')');\n}\nzeilen.push('');\nzeilen.push('> Hinweis: KI-generierter Vorschlag auf Basis oeffentlicher Suchergebnisse. Inhalte eigenstaendig recherchieren und formulieren, keine Passagen aus den Wettbewerberseiten uebernehmen.');\n\nconst warnungen = [];\nif (analyse.serp_hinweis) warnungen.push(analyse.serp_hinweis);\nif (b.warnung) warnungen.push(b.warnung);\nif (analyse.portalanteil_prozent >= 50) warnungen.push('Mehr als die Haelfte der Top-Treffer sind Portale oder Verzeichnisse - eigene Rankingchancen pruefen.');\n\nreturn [{\n  json: {\n    keyword: analyse.keyword,\n    zielmarkt: analyse.zielmarkt,\n    seitentyp: analyse.seitentyp,\n    empfaenger: analyse.empfaenger,\n    angefragt_am: analyse.angefragt_am,\n    suchintention: b.suchintention || '',\n    empfohlene_wortzahl: b.empfohlene_wortzahl || 0,\n    gliederung_kurz: gliederung.map(a => a.h2).join(' | '),\n    pflichtfragen_kurz: (b.pflichtfragen || []).join(' | '),\n    wettbewerber: (analyse.top_ergebnisse || []).slice(0, 5).map(t => t.domain).join(', '),\n    warnungen: warnungen.join(' '),\n    briefing_markdown: zeilen.join('\\n')\n  }\n}];"
      },
      "id": "briefing-formatieren",
      "name": "Briefing als Dokument aufbereiten",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1720, 120]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {"__rl": true, "value": "HIER_SHEET_ID_EINTRAGEN", "mode": "id"},
        "sheetName": {"__rl": true, "value": "Content-Briefings", "mode": "name"},
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Angefragt am": "={{ $json.angefragt_am }}",
            "Keyword": "={{ $json.keyword }}",
            "Zielmarkt": "={{ $json.zielmarkt }}",
            "Seitentyp": "={{ $json.seitentyp }}",
            "Suchintention": "={{ $json.suchintention }}",
            "Wortzahl": "={{ $json.empfohlene_wortzahl }}",
            "Gliederung": "={{ $json.gliederung_kurz }}",
            "Pflichtfragen": "={{ $json.pflichtfragen_kurz }}",
            "Wettbewerber": "={{ $json.wettbewerber }}",
            "Briefing (Markdown)": "={{ $json.briefing_markdown }}",
            "Bearbeiter": "={{ $json.empfaenger }}",
            "Hinweise": "={{ $json.warnungen }}"
          }
        },
        "options": {}
      },
      "id": "sheet-briefing",
      "name": "Briefing im Sheet ablegen",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1940, 120]
    },
    {
      "parameters": {
        "fromEmail": "HIER_ABSENDER_EMAIL_EINTRAGEN",
        "toEmail": "={{ $json.empfaenger }}",
        "subject": "=Content-Briefing: {{ $json.keyword }}",
        "emailFormat": "text",
        "text": "=Hallo,\n\nhier das Briefing zum Keyword \"{{ $json.keyword }}\" ({{ $json.zielmarkt }}, {{ $json.seitentyp }}).\n\nHinweise: {{ $json.warnungen || 'keine' }}\n\n{{ $json.briefing_markdown }}\n\nDas Briefing liegt zusaetzlich im Sheet Content-Briefings.\nErstellt automatisch von n8n.",
        "options": {}
      },
      "id": "mail-briefing",
      "name": "Briefing versenden",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [2160, 120]
    },
    {
      "parameters": {
        "assignments": {"assignments": [
          {"id": "grund", "name": "abbruchgrund", "value": "={{ $json.eingabe_maengel }}", "type": "string"},
          {"id": "aktion", "name": "naechster_schritt", "value": "Formular erneut ausfuellen - ein Keyword aus zwei bis fuenf Woertern, keine ganze Frage und keine URL", "type": "string"},
          {"id": "kw", "name": "eingegeben", "value": "={{ $json.keyword }}", "type": "string"}
        ]},
        "options": {}
      },
      "id": "eingabe-abbruch",
      "name": "Eingabe unbrauchbar",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1060, 380]
    }
  ],
  "connections": {
    "Keyword-Formular": {"main": [[{"node": "Anfrage aufbereiten", "type": "main", "index": 0}]]},
    "Anfrage aufbereiten": {"main": [[{"node": "Keyword brauchbar?", "type": "main", "index": 0}]]},
    "Keyword brauchbar?": {"main": [[{"node": "SERP-Daten abrufen", "type": "main", "index": 0}], [{"node": "Eingabe unbrauchbar", "type": "main", "index": 0}]]},
    "SERP-Daten abrufen": {"main": [[{"node": "Top-Ergebnisse analysieren", "type": "main", "index": 0}]]},
    "Top-Ergebnisse analysieren": {"main": [[{"node": "Content-Briefing erstellen", "type": "main", "index": 0}]]},
    "Content-Briefing erstellen": {"main": [[{"node": "Briefing als Dokument aufbereiten", "type": "main", "index": 0}]]},
    "Briefing als Dokument aufbereiten": {"main": [[{"node": "Briefing im Sheet ablegen", "type": "main", "index": 0}]]},
    "Briefing im Sheet ablegen": {"main": [[{"node": "Briefing versenden", "type": "main", "index": 0}]]}
  },
  "settings": {"executionOrder": "v1"},
  "pinData": {}
}
