{
  "name": "Social-Media-Beitraege aus Blogartikeln erzeugen (mit Freigabe)",
  "meta": {
    "beschreibung": "Liest taeglich den Blog-RSS-Feed, erkennt neue Artikel und laesst die KI daraus plattformspezifische Beitraege texten: LinkedIn sachlich-fachlich, Instagram kuerzer und direkter, jeweils mit Hashtag-Vorschlaegen und einem Bild-Prompt. Bewusste Entscheidung: Es wird nichts automatisch veroeffentlicht — alles landet als Entwurf in einem Freigabe-Sheet, die Marketing-Person bekommt eine Erinnerung. So bleibt die redaktionelle Verantwortung beim Menschen. Hinweis: KI-generierte Bilder gelten als synthetische Inhalte und sind nach Art. 50 EU AI Act zu kennzeichnen; ohne AV-Vertrag mit dem KI-Anbieter keine personenbezogenen Daten in die Prompts geben.",
    "kategorie": "Marketing & Content",
    "schwierigkeit": "mittel",
    "benoetigt": ["Blog mit RSS-Feed", "OpenAI-API (oder Ollama)", "Google Sheets", "SMTP-Postfach"],
    "quelle": "https://die-ki-agentur.de/n8n-agentur/"
  },
  "nodes": [
    {
      "parameters": {
        "content": "## Social-Media-Beitraege aus Blogartikeln\n\n**Ablauf:** Taeglich 07:30 Uhr RSS-Feed lesen → nur Artikel der letzten 24 h → KI textet LinkedIn + Instagram inkl. Hashtags → KI schlaegt Bild-Prompt vor → Laengen- und Qualitaetspruefung → Zeile im Freigabe-Sheet → Erinnerungsmail ans Marketing.\n\n**Bewusst NICHT automatisch gepostet.** Der Workflow endet bei der Freigabe. Wer direkt posten will, haengt hinter das Sheet einen eigenen Publish-Workflow — dann aber ohne redaktionelle Kontrolle.\n\n**Vor dem Start:**\n1. RSS-URL im Node `Blog-Feed lesen` eintragen (meist /feed oder /rss.xml)\n2. Google-Sheet `Freigabe` anlegen, Spalten: Erfasst am, Artikel, Link, LinkedIn-Text, LinkedIn-Hashtags, Instagram-Text, Instagram-Hashtags, Bild-Prompt, Alt-Text, Bildstil, Status, Pruefhinweis\n3. Sheet-ID und Empfaengeradresse (HIER_...) ersetzen\n4. Zeitplan an die Publikationsfrequenz anpassen — bei woechentlichem Blog reicht ein Lauf pro Woche\n5. KI-Bilder beim Posten als KI-generiert kennzeichnen (Art. 50 EU AI Act)\n\n**Betreuung:** https://die-ki-agentur.de/n8n-agentur/",
        "height": 620,
        "width": 480
      },
      "id": "note-doku",
      "name": "Kurzanleitung",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [-160, -80]
    },
    {
      "parameters": {
        "rule": {"interval": [{"field": "cronExpression", "expression": "30 7 * * 1-5"}]}
      },
      "id": "trigger-plan",
      "name": "Taeglicher Lauf",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [380, 200]
    },
    {
      "parameters": {
        "url": "HIER_BLOG_RSS_URL_EINTRAGEN",
        "options": {"ignoreSSL": false}
      },
      "id": "rss-lesen",
      "name": "Blog-Feed lesen",
      "type": "n8n-nodes-base.rssFeedRead",
      "typeVersion": 1.1,
      "position": [600, 200],
      "onError": "continueRegularOutput",
      "retryOnFail": true,
      "maxTries": 3
    },
    {
      "parameters": {
        "jsCode": "// Ein RSS-Feed liefert immer dieselben letzten Eintraege. Ohne Altersfilter\n// wuerde der Workflow jeden Tag dieselben Artikel erneut vertexten — das kostet\n// Tokens und produziert Dubletten im Freigabe-Sheet.\n// 26 statt 24 Stunden als Puffer, falls der Lauf sich mal verspaetet.\nconst maxAlterStunden = 26;\nconst jetzt = Date.now();\nconst raus = [];\n\nfor (const eintrag of $input.all()) {\n  const a = eintrag.json || {};\n  const datum = a.isoDate || a.pubDate || a.date || null;\n  const alterStunden = datum ? (jetzt - new Date(datum).getTime()) / 3600000 : 99999;\n  if (alterStunden > maxAlterStunden || alterStunden < 0) continue;\n\n  // HTML raus, Whitespace normalisieren — Roh-HTML im Prompt kostet nur Tokens.\n  const roh = String(a['content:encoded'] || a.content || a.contentSnippet || '');\n  const text = roh.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\\s+/g, ' ').trim();\n\n  // Aus einem Zweizeiler laesst sich kein sinnvoller Fachbeitrag ableiten.\n  if (text.length < 200) continue;\n\n  raus.push({\n    json: {\n      titel: a.title || 'Ohne Titel',\n      link: a.link || '',\n      veroeffentlicht: datum ? new Date(datum).toLocaleDateString('de-DE') : '',\n      auszug: text.slice(0, 4000)\n    }\n  });\n}\n\n// Leeres Ergebnis wuerde den ganzen Strang stumm abbrechen. Ein Marker-Item\n// haelt den Zweig sichtbar und macht Leerlaeufe im Execution-Log erkennbar.\nif (raus.length === 0) return [{ json: { keine_neuen_artikel: true, geprueft_am: new Date().toLocaleDateString('de-DE') } }];\nreturn raus;"
      },
      "id": "artikel-filtern",
      "name": "Neue Artikel filtern",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [820, 200]
    },
    {
      "parameters": {
        "conditions": {
          "options": {"caseSensitive": true, "version": 2},
          "conditions": [{
            "id": "hat-artikel",
            "leftValue": "={{ $json.titel }}",
            "rightValue": "",
            "operator": {"type": "string", "operation": "notEmpty", "singleValue": true}
          }],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "weiche-artikel",
      "name": "Neue Artikel vorhanden?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1040, 200]
    },
    {
      "parameters": {
        "modelId": {"__rl": true, "value": "gpt-4.1-mini", "mode": "list"},
        "messages": {"values": [
          {"role": "system", "content": "Du bist Social-Media-Redakteur eines deutschen Mittelstandsunternehmens. Du schreibst auf Deutsch, in der Sie-Form, ohne Marketing-Floskeln und ohne Clickbait. Antworte AUSSCHLIESSLICH mit einem JSON-Objekt, keine Erklaerung davor oder danach.\n\nFelder:\n- linkedin_text (string): sachlich-fachlicher Beitrag, 700 bis 1300 Zeichen, erste Zeile ein konkreter Aufhaenger ohne Emoji, danach 2-3 kurze Absaetze, am Ende eine offene Fachfrage an die Leser. Keine Hashtags im Text.\n- linkedin_hashtags (array aus 3 bis 5 strings, jeweils mit Raute, deutsch oder etablierte Fachbegriffe)\n- instagram_text (string): maximal 600 Zeichen, direkter und konkreter Ton, kurze Saetze, hoechstens zwei Emojis, keine Hashtags im Text.\n- instagram_hashtags (array aus 8 bis 12 strings mit Raute, Mischung aus Reichweiten- und Nischen-Tags)\n- kernaussage (string): der Artikel in einem Satz\n- hinweis (string oder null): Anmerkung fuer die Redaktion, etwa wenn der Artikel zu duenn fuer einen guten Beitrag ist\n\nRegeln: Erfinde keine Zahlen, Zitate oder Fakten, die nicht im Artikeltext stehen. Keine Heils- oder Erfolgsversprechen. Nenne den Artikel-Link nicht im Text, der wird separat gesetzt."},
          {"role": "user", "content": "=Titel: {{ $json.titel }}\nLink: {{ $json.link }}\nVeroeffentlicht: {{ $json.veroeffentlicht }}\n\nArtikeltext:\n{{ $json.auszug }}"}
        ]},
        "jsonOutput": true,
        "options": {"temperature": 0.4}
      },
      "id": "ki-beitraege",
      "name": "Beitraege texten",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [1260, 100]
    },
    {
      "parameters": {
        "modelId": {"__rl": true, "value": "gpt-4.1-mini", "mode": "list"},
        "messages": {"values": [
          {"role": "system", "content": "Du entwirfst Bildideen fuer Social-Media-Beitraege eines deutschen B2B-Unternehmens. Antworte AUSSCHLIESSLICH mit einem JSON-Objekt.\n\nFelder:\n- bild_prompt (string): englischer Prompt fuer einen Bildgenerator, konkret in Motiv, Perspektive, Licht und Bildausschnitt. Keine Logos, keine Markennamen, keine erkennbaren realen Personen, kein Text im Bild.\n- bild_alt_text (string): deutscher Alternativtext fuer Barrierefreiheit, ein Satz, beschreibt was zu sehen ist.\n- bildstil (string): kurze Stilangabe, zum Beispiel dokumentarische Fotografie, technische Illustration oder reduzierte Grafik.\n- eignung (string): entweder geeignet oder besser_eigenes_foto — letzteres, wenn ein echtes Foto aus dem Betrieb glaubwuerdiger waere als ein KI-Bild."},
          {"role": "user", "content": "=Thema: {{ $('Neue Artikel filtern').item.json.titel }}\nKernaussage: {{ $json.message.content.kernaussage }}"}
        ]},
        "jsonOutput": true,
        "options": {"temperature": 0.3}
      },
      "id": "ki-bildprompt",
      "name": "Bild-Prompt vorschlagen",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.8,
      "position": [1480, 100]
    },
    {
      "parameters": {
        "jsCode": "// Beide KI-Ergebnisse zusammenfuehren und gegen die harten Plattformgrenzen\n// pruefen. Ein Beitrag, der erst beim Einfuegen in LinkedIn abgeschnitten wird,\n// faellt sonst erst der Marketing-Person auf — hier steht es vorab im Sheet.\nconst LIMIT_LINKEDIN = 3000;\nconst LIMIT_INSTAGRAM = 2200;\nconst raus = [];\n\nfor (const eintrag of $input.all()) {\n  const bild = eintrag.json.message?.content ?? eintrag.json;\n  const texte = $('Beitraege texten').item.json.message?.content ?? $('Beitraege texten').item.json;\n  const artikel = $('Neue Artikel filtern').item.json;\n\n  const liText = String(texte.linkedin_text || '');\n  const igText = String(texte.instagram_text || '');\n  const liTags = Array.isArray(texte.linkedin_hashtags) ? texte.linkedin_hashtags : [];\n  const igTags = Array.isArray(texte.instagram_hashtags) ? texte.instagram_hashtags : [];\n\n  const maengel = [];\n  if (liText.length === 0) maengel.push('LinkedIn-Text fehlt');\n  if (liText.length > LIMIT_LINKEDIN) maengel.push('LinkedIn-Text ueberschreitet ' + LIMIT_LINKEDIN + ' Zeichen');\n  if (igText.length === 0) maengel.push('Instagram-Text fehlt');\n  if (igText.length > LIMIT_INSTAGRAM) maengel.push('Instagram-Text ueberschreitet ' + LIMIT_INSTAGRAM + ' Zeichen');\n  if (liTags.length < 3) maengel.push('Zu wenige LinkedIn-Hashtags');\n  if (igTags.length < 6) maengel.push('Zu wenige Instagram-Hashtags');\n  if (texte.hinweis) maengel.push('KI-Anmerkung: ' + texte.hinweis);\n  if (bild.eignung === 'besser_eigenes_foto') maengel.push('KI empfiehlt ein echtes Foto statt eines generierten Bildes');\n\n  raus.push({\n    json: {\n      erfasst_am: new Date().toLocaleDateString('de-DE'),\n      titel: artikel.titel,\n      link: artikel.link,\n      linkedin_text: liText,\n      linkedin_hashtags: liTags.join(' '),\n      linkedin_zeichen: liText.length,\n      instagram_text: igText,\n      instagram_hashtags: igTags.join(' '),\n      instagram_zeichen: igText.length,\n      bild_prompt: bild.bild_prompt || '',\n      bild_alt_text: bild.bild_alt_text || '',\n      bildstil: bild.bildstil || '',\n      status: maengel.length ? 'Entwurf - bitte pruefen' : 'Entwurf - freigabebereit',\n      pruefhinweis: maengel.length ? maengel.join('; ') : 'Keine Auffaelligkeiten'\n    }\n  });\n}\nreturn raus;"
      },
      "id": "freigabe-pruefen",
      "name": "Entwuerfe pruefen",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [1700, 100]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {"__rl": true, "value": "HIER_SHEET_ID_EINTRAGEN", "mode": "id"},
        "sheetName": {"__rl": true, "value": "Freigabe", "mode": "name"},
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Erfasst am": "={{ $json.erfasst_am }}",
            "Artikel": "={{ $json.titel }}",
            "Link": "={{ $json.link }}",
            "LinkedIn-Text": "={{ $json.linkedin_text }}",
            "LinkedIn-Hashtags": "={{ $json.linkedin_hashtags }}",
            "Instagram-Text": "={{ $json.instagram_text }}",
            "Instagram-Hashtags": "={{ $json.instagram_hashtags }}",
            "Bild-Prompt": "={{ $json.bild_prompt }}",
            "Alt-Text": "={{ $json.bild_alt_text }}",
            "Bildstil": "={{ $json.bildstil }}",
            "Status": "={{ $json.status }}",
            "Pruefhinweis": "={{ $json.pruefhinweis }}"
          }
        },
        "options": {}
      },
      "id": "sheet-freigabe",
      "name": "In Freigabe-Sheet eintragen",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.5,
      "position": [1920, 100]
    },
    {
      "parameters": {
        "fromEmail": "HIER_ABSENDER_EMAIL_EINTRAGEN",
        "toEmail": "HIER_EMAIL_MARKETING_EINTRAGEN",
        "subject": "=Social-Media-Entwurf wartet auf Freigabe: {{ $json.titel }}",
        "emailFormat": "text",
        "text": "=Guten Morgen,\n\nzum Blogartikel \"{{ $json.titel }}\" liegen neue Entwuerfe im Freigabe-Sheet.\n\nArtikel: {{ $json.link }}\nStatus: {{ $json.status }}\nPruefhinweis: {{ $json.pruefhinweis }}\n\nLinkedIn ({{ $json.linkedin_zeichen }} Zeichen):\n{{ $json.linkedin_text }}\n\nHashtags: {{ $json.linkedin_hashtags }}\n\nInstagram ({{ $json.instagram_zeichen }} Zeichen):\n{{ $json.instagram_text }}\n\nHashtags: {{ $json.instagram_hashtags }}\n\nBildidee ({{ $json.bildstil }}):\n{{ $json.bild_prompt }}\nAlt-Text: {{ $json.bild_alt_text }}\n\nBitte im Sheet pruefen, anpassen und den Status auf \"freigegeben\" setzen. Es wird nichts automatisch veroeffentlicht.\nHinweis: KI-generierte Bilder beim Posten als solche kennzeichnen.\n\nErstellt automatisch von n8n.",
        "options": {}
      },
      "id": "mail-erinnerung",
      "name": "Marketing erinnern",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2.1,
      "position": [2140, 100]
    },
    {
      "parameters": {
        "assignments": {"assignments": [
          {"id": "status", "name": "lauf_status", "value": "Keine neuen Artikel im Feed - kein Handlungsbedarf", "type": "string"},
          {"id": "datum", "name": "geprueft_am", "value": "={{ $json.geprueft_am }}", "type": "string"}
        ]},
        "options": {}
      },
      "id": "leerlauf",
      "name": "Kein neuer Artikel",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [1260, 340]
    }
  ],
  "connections": {
    "Taeglicher Lauf": {"main": [[{"node": "Blog-Feed lesen", "type": "main", "index": 0}]]},
    "Blog-Feed lesen": {"main": [[{"node": "Neue Artikel filtern", "type": "main", "index": 0}]]},
    "Neue Artikel filtern": {"main": [[{"node": "Neue Artikel vorhanden?", "type": "main", "index": 0}]]},
    "Neue Artikel vorhanden?": {"main": [[{"node": "Beitraege texten", "type": "main", "index": 0}], [{"node": "Kein neuer Artikel", "type": "main", "index": 0}]]},
    "Beitraege texten": {"main": [[{"node": "Bild-Prompt vorschlagen", "type": "main", "index": 0}]]},
    "Bild-Prompt vorschlagen": {"main": [[{"node": "Entwuerfe pruefen", "type": "main", "index": 0}]]},
    "Entwuerfe pruefen": {"main": [[{"node": "In Freigabe-Sheet eintragen", "type": "main", "index": 0}]]},
    "In Freigabe-Sheet eintragen": {"main": [[{"node": "Marketing erinnern", "type": "main", "index": 0}]]}
  },
  "settings": {"executionOrder": "v1"},
  "pinData": {}
}
