Files
Skrift-Auto-Article/workflows/pruefen-und-importieren.json

1657 lines
68 KiB
JSON

{
"name": "pruefen-und-importieren",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-460,
100
],
"id": "9d7b1805-8faf-4e0b-90d0-faedab546436",
"name": "Manuell starten"
},
{
"parameters": {
"httpMethod": "POST",
"path": "pruefen-und-importieren",
"responseMode": "onReceived",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-460,
280
],
"id": "ceaa8ccf-b4a1-4a0d-a173-b9ed63d44833",
"name": "Webhook Start",
"webhookId": "ddaf87b7-a52e-44ad-8fc9-b0560671eab3"
},
{
"parameters": {
"mode": "manual",
"assignments": {
"assignments": [
{
"id": "c37716f4-15ed-4a09-83f1-7841ff2a4efb",
"name": "nw_base",
"value": "https://app.neuronwriter.com/neuron-api/0.5/writer",
"type": "string"
},
{
"id": "a30eeffc-30a1-42fa-bb43-4662e503872b",
"name": "gitea_base",
"value": "http://gitea:3000",
"type": "string"
},
{
"id": "db835d6c-31f9-4ac1-95ff-096bd685aa5e",
"name": "gitea_owner",
"value": "lucas.orth",
"type": "string"
},
{
"id": "d3f9c73d-bcf1-4ed7-8047-f9eb3968f403",
"name": "gitea_repo",
"value": "Skrift-Auto-Article",
"type": "string"
},
{
"id": "c6a32b0a-032f-498e-aea8-04587961668b",
"name": "branch",
"value": "main",
"type": "string"
},
{
"id": "c7e60ffd-132b-43b4-b3e3-7fd0f66b6546",
"name": "artikel_ordner",
"value": "artikel",
"type": "string"
},
{
"id": "d379f9fc-d1ba-4ac5-8494-1af6068cefca",
"name": "brief_ordner",
"value": "briefe",
"type": "string"
},
{
"id": "46d52c78-8a51-4c5b-b893-ed79a53753a8",
"name": "nachbessern_ordner",
"value": "nachbessern",
"type": "string"
},
{
"id": "ff44870f-d811-402a-afaf-61053ca2bb83",
"name": "status_pfad",
"value": "status-importe.json",
"type": "string"
},
{
"id": "ea4a7974-642d-49df-b419-7cb2007893d6",
"name": "wp_base",
"value": "https://skrift.de",
"type": "string"
},
{
"id": "354bf17a-52d5-4483-9cd6-68721a9c2a23",
"name": "score_ziel",
"value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}",
"type": "number"
},
{
"id": "388a5e43-5ab7-42f2-b3d2-438f72ff58e6",
"name": "pause_zwischen_artikeln",
"value": 5,
"type": "number"
},
{
"id": "9b2fa315-8213-4609-a2e5-be0936e24f20",
"name": "elementor_ausgabe",
"value": false,
"type": "boolean"
},
{
"id": "ffca6f3b-e391-4ca7-adb8-49017472dc7b",
"name": "faq_ueberschrift",
"value": "Häufige Fragen",
"type": "string"
},
{
"id": "2e8ae0e2-c6ea-4495-9fd5-1ad24c2b5dd8",
"name": "benachrichtigung_an",
"value": "privat@lucas-orth.de",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-240,
190
],
"id": "d6943dd4-a080-471e-8d8d-281d2b49c91d",
"name": "Konfiguration"
},
{
"parameters": {
"method": "GET",
"url": "={{ $json.gitea_base }}/api/v1/repos/{{ $json.gitea_owner }}/{{ $json.gitea_repo }}/contents/{{ $json.artikel_ordner }}?ref={{ $json.branch }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
-20,
190
],
"id": "21951ce8-26f3-4473-8051-4e232c228e89",
"name": "Artikelordner lesen",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst antwort = $input.first().json;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst ka = K(antwort);\nif (antwort.statusCode === 404) {\n return [{ json: { leer: true, grund: 'Ordner \"artikel\" existiert noch nicht im Repo.' } }];\n}\nif (antwort.statusCode !== 200) {\n throw new Error('Artikelordner konnte nicht gelesen werden (HTTP ' + antwort.statusCode\n + '). Antwort: ' + JSON.stringify(ka).slice(0, 400));\n}\nconst dateien = (ka || []).filter(e =>\n e.type === 'file' && /^artikel-.+\\.json$/.test(e.name));\n\nif (!dateien.length) {\n return [{ json: { leer: true, grund: 'Keine Dateien nach Muster artikel-<slug>.json gefunden.' } }];\n}\nreturn dateien.map(d => ({ json: {\n name: d.name, pfad: d.path, sha: d.sha,\n slug: d.name.replace(/^artikel-/, '').replace(/\\.json$/, ''),\n}}));\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
200,
190
],
"id": "79588bca-c88b-4f26-8344-1f1aaabbf7b7",
"name": "Artikeldateien filtern"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "a9e1753d-2159-4f99-aa0e-b9acaaf0b413",
"leftValue": "={{ $json.leer === true }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
420,
190
],
"id": "7f5cd106-1cbe-4fd2-9a15-3d31ceef74c0",
"name": "Artikel vorhanden?"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
640,
40
],
"id": "66ea5319-b958-4cde-924a-ff101321bb3e",
"name": "Nichts zu tun"
},
{
"parameters": {
"batchSize": 1,
"options": {
"reset": false
}
},
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
640,
320
],
"id": "59e68a48-9e3a-499c-91a9-2d32ba8a6420",
"name": "Artikel nacheinander"
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $json.pfad }}?ref={{ $('Konfiguration').first().json.branch }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
900,
420
],
"id": "a3787725-913a-4b49-8177-b40a29322c78",
"name": "Artikeldatei laden",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
},
"onError": "continueErrorOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst a = $('Artikel nacheinander').first().json;\nreturn [{ json: { slug: a.slug, ergebnis: 'fehler', stufe: 'artikel-laden',\n meldung: 'Artikeldatei ' + a.pfad + ' konnte nicht geladen werden: '\n + JSON.stringify($input.first().json).slice(0, 400) }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
640
],
"id": "25cb559a-6047-4766-bebd-f090d36d44a2",
"name": "Fehler Artikeldatei",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.brief_ordner }}/brief-{{ $('Artikel nacheinander').first().json.slug }}.json?ref={{ $('Konfiguration').first().json.branch }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1120,
420
],
"id": "d3ea9547-61df-4d13-9dd4-6ffde02be40b",
"name": "Brief laden",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "// Stilpruefung: findet typische Muster maschinell wirkender Texte im Deutschen.\n// Wird von gen_wf2.py in den Code-Node \"Artikel pruefen\" eingebettet und ist\n// lokal mit node testbar. Liefert Hinweise, blockiert nichts.\nfunction stilpruefung(artikel) {\n const hinweise = [];\n const add = (art, stelle) => hinweise.push({ art, stelle: String(stelle).slice(0, 140) });\n const ohneTags = (h) => String(h || '').replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/\\s+/g, ' ').trim();\n\n const html = String(artikel.content_html || '');\n const absaetze = [...html.matchAll(/<p>([\\s\\S]*?)<\\/p>/g)].map(m => ohneTags(m[1])).filter(Boolean);\n const volltext = [ohneTags(artikel.kurzantwort), ohneTags(html),\n ...(Array.isArray(artikel.faq) ? artikel.faq.map(f => ohneTags(f.antwort)) : [])].join(' ');\n\n // 1) Floskeln und Formeln, die in generierten Texten gehaeuft auftreten\n const formeln = [\n ['genau das', 'Formel'], ['genau diese', 'Formel'], ['genau daran', 'Formel'], ['genau so', 'Formel'],\n ['der grund ist', 'Formel'], ['nicht nur', 'Formel \"nicht nur, sondern auch\"'],\n ['entscheidend ist', 'Formel'], ['entscheidend für', 'Formel'], ['größten hebel', 'Formel'],\n ['den unterschied', 'Formel'], ['am ende des tages', 'Floskel'], ['unterm strich', 'Floskel'],\n ['kurz gesagt', 'Floskel'], ['mit anderen worten', 'Floskel'], ['die gute nachricht', 'Floskel'],\n ['spielt eine rolle', 'Floskel'], ['es lohnt sich', 'Floskel'], ['egal ob', 'Formel'],\n ['in diesem artikel', 'Meta-Satz'], ['wir zeigen ihnen', 'Meta-Satz'], ['fazit', 'Zusammenfassung'],\n ['tatsächlich', 'Fuellwort'], ['letztlich', 'Fuellwort'], ['schlicht', 'Fuellwort'], ['simpel', 'Fuellwort'],\n ['erfahrungsgemäß', 'unbelegte Behauptung'], ['in aller regel', 'unbelegte Behauptung'],\n ['studien zeigen', 'unbelegte Behauptung'], ['nachweislich', 'unbelegte Behauptung'],\n ['bekanntlich', 'Floskel'],\n ];\n const klein = volltext.toLowerCase();\n formeln.forEach(([f, art]) => {\n let i = klein.indexOf(f);\n while (i >= 0) {\n add(art + ': \"' + f + '\"', '…' + volltext.slice(Math.max(0, i - 40), i + f.length + 50) + '…');\n i = klein.indexOf(f, i + f.length);\n }\n });\n\n // 2) Aufzaehlungen im Fliesstext\n if (/\\berstens\\b[\\s\\S]*\\bzweitens\\b/i.test(volltext)) add('Aufzaehlung \"Erstens, zweitens\"', 'Im Fliesstext lieber eine echte Liste oder zwei Saetze');\n const dreier = volltext.match(/\\b\\w+(?:ig|lich|ell|bar|iv|sam)\\b, \\w+(?:ig|lich|ell|bar|iv|sam)\\b und \\w+(?:ig|lich|ell|bar|iv|sam)\\b/gi) || [];\n dreier.forEach(d => add('Dreierreihe aus Adjektiven', d));\n\n // 3) Rhetorische Frage im Absatz, direkt beantwortet\n absaetze.forEach(a => {\n const m = a.match(/[^.!?]{8,}\\?\\s+[A-ZÄÖÜ][^.!?]{3,}[.!]/);\n if (m) add('Rhetorische Frage mit Sofortantwort', m[0]);\n });\n\n // 4) Satzlaengen: gleichfoermiger Rhythmus\n const saetze = ohneTags(html).split(/(?<=[.!?])\\s+/).map(s => s.trim())\n .filter(s => s.split(' ').length >= 3);\n const laengen = saetze.map(s => s.split(' ').length);\n if (laengen.length >= 8) {\n const mittel = laengen.reduce((x, y) => x + y, 0) / laengen.length;\n const sd = Math.sqrt(laengen.reduce((x, y) => x + (y - mittel) ** 2, 0) / laengen.length);\n if (sd < 4.5) add('Gleichfoermige Satzlaengen', `Durchschnitt ${mittel.toFixed(1)} Woerter, Streuung nur ${sd.toFixed(1)}. Kurze und lange Saetze mischen.`);\n for (let i = 0; i + 4 <= laengen.length; i++) {\n const fenster = laengen.slice(i, i + 4);\n if (Math.max(...fenster) - Math.min(...fenster) <= 3) {\n add('Vier Saetze gleicher Laenge hintereinander', saetze[i]);\n i += 3;\n }\n }\n }\n\n // 5) Gleiche Satzanfaenge\n for (let i = 0; i + 3 <= saetze.length; i++) {\n const w = saetze.slice(i, i + 3).map(s => s.split(' ')[0].toLowerCase());\n if (w[0] === w[1] && w[1] === w[2] && w[0].length > 2) { add('Drei Saetze mit gleichem Anfang', saetze[i]); i += 2; }\n }\n const anfaenge = {};\n absaetze.forEach(a => { const k = a.split(' ').slice(0, 2).join(' ').toLowerCase(); anfaenge[k] = (anfaenge[k] || 0) + 1; });\n Object.entries(anfaenge).filter(([, n]) => n >= 3).forEach(([k, n]) => add('Absatzanfaenge wiederholen sich', `${n} Absaetze beginnen mit \"${k}\"`));\n\n // 6) Gleich lange Absaetze\n const al = absaetze.map(a => a.split(' ').length).filter(n => n >= 12);\n if (al.length >= 5) {\n const m = al.reduce((x, y) => x + y, 0) / al.length;\n const sd = Math.sqrt(al.reduce((x, y) => x + (y - m) ** 2, 0) / al.length);\n if (sd / m < 0.25) add('Absaetze fast gleich lang', `Durchschnitt ${m.toFixed(0)} Woerter, kaum Abweichung. Einen Absatz bewusst kurz halten.`);\n }\n\n // 7) Praxiswissen: ohne eigene Stichpunkte bleibt der Text generisch\n if (!Array.isArray(artikel.praxis) || artikel.praxis.length === 0) {\n add('Kein Praxiswissen hinterlegt', 'Feld \"praxis\" in artikel-index.json fuellen, dann den Artikel darauf aufbauen');\n }\n\n // 8) Interne Links: primaer auf Business-Leistungsseiten\n const B = 'https://skrift.de/';\n const BUSINESS = ['handgeschriebene-businessbriefe', 'handgeschriebene-postkarten',\n 'unterschriftenservice', 'automatisierte-follow-ups'];\n const NEUTRAL = ['papier-schriftmuster', 'kontakt'];\n const PRIVAT = ['handschrift-fuer-privatpersonen', 'handgeschriebene-private-briefe',\n 'handgeschriebene-private-postkarten'];\n const slugVon = (u) => String(u || '').replace(/^https?:\\/\\/(www\\.)?skrift\\.de\\/?/, '').replace(/\\/.*$/, '');\n const links = [...html.matchAll(/<a\\s[^>]*href=\"([^\"]+)\"/g)].map(m => m[1]);\n const intern = links.filter(u => /^https?:\\/\\/(www\\.)?skrift\\.de/.test(u) || u.startsWith('/'));\n const slugs = intern.map(u => u.startsWith('/') ? u.replace(/^\\/|\\/.*$/g, '') : slugVon(u));\n\n const cta = slugVon(artikel.cta_ziel);\n if (!BUSINESS.includes(cta)) add('Links: CTA-Ziel ist keine Business-Leistungsseite', `cta_ziel \"${artikel.cta_ziel || 'leer'}\", erlaubt: ${BUSINESS.join(', ')}`);\n const ctaAnzahl = slugs.filter(s => s === cta).length;\n if (ctaAnzahl < 2) add('Links: CTA-Ziel zu selten verlinkt', `${ctaAnzahl}x im Text, mindestens 2x (im Text und im Abschluss-CTA)`);\n const bus = slugs.filter(s => BUSINESS.includes(s));\n const priv = slugs.filter(s => PRIVAT.includes(s));\n if (bus.length < 2) add('Links: zu wenige Business-Links', `${bus.length} Link(s) auf Business-Seiten, mindestens 2`);\n if (priv.length > 1) add('Links: zu viele Privatkunden-Links', `${priv.length} Links, hoechstens einer im Block \"Auch für Privatpersonen.\"`);\n if (priv.length) {\n const block = (html.split(/<h3>Auch für Privatpersonen\\.<\\/h3>/)[1] || '').split(/<h2/)[0];\n const imBlock = PRIVAT.some(s => block.includes(s));\n if (!imBlock) add('Links: Privatkunden-Link ausserhalb des Privat-Blocks', 'Privat-Links nur im Block \"Auch für Privatpersonen.\"');\n }\n if (priv.length >= bus.length && priv.length > 0) add('Links: Privat-Links nicht in der Minderheit', `${bus.length} Business gegen ${priv.length} Privat`);\n const unbekannt = [...new Set(slugs.filter(s => s && !BUSINESS.includes(s) && !NEUTRAL.includes(s) && !PRIVAT.includes(s)))];\n unbekannt.forEach(s => add('Links: kein Linkziel fuer Ratgeber', `/${s}/ (z. B. Dankeseite, Rechtstexte)`));\n\n return hinweise;\n}\n\n\nconst meta = $('Artikel nacheinander').first().json;\nconst artRoh = $('Artikeldatei laden').first().json;\nconst briefAntwort = $input.first().json;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kb = K(briefAntwort);\n\nlet artikel;\ntry { artikel = JSON.parse(Buffer.from(artRoh.content, 'base64').toString('utf8')); }\ncatch (e) { throw new Error('artikel-' + meta.slug + '.json ist kein gueltiges JSON: ' + e.message); }\n\nconst pflicht = ['slug', 'titel', 'query_id', 'kategorie', 'content_html'];\nconst fehlt = pflicht.filter(f => !artikel[f]);\nif (fehlt.length) {\n throw new Error('artikel-' + meta.slug + '.json fehlen Pflichtfelder: ' + fehlt.join(', ')\n + '. Erwartetes Format siehe README Abschnitt \"Vertrag Cowork -> Workflow 2\".');\n}\nif (artikel.slug !== meta.slug) {\n throw new Error('Slug im Dateinamen (' + meta.slug + ') und im JSON (' + artikel.slug + ') stimmen nicht ueberein.');\n}\n\nlet brief = null;\nif (briefAntwort.statusCode === 200 && kb && kb.content) {\n try { brief = JSON.parse(Buffer.from(kb.content, 'base64').toString('utf8')); }\n catch (e) { brief = null; }\n}\n\n// Preise muessen als Shortcode stehen, nie ausgeschrieben.\nconst preisTreffer = [];\nconst re = /(\\d{1,3}(?:[.,]\\d{1,2})?)\\s*(?:€|EUR|Euro)/gi;\nlet m;\nwhile ((m = re.exec(String(artikel.content_html || ''))) !== null) {\n preisTreffer.push(m[0].trim());\n}\nconst shortcodes = (String(artikel.content_html || '').match(/\\[skrift_preis[^\\]]*\\]/g) || []);\n\n// NeuronWriter bewertet nur den uebergebenen Text. Kurzantwort und FAQ stehen\n// aber auf der fertigen Seite, also gehoeren sie in die Bewertung. Der gespeicherte\n// Artikel bleibt unveraendert, das hier ist nur der Pruefkoerper.\nconst faqText = (Array.isArray(artikel.faq) ? artikel.faq : [])\n .map(f => `<h3>${f.frage || ''}</h3><p>${f.antwort || ''}</p>`).join('\\n');\nconst bewertungs_html = [\n artikel.kurzantwort ? `<p>${artikel.kurzantwort}</p>` : '',\n artikel.content_html || '',\n faqText ? `<h2>Häufige Fragen</h2>\\n${faqText}` : '',\n].filter(Boolean).join('\\n');\n\nconst praxis = Array.isArray(artikel.praxis) && artikel.praxis.length ? artikel.praxis\n : ((brief && brief.artikel && brief.artikel.praxis) || []);\n\nreturn [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief,\n bewertungs_html,\n stil_hinweise: stilpruefung({ ...artikel, praxis }),\n preis_warnung: preisTreffer.length\n ? `${preisTreffer.length} ausgeschriebene(r) Preis(e) im Text: ${[...new Set(preisTreffer)].slice(0,5).join(', ')}. `\n + 'Bitte durch [skrift_preis key=\"...\"] ersetzen.'\n : null,\n preis_shortcodes: shortcodes.length } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
420
],
"id": "01b083a8-acdd-4964-80fc-26770a1dcecb",
"name": "Artikel pruefen"
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Konfiguration').first().json.nw_base }}/evaluate-content",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1560,
420
],
"id": "939d7498-7c06-49a7-b96b-8ed0095edf94",
"name": "Score ermitteln",
"credentials": {
"httpHeaderAuth": {
"id": "U23n2O980GKWocWq",
"name": "NeuronWriter API"
}
},
"onError": "continueErrorOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst p = $('Artikel pruefen').first().json;\nreturn [{ json: { slug: p.slug, ergebnis: 'fehler', stufe: 'evaluate-content',\n meldung: 'NeuronWriter /evaluate-content fehlgeschlagen. Haeufigste Ursache: query_id \"'\n + p.artikel.query_id + '\" existiert nicht oder gehoert zu einem anderen Projekt. Antwort: '\n + JSON.stringify($input.first().json).slice(0, 400) }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1560,
640
],
"id": "e3135c57-8738-4d82-914c-8813d3916dcc",
"name": "Fehler Score",
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst p = $('Artikel pruefen').first().json;\nconst e = $input.first().json;\nconst ziel = $('Konfiguration').first().json.score_ziel;\n\nif (typeof e.content_score !== 'number') {\n throw new Error('NeuronWriter lieferte kein numerisches content_score. Antwort: '\n + JSON.stringify(e).slice(0, 300));\n}\nreturn [{ json: { ...p, score: e.content_score, score_ziel: ziel, bestanden: e.content_score >= ziel } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
420
],
"id": "b1ea4d99-d731-4468-8525-b1af09e0acc3",
"name": "Score auswerten"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "851f792a-479c-4d4b-8801-15010f03be39",
"leftValue": "={{ $json.bestanden }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2000,
420
],
"id": "7187c432-a84a-4a61-b653-a864626f9c0b",
"name": "Score erreicht?"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\n// /evaluate-content liefert nur {status, content_score}, keine Begriffsliste.\n// Die Luecken werden deshalb gegen den Brief aus Workflow 1 selbst berechnet.\nconst s = $input.first().json;\nconst brief = s.brief;\n\nconst text = String(s.bewertungs_html || s.artikel.content_html || '')\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/&nbsp;/g, ' ')\n .replace(/\\s+/g, ' ')\n .toLowerCase();\n\nconst zaehle = (begriff) => {\n const b = String(begriff).toLowerCase().trim();\n if (!b) return 0;\n return text.split(b).length - 1;\n};\n\nconst pruefe = (liste) => (liste || []).map(e => {\n const ist = zaehle(e.begriff);\n const min = e.min ?? 1;\n return { begriff: e.begriff, soll_min: min, soll_max: e.max ?? null, ist,\n fehlt: ist < min, luecke: Math.max(0, min - ist) };\n});\n\nlet pflicht = [], erweitert = [], fragen_offen = [];\nif (brief) {\n pflicht = pruefe(brief.begriffe_pflicht).filter(x => x.fehlt);\n erweitert = pruefe(brief.begriffe_erweitert).filter(x => x.fehlt);\n fragen_offen = (brief.fragen || [])\n .filter(f => f.gewicht && f.gewicht >= 6)\n .filter(f => {\n const kern = String(f.frage).toLowerCase()\n .replace(/[?.,!]/g, '').split(' ').filter(w => w.length > 5).slice(0, 3);\n return kern.length ? !kern.every(w => text.includes(w)) : false;\n })\n .map(f => ({ frage: f.frage, gewicht: f.gewicht }));\n}\n\nconst woerter = text.split(' ').filter(Boolean).length;\n\nconst bericht = {\n slug: s.slug,\n titel: s.artikel.titel,\n query_id: s.artikel.query_id,\n score: s.score,\n score_ziel: s.score_ziel,\n geprueft_am: new Date().toISOString(),\n brief_vorhanden: s.brief_vorhanden,\n wortzahl: { ist: woerter, ziel: brief ? brief.wortzahl_ziel : null,\n differenz: brief && brief.wortzahl_ziel ? woerter - brief.wortzahl_ziel : null },\n fehlende_pflichtbegriffe: pflicht,\n fehlende_zusatzbegriffe: erweitert,\n unbeantwortete_fragen: fragen_offen,\n stil_hinweise: s.stil_hinweise || [],\n hinweis: s.brief_vorhanden\n ? 'Begriffe gegen briefe/brief-' + s.slug + '.json abgeglichen. Zaehlung als Teilstring, Komposita zaehlen mit.'\n : 'Kein Brief gefunden, daher nur der Score. Workflow 1 fuer diesen Slug erneut laufen lassen.',\n regel: 'Pflichtbegriffe sind verbindlich. Zusatzbegriffe nur einbauen, wenn der Satz dadurch nicht schlechter wird. Keine Saetze nur fuer den Score.',\n};\n\nconst konf = $('Konfiguration').first().json;\nreturn [{ json: { slug: s.slug, score: s.score, bericht,\n pfad: konf.nachbessern_ordner + '/nachbessern-' + s.slug + '.json',\n anzahl_pflicht: pflicht.length, anzahl_erweitert: erweitert.length }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
640
],
"id": "202c13fa-f4eb-4f85-a1a9-97f953ac99dc",
"name": "Fehlende Begriffe ermitteln"
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $json.pfad }}?ref={{ $('Konfiguration').first().json.branch }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
2440,
640
],
"id": "8cbf40d1-3067-461f-a394-796fb71981d0",
"name": "Nachbesserung vorhanden?",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst n = $('Fehlende Begriffe ermitteln').first().json;\nconst p = $input.first().json;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kp = K(p);\nconst da = p.statusCode === 200 && kp && kp.sha;\nconst konf = $('Konfiguration').first().json;\nreturn [{ json: { ...n, methode: da ? 'PUT' : 'POST', commit_body: JSON.stringify({\n content: Buffer.from(JSON.stringify(n.bericht, null, 2), 'utf8').toString('base64'),\n message: `Nachbesserung ${n.slug}: Score ${n.score} unter Ziel`,\n branch: konf.branch, ...(da ? { sha: kp.sha } : {}),\n})}}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
640
],
"id": "b20fd046-39d8-48bf-941d-0866963437d0",
"name": "Nachbesserung vorbereiten"
},
{
"parameters": {
"method": "={{ $json.methode }}",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $json.pfad }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.commit_body }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
2880,
640
],
"id": "0197b51e-4b00-4fd9-acb0-86a5193ed692",
"name": "Nachbesserung committen",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
},
"onError": "continueErrorOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst n = $('Fehlende Begriffe ermitteln').first().json;\nreturn [{ json: { slug: n.slug, ergebnis: 'fehler', stufe: 'nachbessern-commit', score: n.score,\n meldung: 'Nachbesserungsdatei konnte nicht geschrieben werden: '\n + JSON.stringify($input.first().json).slice(0, 400) }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2880,
860
],
"id": "9c770168-67a5-4ec5-9498-1037533d808d",
"name": "Fehler Nachbesserung",
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst n = $('Fehlende Begriffe ermitteln').first().json;\nconst p = $('Artikel pruefen').first().json;\nreturn [{ json: { slug: n.slug, ergebnis: 'nachbessern', score: n.score,\n preis_warnung: p.preis_warnung || null,\n stil_hinweise: p.stil_hinweise || [],\n score_ziel: n.bericht.score_ziel, pfad: n.pfad,\n anzahl_pflicht: n.anzahl_pflicht, anzahl_erweitert: n.anzahl_erweitert,\n wortzahl: n.bericht.wortzahl,\n meldung: `Score ${n.score} unter Ziel ${n.bericht.score_ziel}. `\n + `${n.anzahl_pflicht} Pflichtbegriffe fehlen, ${n.anzahl_erweitert} Zusatzbegriffe.` }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3100,
640
],
"id": "0a987ebf-e478-48ce-b721-598e2759019a",
"name": "Nachbesserung gemerkt"
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Konfiguration').first().json.nw_base }}/import-content",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
2220,
240
],
"id": "4ee193df-d21a-4a4b-a78e-ce967ce6fabf",
"name": "Revision zu NeuronWriter",
"credentials": {
"httpHeaderAuth": {
"id": "U23n2O980GKWocWq",
"name": "NeuronWriter API"
}
},
"onError": "continueErrorOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $('Score auswerten').first().json;\nreturn [{ json: { ...s, import_warnung: 'Revision konnte nicht zu NeuronWriter gespeichert werden: '\n + JSON.stringify($input.first().json).slice(0, 300) }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
60
],
"id": "9fa33651-10b2-4068-a5db-0c1705a394a6",
"name": "Fehler Revision",
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.wp_base }}/wp-json/wp/v2/posts?slug={{ $('Score auswerten').first().json.slug }}&status=publish,draft,pending,future,private&context=edit",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
2440,
240
],
"id": "bd2f2ec2-0ce6-4491-86cf-342aeecbbeed",
"name": "Beitrag vorhanden?",
"credentials": {
"httpBasicAuth": {
"id": "WIN05TZgM0K5VOGy",
"name": "WP skrift.de Redakteur"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.wp_base }}/wp-json/wp/v2/categories?search={{ encodeURIComponent($('Score auswerten').first().json.artikel.kategorie) }}&per_page=100",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
2660,
240
],
"id": "caa0d4e8-3bca-4fd1-90e4-824837ca8c01",
"name": "Kategorie suchen",
"credentials": {
"httpBasicAuth": {
"id": "WIN05TZgM0K5VOGy",
"name": "WP skrift.de Redakteur"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $('Score auswerten').first().json;\nconst gesucht = String(s.artikel.kategorie).trim();\nconst antwort = $input.first().json;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst ka = K(antwort);\nconst treffer = (antwort.statusCode === 200 && Array.isArray(ka))\n ? ka.find(c => c.name.toLowerCase() === gesucht.toLowerCase()) : null;\nreturn [{ json: { ...s, kategorie_id: treffer ? treffer.id : null,\n kategorie_name: gesucht, kategorie_fehlt: !treffer } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2880,
240
],
"id": "cd297bed-663d-47f3-ae1d-7442e7251e69",
"name": "Kategorie pruefen"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "f05870e6-38ac-4e5e-8b77-c75437e2d520",
"leftValue": "={{ $json.kategorie_fehlt }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3100,
240
],
"id": "01a425ce-96d8-4a77-b797-946bc628ad2f",
"name": "Kategorie anlegen?"
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Konfiguration').first().json.wp_base }}/wp-json/wp/v2/categories",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ name: $json.kategorie_name }) }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
3320,
140
],
"id": "ed15de50-93a9-4413-8729-3aa6c57cac6a",
"name": "Kategorie anlegen",
"credentials": {
"httpBasicAuth": {
"id": "WIN05TZgM0K5VOGy",
"name": "WP skrift.de Redakteur"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $('Kategorie pruefen').first().json;\nconst e = $input.first().json;\nconst id = s.kategorie_id || e.id || null;\nif (!id) {\n throw new Error('Kategorie \"' + s.kategorie_name + '\" konnte weder gefunden noch angelegt werden. '\n + 'Antwort: ' + JSON.stringify(e).slice(0, 300));\n}\nreturn [{ json: { ...s, kategorie_id: id } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3540,
240
],
"id": "1dad3b28-189b-430a-ac32-c1724c816f16",
"name": "Kategorie festlegen"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $input.first().json;\nconst dup = $('Beitrag vorhanden?').first().json;\nconst a = s.artikel;\n\n// Duplikatspruefung: exakter Slug-Treffer entscheidet ueber anlegen oder aktualisieren.\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kd = K(dup);\nconst treffer = (dup.statusCode === 200 && Array.isArray(kd))\n ? kd.find(p => p.slug === s.slug) : null;\n\nconst heute = new Date().toISOString().slice(0, 10);\n\nconst meta = {\n kurzantwort: a.kurzantwort || '',\n cta_ziel: a.cta_ziel || '',\n stand_datum: a.stand_datum || heute,\n faq: Array.isArray(a.faq)\n ? a.faq.filter(f => f && f.frage).map(f => ({ frage: String(f.frage), antwort: String(f.antwort || '') }))\n : [],\n _seopress_titles_title: a.seo_titel || a.titel,\n _seopress_titles_desc: a.seo_beschreibung || '',\n _seopress_analysis_target_kw: a.keyword || '',\n};\n\n// --- Elementor-Struktur aus Artikel und FAQ ---\n// Zerlegt den Text an den H2-Grenzen in Text-Editor-Widgets und haengt das FAQ\n// als nested-accordion an, dasselbe Widget wie auf den Leistungsseiten.\nconst konf = $('Konfiguration').first().json;\nconst eid = () => Math.random().toString(16).slice(2, 9);\nconst textWidget = (h) => ({ id: eid(), elType: 'widget', settings: { editor: h },\n elements: [], widgetType: 'text-editor' });\nconst behaelter = (kinder, innen) => {\n const s = { content_width: 'full' };\n if (innen) Object.assign(s, {\n _title: 'Element',\n padding: { unit: 'px', top: '8', right: '8', bottom: '8', left: '8', isLinked: true },\n flex_gap: { column: '20', row: '20', isLinked: true, unit: 'px', size: 20 },\n });\n const c = { id: eid(), elType: 'container', settings: s, elements: kinder };\n if (innen) { c.isInner = true; c.isLocked = true; }\n return c;\n};\n\nfunction elementorBauen() {\n const wurzel = [];\n if (a.kurzantwort) {\n wurzel.push(behaelter([textWidget(\n `<div class=\"skrift-kurzantwort\"><p>${a.kurzantwort}</p></div>`)]));\n }\n const html = String(a.content_html || '');\n const stellen = [];\n const re2 = /<h2[ >]/g;\n let mm;\n while ((mm = re2.exec(html)) !== null) stellen.push(mm.index);\n const bloecke = [];\n if (!stellen.length) {\n if (html.trim()) bloecke.push(html.trim());\n } else {\n if (stellen[0] > 0 && html.slice(0, stellen[0]).trim()) bloecke.push(html.slice(0, stellen[0]).trim());\n stellen.forEach((s, i) => {\n const ende = i + 1 < stellen.length ? stellen[i + 1] : html.length;\n const b = html.slice(s, ende).trim();\n if (b) bloecke.push(b);\n });\n }\n bloecke.forEach(b => wurzel.push(behaelter([textWidget(b)])));\n\n if (meta.faq.length) {\n const items = [], kinder = [];\n meta.faq.forEach(f => {\n items.push({ item_title: f.frage, _id: eid() });\n kinder.push(behaelter([textWidget(`<p>${f.antwort || ''}</p>`)], true));\n });\n wurzel.push(behaelter([\n textWidget(`<h2>${konf.faq_ueberschrift || 'Häufige Fragen'}</h2>`),\n { id: eid(), elType: 'widget', widgetType: 'nested-accordion',\n settings: { items, faq_schema: 'yes', default_state: 'all_collapsed' },\n elements: kinder },\n ]));\n }\n return wurzel;\n}\n\nif (konf.elementor_ausgabe) {\n meta._elementor_data = JSON.stringify(elementorBauen());\n meta._elementor_edit_mode = 'builder';\n meta._elementor_template_type = 'wp-post';\n}\n\nconst body = {\n title: a.titel,\n slug: s.slug,\n content: a.content_html,\n status: 'draft', // harte Vorgabe: nie automatisch veroeffentlichen\n categories: [s.kategorie_id],\n meta,\n};\nif (a.auszug) body.excerpt = a.auszug;\n\nreturn [{ json: {\n ...s,\n vorhanden: !!treffer,\n post_id: treffer ? treffer.id : null,\n alter_status: treffer ? treffer.status : null,\n wp_url: treffer\n ? `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts/${treffer.id}`\n : `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts`,\n wp_body: JSON.stringify(body),\n}}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3760,
240
],
"id": "b64f3c2d-f4a3-4e61-abd0-4b2b5fa42661",
"name": "WordPress-Body bauen"
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.wp_url }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.wp_body }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
3980,
240
],
"id": "d1d7fd80-caf7-46cf-9ded-9aa3b97b9a15",
"name": "Beitrag anlegen oder aktualisieren",
"credentials": {
"httpBasicAuth": {
"id": "WIN05TZgM0K5VOGy",
"name": "WP skrift.de Redakteur"
}
},
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst v = $('WordPress-Body bauen').first().json;\nconst p = $input.first().json;\n\nif (!p.id) {\n throw new Error('WordPress lieferte keine Beitrags-ID zurueck. Antwort: '\n + JSON.stringify(p).slice(0, 400));\n}\nif (p.status !== 'draft') {\n throw new Error('Beitrag ' + p.id + ' hat Status \"' + p.status\n + '\" statt \"draft\". Der Workflow darf nie veroeffentlichen. Bitte pruefen.');\n}\n\n// Gegenprobe: sind die Meta-Felder wirklich angekommen?\nconst m = p.meta || {};\nconst fehlend = ['kurzantwort', 'cta_ziel', 'stand_datum', 'faq']\n .filter(k => m[k] === undefined);\nconst faq_ok = Array.isArray(m.faq) ? m.faq.length : -1;\n\nconst konf = $('Konfiguration').first().json;\nreturn [{ json: {\n slug: v.slug, ergebnis: 'importiert', score: v.score, score_ziel: v.score_ziel,\n post_id: p.id, aktion: v.vorhanden ? 'aktualisiert' : 'neu angelegt',\n alter_status: v.alter_status, status: p.status,\n kategorie: v.kategorie_name, kategorie_id: v.kategorie_id,\n vorschau_url: `${konf.wp_base}/?p=${p.id}&preview=true`,\n bearbeiten_url: `${konf.wp_base}/wp-admin/post.php?post=${p.id}&action=edit`,\n meta_warnung: fehlend.length\n ? 'Diese Meta-Felder kamen nicht zurueck: ' + fehlend.join(', ')\n + '. Mu-Plugin skrift-ratgeber-felder.php installiert?'\n : null,\n faq_eintraege: faq_ok,\n elementor: (p.meta || {})._elementor_edit_mode === 'builder'\n ? (() => { try { return JSON.parse((p.meta || {})._elementor_data || '[]').length + ' Container'; }\n catch (e) { return 'ungueltig'; } })()\n : 'klassischer Inhalt',\n seo_titel: (p.meta || {})._seopress_titles_title || null,\n preis_warnung: v.preis_warnung || null,\n stil_hinweise: v.stil_hinweise || [],\n preis_shortcodes: v.preis_shortcodes || 0,\n}}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4200,
240
],
"id": "a16f780a-5f7e-4581-b0f5-7f55464e87d1",
"name": "Ergebnis pruefen"
},
{
"parameters": {
"method": "POST",
"url": "={{ $('Konfiguration').first().json.wp_base }}/wp-json/seopress/v1/commands/clear-cache",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 30000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({}) }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
4420,
240
],
"id": "f1b48e11-b8c6-4842-913c-5a8a43b042d5",
"name": "Cache leeren",
"credentials": {
"httpBasicAuth": {
"id": "WIN05TZgM0K5VOGy",
"name": "WP skrift.de Redakteur"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst r = $('Ergebnis pruefen').first().json;\nconst c = $input.first().json;\nreturn [{ json: { ...r,\n cache_geleert: c.statusCode === 200,\n cache_hinweis: c.statusCode === 200 ? null\n : 'Cache-Endpunkt antwortete mit HTTP ' + c.statusCode + '. Kein Abbruchgrund.' }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4640,
240
],
"id": "6708b72c-7577-4bb5-b824-652648fff48c",
"name": "Importergebnis merken"
},
{
"parameters": {
"amount": "={{ $('Konfiguration').first().json.pause_zwischen_artikeln }}",
"unit": "seconds"
},
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
4860,
420
],
"id": "2297ffc3-9c1a-48a2-ba7a-20f394af02f0",
"name": "Pause Rate Limit",
"webhookId": "cf34062f-fc9c-49de-b651-9a8d020b9d57"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst alle = $input.all().map(i => i.json);\nconst importiert = alle.filter(r => r.ergebnis === 'importiert');\nconst nachbessern = alle.filter(r => r.ergebnis === 'nachbessern');\nconst fehler = alle.filter(r => r.ergebnis === 'fehler');\n\nconst zeilen = importiert.map(r => ({\n slug: r.slug, score: r.score, datum: new Date().toISOString(),\n post_id: r.post_id, vorschau_url: r.vorschau_url,\n aktion: r.aktion, kategorie: r.kategorie,\n}));\n\nreturn [{ json: { alle, importiert, nachbessern, fehler,\n neue_zeilen: zeilen,\n anzahl_importiert: importiert.length,\n anzahl_nachbessern: nachbessern.length,\n anzahl_fehler: fehler.length }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
900,
80
],
"id": "056c1cf1-f7ba-48dc-a1c2-3d22d70e4550",
"name": "Ergebnisse sammeln"
},
{
"parameters": {
"method": "GET",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.status_pfad }}?ref={{ $('Konfiguration').first().json.branch }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": true,
"fullResponse": true
}
},
"timeout": 60000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1120,
80
],
"id": "88815af5-2481-4518-adf6-d50ec1cb28c8",
"name": "Statustabelle lesen",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
}
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $('Ergebnisse sammeln').first().json;\nconst p = $input.first().json;\nconst konf = $('Konfiguration').first().json;\n\nlet bestand = [];\nlet sha = null;\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kp = K(p);\nif (p.statusCode === 200 && kp && kp.content) {\n sha = kp.sha;\n try { bestand = JSON.parse(Buffer.from(kp.content, 'base64').toString('utf8')); }\n catch (e) { bestand = []; }\n if (!Array.isArray(bestand)) bestand = [];\n}\n\n// Je Slug nur die juengste Zeile behalten.\nconst neu = [...bestand];\ns.neue_zeilen.forEach(z => {\n const i = neu.findIndex(x => x.slug === z.slug);\n if (i >= 0) neu[i] = z; else neu.push(z);\n});\n\nreturn [{ json: { ueberspringen: s.neue_zeilen.length === 0, commit_body: JSON.stringify({\n content: Buffer.from(JSON.stringify(neu, null, 2), 'utf8').toString('base64'),\n message: `Import: ${s.anzahl_importiert} Beitrag/Beitraege als Entwurf`,\n branch: konf.branch, ...(sha ? { sha } : {}),\n}), methode: sha ? 'PUT' : 'POST' }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1340,
80
],
"id": "c094f8dd-9756-47b6-876b-057507091af2",
"name": "Statustabelle fortschreiben"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"conditions": [
{
"id": "51ddce38-e3ef-46d4-8922-b51f79ff011a",
"leftValue": "={{ $json.ueberspringen }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"looseTypeValidation": true,
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1560,
80
],
"id": "480901ea-3057-402a-8828-c449d4127118",
"name": "Gab es Importe?"
},
{
"parameters": {
"method": "={{ $json.methode }}",
"url": "={{ $('Konfiguration').first().json.gitea_base }}/api/v1/repos/{{ $('Konfiguration').first().json.gitea_owner }}/{{ $('Konfiguration').first().json.gitea_repo }}/contents/{{ $('Konfiguration').first().json.status_pfad }}",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"options": {
"response": {
"response": {
"neverError": false
}
},
"timeout": 60000
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json.commit_body }}",
"sendHeaders": true,
"specifyHeaders": "json",
"jsonHeaders": "={\"Content-Type\":\"application/json\"}"
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
1780,
160
],
"id": "461196bc-73d5-49a9-be96-9da3866479dc",
"name": "Statustabelle schreiben",
"credentials": {
"httpHeaderAuth": {
"id": "VOD4JM5zwpSC8wAr",
"name": "Gitea Skrift Auto Article"
}
},
"onError": "continueErrorOutput",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 3000
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nreturn [{ json: { statustabelle_warnung: 'Statustabelle konnte nicht geschrieben werden: '\n + JSON.stringify($input.first().json).slice(0, 300) }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
340
],
"id": "a754dbe7-60b0-43c2-80bd-ff88df4eac50",
"name": "Fehler Statustabelle",
"onError": "continueRegularOutput"
},
{
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "\nconst s = $('Ergebnisse sammeln').first().json;\nconst z = [];\nz.push(`Importiert: ${s.anzahl_importiert} | Nachbessern: ${s.anzahl_nachbessern} | Fehler: ${s.anzahl_fehler}`);\nz.push('');\n\nif (s.importiert.length) {\n z.push('Als Entwurf in WordPress:');\n s.importiert.forEach(r => {\n z.push(` ${r.slug} (Score ${r.score}, ${r.aktion})`);\n z.push(` Vorschau: ${r.vorschau_url}`);\n z.push(` Bearbeiten: ${r.bearbeiten_url}`);\n if (r.elementor) z.push(` Layout: ${r.elementor}`);\n if (r.meta_warnung) z.push(` ACHTUNG: ${r.meta_warnung}`);\n if (r.preis_warnung) z.push(` PREISE: ${r.preis_warnung}`);\n if (r.stil_hinweise && r.stil_hinweise.length) {\n z.push(` STIL: ${r.stil_hinweise.length} Hinweis(e)`);\n r.stil_hinweise.slice(0, 6).forEach(h => z.push(` - ${h.art}: ${h.stelle}`));\n if (r.stil_hinweise.length > 6) z.push(` ... weitere im Nachbesserungsbericht bzw. in der Execution`);\n }\n if (r.cache_hinweis) z.push(` Hinweis: ${r.cache_hinweis}`);\n });\n z.push('');\n}\nif (s.nachbessern.length) {\n z.push('Score zu niedrig, Nachbesserung liegt im Repo:');\n s.nachbessern.forEach(r => {\n z.push(` ${r.slug}: Score ${r.score} von ${r.score_ziel}`);\n z.push(` ${r.anzahl_pflicht} Pflichtbegriffe fehlen, ${r.anzahl_erweitert} Zusatzbegriffe`);\n if (r.wortzahl && r.wortzahl.differenz !== null) {\n z.push(` Wortzahl ${r.wortzahl.ist} von ${r.wortzahl.ziel} (${r.wortzahl.differenz > 0 ? '+' : ''}${r.wortzahl.differenz})`);\n }\n z.push(` Datei: ${r.pfad}`);\n if (r.preis_warnung) z.push(` PREISE: ${r.preis_warnung}`);\n if (r.stil_hinweise && r.stil_hinweise.length) {\n z.push(` STIL: ${r.stil_hinweise.length} Hinweis(e)`);\n r.stil_hinweise.slice(0, 6).forEach(h => z.push(` - ${h.art}: ${h.stelle}`));\n if (r.stil_hinweise.length > 6) z.push(` ... weitere im Nachbesserungsbericht bzw. in der Execution`);\n }\n });\n z.push('');\n}\nif (s.fehler.length) {\n z.push('Fehler:');\n s.fehler.forEach(r => z.push(` ${r.slug || '?'} [${r.stufe}]: ${r.meldung}`));\n z.push('');\n}\nz.push('Kein Beitrag wurde veroeffentlicht. Alle Importe stehen auf Entwurf.');\n\nreturn [{ json: {\n betreff: `Skrift Ratgeber: ${s.anzahl_importiert} Entwurf/Entwuerfe`\n + (s.anzahl_nachbessern ? `, ${s.anzahl_nachbessern} zum Nachbessern` : '')\n + (s.anzahl_fehler ? `, ${s.anzahl_fehler} Fehler` : ''),\n text: z.join('\\n') }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
80
],
"id": "86c65a05-ec2b-49fd-9134-0538d0be1065",
"name": "Meldung formulieren"
},
{
"parameters": {
"fromEmail": "",
"toEmail": "={{ $('Konfiguration').first().json.benachrichtigung_an }}",
"subject": "={{ $json.betreff }}",
"emailFormat": "text",
"text": "={{ $json.text }}",
"options": {}
},
"type": "n8n-nodes-base.emailSend",
"typeVersion": 2.1,
"position": [
2220,
80
],
"id": "0a421a3c-7f3a-4cc0-b3ca-edd79e6a4584",
"name": "Benachrichtigung",
"disabled": true,
"notes": "SMTP-Credential zuweisen, Absender eintragen, dann aktivieren."
}
],
"connections": {
"Manuell starten": {
"main": [
[
{
"node": "Konfiguration",
"type": "main",
"index": 0
}
]
]
},
"Webhook Start": {
"main": [
[
{
"node": "Konfiguration",
"type": "main",
"index": 0
}
]
]
},
"Konfiguration": {
"main": [
[
{
"node": "Artikelordner lesen",
"type": "main",
"index": 0
}
]
]
},
"Artikelordner lesen": {
"main": [
[
{
"node": "Artikeldateien filtern",
"type": "main",
"index": 0
}
]
]
},
"Artikeldateien filtern": {
"main": [
[
{
"node": "Artikel vorhanden?",
"type": "main",
"index": 0
}
]
]
},
"Artikel vorhanden?": {
"main": [
[
{
"node": "Nichts zu tun",
"type": "main",
"index": 0
}
],
[
{
"node": "Artikel nacheinander",
"type": "main",
"index": 0
}
]
]
},
"Artikel nacheinander": {
"main": [
[
{
"node": "Ergebnisse sammeln",
"type": "main",
"index": 0
}
],
[
{
"node": "Artikeldatei laden",
"type": "main",
"index": 0
}
]
]
},
"Artikeldatei laden": {
"main": [
[
{
"node": "Brief laden",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehler Artikeldatei",
"type": "main",
"index": 0
}
]
]
},
"Brief laden": {
"main": [
[
{
"node": "Artikel pruefen",
"type": "main",
"index": 0
}
]
]
},
"Artikel pruefen": {
"main": [
[
{
"node": "Score ermitteln",
"type": "main",
"index": 0
}
]
]
},
"Score ermitteln": {
"main": [
[
{
"node": "Score auswerten",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehler Score",
"type": "main",
"index": 0
}
]
]
},
"Score auswerten": {
"main": [
[
{
"node": "Score erreicht?",
"type": "main",
"index": 0
}
]
]
},
"Score erreicht?": {
"main": [
[
{
"node": "Revision zu NeuronWriter",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehlende Begriffe ermitteln",
"type": "main",
"index": 0
}
]
]
},
"Fehlende Begriffe ermitteln": {
"main": [
[
{
"node": "Nachbesserung vorhanden?",
"type": "main",
"index": 0
}
]
]
},
"Nachbesserung vorhanden?": {
"main": [
[
{
"node": "Nachbesserung vorbereiten",
"type": "main",
"index": 0
}
]
]
},
"Nachbesserung vorbereiten": {
"main": [
[
{
"node": "Nachbesserung committen",
"type": "main",
"index": 0
}
]
]
},
"Nachbesserung committen": {
"main": [
[
{
"node": "Nachbesserung gemerkt",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehler Nachbesserung",
"type": "main",
"index": 0
}
]
]
},
"Revision zu NeuronWriter": {
"main": [
[
{
"node": "Beitrag vorhanden?",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehler Revision",
"type": "main",
"index": 0
}
]
]
},
"Fehler Revision": {
"main": [
[
{
"node": "Beitrag vorhanden?",
"type": "main",
"index": 0
}
]
]
},
"Beitrag vorhanden?": {
"main": [
[
{
"node": "Kategorie suchen",
"type": "main",
"index": 0
}
]
]
},
"Kategorie suchen": {
"main": [
[
{
"node": "Kategorie pruefen",
"type": "main",
"index": 0
}
]
]
},
"Kategorie pruefen": {
"main": [
[
{
"node": "Kategorie anlegen?",
"type": "main",
"index": 0
}
]
]
},
"Kategorie anlegen?": {
"main": [
[
{
"node": "Kategorie anlegen",
"type": "main",
"index": 0
}
],
[
{
"node": "Kategorie festlegen",
"type": "main",
"index": 0
}
]
]
},
"Kategorie anlegen": {
"main": [
[
{
"node": "Kategorie festlegen",
"type": "main",
"index": 0
}
]
]
},
"Kategorie festlegen": {
"main": [
[
{
"node": "WordPress-Body bauen",
"type": "main",
"index": 0
}
]
]
},
"WordPress-Body bauen": {
"main": [
[
{
"node": "Beitrag anlegen oder aktualisieren",
"type": "main",
"index": 0
}
]
]
},
"Beitrag anlegen oder aktualisieren": {
"main": [
[
{
"node": "Ergebnis pruefen",
"type": "main",
"index": 0
}
]
]
},
"Ergebnis pruefen": {
"main": [
[
{
"node": "Cache leeren",
"type": "main",
"index": 0
}
]
]
},
"Cache leeren": {
"main": [
[
{
"node": "Importergebnis merken",
"type": "main",
"index": 0
}
]
]
},
"Importergebnis merken": {
"main": [
[
{
"node": "Pause Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Nachbesserung gemerkt": {
"main": [
[
{
"node": "Pause Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Fehler Nachbesserung": {
"main": [
[
{
"node": "Pause Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Fehler Artikeldatei": {
"main": [
[
{
"node": "Pause Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Fehler Score": {
"main": [
[
{
"node": "Pause Rate Limit",
"type": "main",
"index": 0
}
]
]
},
"Pause Rate Limit": {
"main": [
[
{
"node": "Artikel nacheinander",
"type": "main",
"index": 0
}
]
]
},
"Ergebnisse sammeln": {
"main": [
[
{
"node": "Statustabelle lesen",
"type": "main",
"index": 0
}
]
]
},
"Statustabelle lesen": {
"main": [
[
{
"node": "Statustabelle fortschreiben",
"type": "main",
"index": 0
}
]
]
},
"Statustabelle fortschreiben": {
"main": [
[
{
"node": "Gab es Importe?",
"type": "main",
"index": 0
}
]
]
},
"Gab es Importe?": {
"main": [
[
{
"node": "Meldung formulieren",
"type": "main",
"index": 0
}
],
[
{
"node": "Statustabelle schreiben",
"type": "main",
"index": 0
}
]
]
},
"Statustabelle schreiben": {
"main": [
[
{
"node": "Meldung formulieren",
"type": "main",
"index": 0
}
],
[
{
"node": "Fehler Statustabelle",
"type": "main",
"index": 0
}
]
]
},
"Fehler Statustabelle": {
"main": [
[
{
"node": "Meldung formulieren",
"type": "main",
"index": 0
}
]
]
},
"Meldung formulieren": {
"main": [
[
{
"node": "Benachrichtigung",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveManualExecutions": true,
"callerPolicy": "workflowsFromSameOwner"
}
}