diff --git a/README.md b/README.md index dea7e19..59b0b29 100644 --- a/README.md +++ b/README.md @@ -275,7 +275,7 @@ sie beim Schreiben direkt vorliegen. ### Zielwert anpassen -Im Node **Konfiguration**, Feld `score_ziel`. Standard ist 72. +Im Node **Konfiguration**, Feld `score_ziel`. Standard ist 65. Der Wert gilt fuer alle Artikel eines Laufs. Fuer einen einzelnen Lauf laesst er sich ueber den Webhook uebersteuern, @@ -286,10 +286,52 @@ curl -X POST https://n8n.lucas-orth.de/webhook/pruefen-und-importieren \ -H "Content-Type: application/json" -d '{"score_ziel":60}' ``` -Ohne Angabe bleibt es bei 72. +Ohne Angabe bleibt es bei 65. Alle anderen Stellschrauben liegen im selben Node: Basis-URLs, Ordnernamen, Pause zwischen Artikeln, Empfaengeradresse. +### Warum 65 und nicht hoeher + +Ab etwa 70 verlangt NeuronWriter Wettbewerbernamen, ausgeschriebene Preise und +exakte Wortfolgen, die sich nur mit schiefen Saetzen einbauen lassen. Der +Referenzartikel kam mit solchen Saetzen auf 72 und ohne sie auf 71. Verbindlich +sind deshalb nur die Pflichtbegriffe, Zusatzbegriffe nur, wenn der Satz dadurch +nicht schlechter wird. + +Score, Luecken und Stilpruefung rechnen gegen Kurzantwort, Artikel und FAQ +zusammen, weil alle drei auf der fertigen Seite stehen. + +### Stilpruefung + +Workflow 2 prueft jeden Artikel auf Muster, die maschinell wirken, und meldet +die Fundstellen in der Benachrichtigung und im Nachbesserungsbericht. Der Import +wird dadurch nicht blockiert. + +Geprueft werden Formeln wie "Genau das", "Der Grund ist", "nicht nur, sondern +auch", "entscheidend ist", "der groesste Hebel", "macht den Unterschied", +Meta-Saetze wie "Wir zeigen Ihnen", unbelegte Behauptungen wie "erfahrungsgemaess" +oder "in aller Regel", Aufzaehlungen mit erstens und zweitens, Dreierreihen aus +Adjektiven, rhetorische Fragen mit Sofortantwort, gleichfoermige Satzlaengen, +gleiche Satz- und Absatzanfaenge, gleich lange Absaetze und fehlendes +Praxiswissen. + +Die Regeln liegen in `build/stilpruefung.js` und lassen sich dort erweitern. +Lokal testen: + +``` +node -e "const {stilpruefung}=require('./build/stilpruefung.js');console.log(stilpruefung(require('./artikel/artikel-SLUG.json')))" +``` + +Danach `python3 build/gen_wf2.py` und den Workflow in n8n aktualisieren. + +### Praxiswissen + +Jede Zeile in `artikel-index.json` hat ein Feld `praxis`, eine Liste von drei +bis fuenf Stichpunkten aus der eigenen Arbeit: Beispiele, typische Fehler, +Zahlen aus der Produktion, klare Empfehlungen, auch Faelle, in denen Handschrift +sich nicht lohnt. Workflow 1 legt sie in den Brief. Ist die Liste leer, wird der +Artikel nicht geschrieben, sondern zuerst das Praxiswissen eingeholt. + ### Nachbesserungsbericht `/evaluate-content` liefert ausschliesslich `{status, content_score}`, diff --git a/build/gen_wf1.py b/build/gen_wf1.py index feaa75f..512b84d 100644 --- a/build/gen_wf1.py +++ b/build/gen_wf1.py @@ -276,7 +276,8 @@ const brief = { wortzahl_ziel: ((n.metrics || {}).word_count || {}).target ?? null, // --- Artikelkontext aus der Tabelle --- - artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel }, + artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel, + praxis: Array.isArray(a.praxis) ? a.praxis : [] }, // --- Zusatz fuer die Schreibvorlage --- zusatz: { @@ -308,6 +309,18 @@ const brief = { faq: 'FAQ nicht in content_html schreiben, sondern als Feld faq mit Frage-Antwort-Paaren. ' + 'Die Ausgabe im Beitrag uebernimmt der Shortcode [skrift_faq].', kurzantwort: 'Ein Absatz, der die Hauptfrage direkt beantwortet, als eigenes Feld kurzantwort.', + praxis: 'Der Artikel baut auf den Stichpunkten in artikel.praxis auf. Jeder Stichpunkt kommt ' + + 'erkennbar im Text vor, als Beispiel, Erfahrung oder klare Empfehlung. Ist die Liste leer, ' + + 'wird der Artikel nicht geschrieben, sondern zuerst Praxiswissen bei Lucas erfragt.', + begriffe: 'begriffe_pflicht sind verbindlich. begriffe_erweitert nur verwenden, wenn der Satz ' + + 'dadurch nicht schlechter wird. Keine Saetze, die nur fuer den NeuronWriter-Score da sind. ' + + 'Keine Wettbewerbernamen. Zielwert im Import ist 65.', + stil: 'Kurze und lange Saetze mischen. Absaetze unterschiedlich lang. Keine Formeln wie ' + + '"Genau das", "Der Grund ist", "nicht nur, sondern auch", "entscheidend ist", ' + + '"der groesste Hebel", "macht den Unterschied". Keine Aufzaehlung mit erstens, zweitens. ' + + 'Keine rhetorische Frage mit Sofortantwort. Keine Behauptung ohne Beleg, also kein ' + + '"erfahrungsgemaess" oder "in aller Regel". Wo es passt, auch abraten: wann sich ' + + 'Handschrift nicht lohnt.', }, meta: { diff --git a/build/gen_wf2.py b/build/gen_wf2.py index c0aca9b..6334c46 100644 --- a/build/gen_wf2.py +++ b/build/gen_wf2.py @@ -4,6 +4,8 @@ import json, os, uuid CRED_NW = {"httpHeaderAuth": {"id": "U23n2O980GKWocWq", "name": "NeuronWriter API"}} CRED_GITEA = {"httpHeaderAuth": {"id": "VOD4JM5zwpSC8wAr", "name": "Gitea Skrift Auto Article"}} +_js = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "stilpruefung.js"), encoding="utf-8").read() +STIL_JS = _js.split("if (typeof module")[0] + "\n" CRED_WP = {"httpBasicAuth": {"id": "WIN05TZgM0K5VOGy", "name": "WP skrift.de Redakteur"}} nodes, conns = [], {} @@ -66,7 +68,7 @@ KONF = node("Konfiguration", "n8n-nodes-base.set", 3.4, [-240, 190], { {"id": nid(), "name": "nachbessern_ordner","value": "nachbessern", "type": "string"}, {"id": nid(), "name": "status_pfad", "value": "status-importe.json", "type": "string"}, {"id": nid(), "name": "wp_base", "value": "https://skrift.de", "type": "string"}, - {"id": nid(), "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 72 }}", "type": "number"}, + {"id": nid(), "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}", "type": "number"}, {"id": nid(), "name": "pause_zwischen_artikeln", "value": 5, "type": "number"}, {"id": nid(), "name": "elementor_ausgabe", "value": False, "type": "boolean"}, {"id": nid(), "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string"}, @@ -135,7 +137,7 @@ BRIEF = http("Brief laden", [1120, 420], "GET", extra={"options": {"response": {"response": {"neverError": True, "fullResponse": True}}, "timeout": 60000}}) link(ART, BRIEF, out=0) -PRUEFEN = code("Artikel pruefen", [1340, 420], r""" +PRUEFEN = code("Artikel pruefen", [1340, 420], STIL_JS + r""" const meta = $('Artikel nacheinander').first().json; const artRoh = $('Artikeldatei laden').first().json; const briefAntwort = $input.first().json; @@ -183,8 +185,12 @@ const bewertungs_html = [ faqText ? `

Häufige Fragen

\n${faqText}` : '', ].filter(Boolean).join('\n'); +const praxis = Array.isArray(artikel.praxis) && artikel.praxis.length ? artikel.praxis + : ((brief && brief.artikel && brief.artikel.praxis) || []); + return [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief, bewertungs_html, + stil_hinweise: stilpruefung({ ...artikel, praxis }), preis_warnung: preisTreffer.length ? `${preisTreffer.length} ausgeschriebene(r) Preis(e) im Text: ${[...new Set(preisTreffer)].slice(0,5).join(', ')}. ` + 'Bitte durch [skrift_preis key="..."] ersetzen.' @@ -284,9 +290,11 @@ const bericht = { fehlende_pflichtbegriffe: pflicht, fehlende_zusatzbegriffe: erweitert, unbeantwortete_fragen: fragen_offen, + stil_hinweise: s.stil_hinweise || [], hinweis: s.brief_vorhanden ? 'Begriffe gegen briefe/brief-' + s.slug + '.json abgeglichen. Zaehlung als Teilstring, Komposita zaehlen mit.' : 'Kein Brief gefunden, daher nur der Score. Workflow 1 fuer diesen Slug erneut laufen lassen.', + regel: 'Pflichtbegriffe sind verbindlich. Zusatzbegriffe nur einbauen, wenn der Satz dadurch nicht schlechter wird. Keine Saetze nur fuer den Score.', }; const konf = $('Konfiguration').first().json; @@ -336,6 +344,7 @@ const n = $('Fehlende Begriffe ermitteln').first().json; const p = $('Artikel pruefen').first().json; return [{ json: { slug: n.slug, ergebnis: 'nachbessern', score: n.score, preis_warnung: p.preis_warnung || null, + stil_hinweise: p.stil_hinweise || [], score_ziel: n.bericht.score_ziel, pfad: n.pfad, anzahl_pflicht: n.anzahl_pflicht, anzahl_erweitert: n.anzahl_erweitert, wortzahl: n.bericht.wortzahl, @@ -568,6 +577,7 @@ return [{ json: { : 'klassischer Inhalt', seo_titel: (p.meta || {})._seopress_titles_title || null, preis_warnung: v.preis_warnung || null, + stil_hinweise: v.stil_hinweise || [], preis_shortcodes: v.preis_shortcodes || 0, }}]; """) @@ -686,6 +696,11 @@ if (s.importiert.length) { if (r.elementor) z.push(` Layout: ${r.elementor}`); if (r.meta_warnung) z.push(` ACHTUNG: ${r.meta_warnung}`); if (r.preis_warnung) z.push(` PREISE: ${r.preis_warnung}`); + if (r.stil_hinweise && r.stil_hinweise.length) { + z.push(` STIL: ${r.stil_hinweise.length} Hinweis(e)`); + r.stil_hinweise.slice(0, 6).forEach(h => z.push(` - ${h.art}: ${h.stelle}`)); + if (r.stil_hinweise.length > 6) z.push(` ... weitere im Nachbesserungsbericht bzw. in der Execution`); + } if (r.cache_hinweis) z.push(` Hinweis: ${r.cache_hinweis}`); }); z.push(''); @@ -700,6 +715,11 @@ if (s.nachbessern.length) { } z.push(` Datei: ${r.pfad}`); if (r.preis_warnung) z.push(` PREISE: ${r.preis_warnung}`); + if (r.stil_hinweise && r.stil_hinweise.length) { + z.push(` STIL: ${r.stil_hinweise.length} Hinweis(e)`); + r.stil_hinweise.slice(0, 6).forEach(h => z.push(` - ${h.art}: ${h.stelle}`)); + if (r.stil_hinweise.length > 6) z.push(` ... weitere im Nachbesserungsbericht bzw. in der Execution`); + } }); z.push(''); } diff --git a/build/stilpruefung.js b/build/stilpruefung.js new file mode 100644 index 0000000..2404ce2 --- /dev/null +++ b/build/stilpruefung.js @@ -0,0 +1,89 @@ +// Stilpruefung: findet typische Muster maschinell wirkender Texte im Deutschen. +// Wird von gen_wf2.py in den Code-Node "Artikel pruefen" eingebettet und ist +// lokal mit node testbar. Liefert Hinweise, blockiert nichts. +function stilpruefung(artikel) { + const hinweise = []; + const add = (art, stelle) => hinweise.push({ art, stelle: String(stelle).slice(0, 140) }); + const ohneTags = (h) => String(h || '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').replace(/\s+/g, ' ').trim(); + + const html = String(artikel.content_html || ''); + const absaetze = [...html.matchAll(/

([\s\S]*?)<\/p>/g)].map(m => ohneTags(m[1])).filter(Boolean); + const volltext = [ohneTags(artikel.kurzantwort), ohneTags(html), + ...(Array.isArray(artikel.faq) ? artikel.faq.map(f => ohneTags(f.antwort)) : [])].join(' '); + + // 1) Floskeln und Formeln, die in generierten Texten gehaeuft auftreten + const formeln = [ + ['genau das', 'Formel'], ['genau diese', 'Formel'], ['genau daran', 'Formel'], ['genau so', 'Formel'], + ['der grund ist', 'Formel'], ['nicht nur', 'Formel "nicht nur, sondern auch"'], + ['entscheidend ist', 'Formel'], ['entscheidend für', 'Formel'], ['größten hebel', 'Formel'], + ['den unterschied', 'Formel'], ['am ende des tages', 'Floskel'], ['unterm strich', 'Floskel'], + ['kurz gesagt', 'Floskel'], ['mit anderen worten', 'Floskel'], ['die gute nachricht', 'Floskel'], + ['spielt eine rolle', 'Floskel'], ['es lohnt sich', 'Floskel'], ['egal ob', 'Formel'], + ['in diesem artikel', 'Meta-Satz'], ['wir zeigen ihnen', 'Meta-Satz'], ['fazit', 'Zusammenfassung'], + ['tatsächlich', 'Fuellwort'], ['letztlich', 'Fuellwort'], ['schlicht', 'Fuellwort'], ['simpel', 'Fuellwort'], + ['erfahrungsgemäß', 'unbelegte Behauptung'], ['in aller regel', 'unbelegte Behauptung'], + ['studien zeigen', 'unbelegte Behauptung'], ['nachweislich', 'unbelegte Behauptung'], + ['bekanntlich', 'Floskel'], + ]; + const klein = volltext.toLowerCase(); + formeln.forEach(([f, art]) => { + let i = klein.indexOf(f); + while (i >= 0) { + add(art + ': "' + f + '"', '…' + volltext.slice(Math.max(0, i - 40), i + f.length + 50) + '…'); + i = klein.indexOf(f, i + f.length); + } + }); + + // 2) Aufzaehlungen im Fliesstext + if (/\berstens\b[\s\S]*\bzweitens\b/i.test(volltext)) add('Aufzaehlung "Erstens, zweitens"', 'Im Fliesstext lieber eine echte Liste oder zwei Saetze'); + 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) || []; + dreier.forEach(d => add('Dreierreihe aus Adjektiven', d)); + + // 3) Rhetorische Frage im Absatz, direkt beantwortet + absaetze.forEach(a => { + const m = a.match(/[^.!?]{8,}\?\s+[A-ZÄÖÜ][^.!?]{3,}[.!]/); + if (m) add('Rhetorische Frage mit Sofortantwort', m[0]); + }); + + // 4) Satzlaengen: gleichfoermiger Rhythmus + const saetze = ohneTags(html).split(/(?<=[.!?])\s+/).map(s => s.trim()) + .filter(s => s.split(' ').length >= 3); + const laengen = saetze.map(s => s.split(' ').length); + if (laengen.length >= 8) { + const mittel = laengen.reduce((x, y) => x + y, 0) / laengen.length; + const sd = Math.sqrt(laengen.reduce((x, y) => x + (y - mittel) ** 2, 0) / laengen.length); + if (sd < 4.5) add('Gleichfoermige Satzlaengen', `Durchschnitt ${mittel.toFixed(1)} Woerter, Streuung nur ${sd.toFixed(1)}. Kurze und lange Saetze mischen.`); + for (let i = 0; i + 4 <= laengen.length; i++) { + const fenster = laengen.slice(i, i + 4); + if (Math.max(...fenster) - Math.min(...fenster) <= 3) { + add('Vier Saetze gleicher Laenge hintereinander', saetze[i]); + i += 3; + } + } + } + + // 5) Gleiche Satzanfaenge + for (let i = 0; i + 3 <= saetze.length; i++) { + const w = saetze.slice(i, i + 3).map(s => s.split(' ')[0].toLowerCase()); + if (w[0] === w[1] && w[1] === w[2] && w[0].length > 2) { add('Drei Saetze mit gleichem Anfang', saetze[i]); i += 2; } + } + const anfaenge = {}; + absaetze.forEach(a => { const k = a.split(' ').slice(0, 2).join(' ').toLowerCase(); anfaenge[k] = (anfaenge[k] || 0) + 1; }); + Object.entries(anfaenge).filter(([, n]) => n >= 3).forEach(([k, n]) => add('Absatzanfaenge wiederholen sich', `${n} Absaetze beginnen mit "${k}"`)); + + // 6) Gleich lange Absaetze + const al = absaetze.map(a => a.split(' ').length).filter(n => n >= 12); + if (al.length >= 5) { + const m = al.reduce((x, y) => x + y, 0) / al.length; + const sd = Math.sqrt(al.reduce((x, y) => x + (y - m) ** 2, 0) / al.length); + if (sd / m < 0.25) add('Absaetze fast gleich lang', `Durchschnitt ${m.toFixed(0)} Woerter, kaum Abweichung. Einen Absatz bewusst kurz halten.`); + } + + // 7) Praxiswissen: ohne eigene Stichpunkte bleibt der Text generisch + if (!Array.isArray(artikel.praxis) || artikel.praxis.length === 0) { + add('Kein Praxiswissen hinterlegt', 'Feld "praxis" in artikel-index.json fuellen, dann den Artikel darauf aufbauen'); + } + + return hinweise; +} +if (typeof module !== 'undefined') module.exports = { stilpruefung }; diff --git a/workflows/brief-vorbereiten.json b/workflows/brief-vorbereiten.json index e641018..b21b75c 100644 --- a/workflows/brief-vorbereiten.json +++ b/workflows/brief-vorbereiten.json @@ -9,7 +9,7 @@ -460, 40 ], - "id": "5ad6950e-a5e9-4d1c-9f45-64e7f40c1345", + "id": "260ec628-48b5-4211-92df-2f12c211c736", "name": "Manuell starten" }, { @@ -33,7 +33,7 @@ -460, 200 ], - "id": "55fa4d8d-8362-4662-b422-5cf03694b4f8", + "id": "16f0bac4-8005-4dde-bf37-a38ff71fef70", "name": "Zeitplan Montag 07:00", "disabled": true }, @@ -50,9 +50,9 @@ -460, 360 ], - "id": "675b0a53-a893-40ef-99d6-fafaa8ac0792", + "id": "ab067d79-fb3a-430a-9361-7414ff5958d8", "name": "Webhook Start", - "webhookId": "2de1c213-4fb5-4e91-8b5c-9ce69ef25ca3" + "webhookId": "50276479-7f98-4b3e-8149-98156f310211" }, { "parameters": { @@ -60,85 +60,85 @@ "assignments": { "assignments": [ { - "id": "d43ddbb3-2514-41c6-b888-02abc4e15d41", + "id": "1cdff46d-72b5-4134-8033-1eb408b0b5ea", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "bd516db2-6009-4bba-99d2-189750fc1cd2", + "id": "9e99cf26-df0d-4a3c-8918-7b9d106ee173", "name": "nw_project", "value": "3da4d745e0e223cb", "type": "string" }, { - "id": "7ec57b28-ee76-4fdb-87e6-5e07c56cdd4d", + "id": "a2b26c3e-1622-418b-899a-fec46ef2728f", "name": "nw_engine", "value": "google.de", "type": "string" }, { - "id": "e670bd6a-6327-4570-ac3b-09baff9dc81a", + "id": "ff4d76cd-46e8-4a2c-a32c-bc84fc0a7f00", "name": "nw_language", "value": "German", "type": "string" }, { - "id": "c5671976-8b69-4788-9e36-86165d5e97f4", + "id": "75a7f1ec-01fa-4661-8f78-c149666ccc8a", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "17ab5e1f-db56-45ad-8392-ff0dfed8a965", + "id": "f389f8d3-c68d-40e2-9f41-797772f3aa11", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "3a233c4d-69b7-47cc-97f6-0dc06c2923f6", + "id": "7dc436f3-31d5-454a-ae6a-fa9866845f8d", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "83e56d69-847f-485c-9331-acffd8390fda", + "id": "efa63b44-4c3f-45f9-9cc4-b3f60d43f764", "name": "branch", "value": "main", "type": "string" }, { - "id": "9ddffd30-8245-4e0e-85c6-4ef350dc45d9", + "id": "b38db5de-7a58-41bc-8399-5a4c66843927", "name": "index_pfad", "value": "artikel-index.json", "type": "string" }, { - "id": "47ac93b9-529b-43a4-a3d1-76d83f056827", + "id": "84b0e1f0-3f1a-40d8-b27c-213eb2c02eec", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "67d79c76-ca3a-4066-97a8-6d135f55244e", + "id": "58f3fbef-d444-47fc-9943-17f7f9c18d37", "name": "poll_max", "value": 30, "type": "number" }, { - "id": "0766321e-b286-48b9-90c3-63b0f9d22a82", + "id": "098701a1-de5b-4804-a9c0-81fe9a6204d7", "name": "poll_sekunden", "value": 20, "type": "number" }, { - "id": "9effca14-631e-419d-8dba-4e5c44c1b817", + "id": "9bc5db24-3fac-48bb-a118-273198c2eab3", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "4cf2db61-c55a-43d1-8d7b-427e35ac703e", + "id": "08c47277-5335-4ea4-b48b-48a43af36fbd", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -153,7 +153,7 @@ -240, 200 ], - "id": "aba36956-c83a-44c4-9012-fe80979f8545", + "id": "2fc4b728-84b7-466c-a2ae-74bf117d5fa1", "name": "Konfiguration" }, { @@ -177,7 +177,7 @@ -20, 200 ], - "id": "0ebb8bf1-d129-4066-8c59-2b4c3b27cd52", + "id": "9d8d9368-18b3-43b0-acd5-22214e0c9aa5", "name": "Artikeltabelle lesen", "credentials": { "httpHeaderAuth": { @@ -201,7 +201,7 @@ -20, 420 ], - "id": "5093a5ca-b790-459f-bf12-5e221431323f", + "id": "0dd66fcb-5058-481b-a94d-3e06529708cf", "name": "Fehler Artikeltabelle" }, { @@ -215,7 +215,7 @@ 200, 200 ], - "id": "335206d7-0cd9-4e66-9534-bdf52618deac", + "id": "bd1be404-0967-4b39-bef3-61fbb0069745", "name": "Tabelle auswerten" }, { @@ -229,7 +229,7 @@ }, "conditions": [ { - "id": "90341bb9-1b82-49ee-8a6e-3351c7140dbe", + "id": "e9f8b220-567d-4ef9-9c9c-05ff695b6ab7", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -250,7 +250,7 @@ 420, 200 ], - "id": "74528ffd-5113-4ada-a8a9-aa4e1e8a5309", + "id": "2577013e-3a1e-4960-b5d9-f727d45c6cdc", "name": "Offene Artikel vorhanden?" }, { @@ -261,7 +261,7 @@ 640, 60 ], - "id": "c7123211-a46c-4446-a10d-5ba1083b05b2", + "id": "80b41612-8f5f-4faa-911f-b2f964dc9db0", "name": "Nichts zu tun" }, { @@ -277,7 +277,7 @@ 640, 320 ], - "id": "b690989b-0358-43aa-b7e3-efd5511b3d58", + "id": "4efca105-2742-4f2c-b248-4bd702453fe2", "name": "Artikel nacheinander" }, { @@ -307,7 +307,7 @@ 900, 420 ], - "id": "95a1f069-15c6-405e-888e-2d317891b28c", + "id": "647e03b7-5ae9-44ca-acbc-9309338b7486", "name": "Analyse starten", "credentials": { "httpHeaderAuth": { @@ -331,7 +331,7 @@ 900, 640 ], - "id": "bb797361-40f3-46da-b359-c2ce94ad3bec", + "id": "7a4abc2e-df42-4fdb-bb9b-6c2984c011c2", "name": "Fehler Analyse starten", "onError": "continueRegularOutput" }, @@ -346,7 +346,7 @@ 1120, 420 ], - "id": "5320b757-5a3f-465e-9b3f-3da46b590824", + "id": "0a808a0a-74f3-4033-b0e3-9e5dd3bac559", "name": "Poll vorbereiten" }, { @@ -360,9 +360,9 @@ 1340, 420 ], - "id": "0b0db985-8687-4b8a-b183-7a1072d8180f", + "id": "36006210-77af-41b6-9478-fbaa85729e46", "name": "Warten", - "webhookId": "dcd62081-3c28-4b1c-87b1-058ee2e774fc" + "webhookId": "febdadfe-bb41-4a36-b19c-ed9f73a49f33" }, { "parameters": { @@ -391,7 +391,7 @@ 1560, 420 ], - "id": "c2d91e7a-97cd-446d-9f59-973d2fd2e872", + "id": "58b3d12c-37c6-49f6-926a-0702ec80e38d", "name": "Analyse abfragen", "credentials": { "httpHeaderAuth": { @@ -415,7 +415,7 @@ 1560, 640 ], - "id": "32ea6fc9-45ad-4e9c-86db-5eda4a369308", + "id": "b81a94e6-6ebd-4f93-b4ea-f42f0c784d76", "name": "Fehler Analyse abfragen", "onError": "continueRegularOutput" }, @@ -430,7 +430,7 @@ }, "conditions": [ { - "id": "540396ef-e549-47e4-aab0-2658f397d1b0", + "id": "09def6f2-cc81-49ba-90c8-d445980a842b", "leftValue": "={{ $json.status }}", "rightValue": "ready", "operator": { @@ -450,7 +450,7 @@ 1780, 420 ], - "id": "3611443b-1e91-4241-8c61-3c18e3a6a3d2", + "id": "1be6257c-67fc-431d-b485-c155e0d6cd35", "name": "Analyse fertig?" }, { @@ -464,7 +464,7 @@ 1780, 660 ], - "id": "524f0b05-2fce-4223-9974-dbfd9571e9ac", + "id": "e2ca0648-f4ba-428d-8547-6bfb99869933", "name": "Versuch zaehlen" }, { @@ -478,7 +478,7 @@ }, "conditions": [ { - "id": "f6a87ddd-b2da-4164-90ff-159bf3d0e33e", + "id": "2e05e26d-97e6-42e0-bdfb-6f0594935519", "leftValue": "={{ $json.versuch }}", "rightValue": "={{ $('Konfiguration').first().json.poll_max }}", "operator": { @@ -498,7 +498,7 @@ 2000, 660 ], - "id": "5bcea09f-ad45-4347-ad0d-fcefbbae7b50", + "id": "20f7854d-52bd-4c9f-be98-21b79e242993", "name": "Poll-Limit erreicht?" }, { @@ -512,13 +512,13 @@ 2220, 780 ], - "id": "f2b2da7e-2014-460f-af19-d9d08c1c8516", + "id": "4809a2ca-50ec-4733-aacc-cb70a095510a", "name": "Timeout melden" }, { "parameters": { "mode": "runOnceForAllItems", - "jsCode": "\n// Feldnamen stammen aus einer echten /get-query-Antwort, siehe docs/feldnachweis.md\nconst a = $('Poll vorbereiten').first().json;\nconst n = $input.first().json;\n\nconst liste = (arr) => (arr || []).map(x => ({\n begriff: x.t,\n min: Array.isArray(x.sugg_usage) ? x.sugg_usage[0] : null,\n max: Array.isArray(x.sugg_usage) ? x.sugg_usage[1] : null,\n bei_wettbewerbern_pc: x.usage_pc ?? null,\n}));\n\nconst fragen = [];\nconst gesehen = new Set();\nconst addFrage = (q, quelle, gewicht) => {\n if (!q) return;\n const k = q.trim().toLowerCase();\n if (gesehen.has(k)) return;\n gesehen.add(k);\n fragen.push({ frage: q.trim(), quelle, gewicht });\n};\nObject.entries((n.ideas && n.ideas.topic_matrix) || {})\n .sort((x, y) => (y[1].importance || 0) - (x[1].importance || 0))\n .forEach(([q, v]) => addFrage(q, 'topic_matrix', v.importance || null));\n((n.ideas || {}).people_also_ask || []).forEach(x => addFrage(x.q, 'people_also_ask', null));\n((n.ideas || {}).suggest_questions || []).forEach(x => addFrage(x.q, 'suggest_questions', null));\n((n.ideas || {}).content_questions || []).forEach(x => addFrage(x.q, 'content_questions', null));\n\nconst brief = {\n // --- die vom Auftrag geforderten Felder ---\n query_id: n.query,\n keyword: n.keyword,\n begriffe_pflicht: liste((n.terms || {}).content_basic),\n begriffe_erweitert: liste((n.terms || {}).content_extended),\n fragen,\n wortzahl_ziel: ((n.metrics || {}).word_count || {}).target ?? null,\n\n // --- Artikelkontext aus der Tabelle ---\n artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel },\n\n // --- Zusatz fuer die Schreibvorlage ---\n zusatz: {\n wortzahl_median: ((n.metrics || {}).word_count || {}).median ?? null,\n lesbarkeit_ziel: ((n.metrics || {}).readability || {}).target ?? null,\n begriffe_titel: ((n.terms || {}).title || []).map(x => x.t),\n begriffe_description: ((n.terms || {}).desc || []).map(x => x.t),\n begriffe_h1: ((n.terms || {}).h1 || []).map(x => x.t),\n begriffe_h2: ((n.terms || {}).h2 || []).map(x => x.t),\n entitaeten: ((n.terms || {}).entities || []).slice(0, 15).map(x => ({ name: x.t, wichtigkeit: x.importance })),\n suchintention: (n.serp_summary || {}).top_intent ?? null,\n haeufigster_inhaltstyp: (n.serp_summary || {}).top_content_type ?? null,\n gliederungen_top5: (n.competitors || []).slice(0, 5).map(c => ({\n rang: c.rank, url: c.url, score: c.content_score, wortzahl: c.word_count,\n ueberschriften: (c.headers || []).map(h => `${h[0]}: ${h[1]}`).slice(0, 25),\n })),\n terms_txt_content_basic: ((n.terms_txt || {}).content_basic_w_ranges) ?? null,\n },\n\n schreibregeln: {\n preise: 'Preise niemals ausschreiben. Immer den Shortcode einsetzen: '\n + '[skrift_preis key=\"SCHLUESSEL\" ab=\"0|1\" brutto=\"0|1\"]. '\n + 'key ist der Produktschluessel (z. B. briefe, postkarten, porto_inland). '\n + 'ab=1 liefert den guenstigsten Staffelpreis, ab=0 den Standardpreis. '\n + 'brutto=1 inkl. MwSt., brutto=0 netto. Das Wort \"ab\" selbst gehoert in den Fliesstext, '\n + 'der Shortcode liefert nur die Zahl. Beispiel: '\n + 'Handschriftliche Briefe ab [skrift_preis key=\"briefe\" ab=\"1\"] pro Stueck. '\n + 'Ein unbekannter key ergibt eine leere Ausgabe, keinen Fehler.',\n faq: 'FAQ nicht in content_html schreiben, sondern als Feld faq mit Frage-Antwort-Paaren. '\n + 'Die Ausgabe im Beitrag uebernimmt der Shortcode [skrift_faq].',\n kurzantwort: 'Ein Absatz, der die Hauptfrage direkt beantwortet, als eigenes Feld kurzantwort.',\n },\n\n meta: {\n erzeugt_am: new Date().toISOString(),\n analyse_url: n.query_url || a.query_url || null,\n freigabe_url: n.share_url || a.share_url || null,\n sprache: n.language, suchmaschine: n.engine, projekt: n.project,\n },\n};\n\nreturn [{ json: { slug: a.slug, zeilen_index: a.zeilen_index, query: n.query,\n keyword: n.keyword, brief } }];\n" + "jsCode": "\n// Feldnamen stammen aus einer echten /get-query-Antwort, siehe docs/feldnachweis.md\nconst a = $('Poll vorbereiten').first().json;\nconst n = $input.first().json;\n\nconst liste = (arr) => (arr || []).map(x => ({\n begriff: x.t,\n min: Array.isArray(x.sugg_usage) ? x.sugg_usage[0] : null,\n max: Array.isArray(x.sugg_usage) ? x.sugg_usage[1] : null,\n bei_wettbewerbern_pc: x.usage_pc ?? null,\n}));\n\nconst fragen = [];\nconst gesehen = new Set();\nconst addFrage = (q, quelle, gewicht) => {\n if (!q) return;\n const k = q.trim().toLowerCase();\n if (gesehen.has(k)) return;\n gesehen.add(k);\n fragen.push({ frage: q.trim(), quelle, gewicht });\n};\nObject.entries((n.ideas && n.ideas.topic_matrix) || {})\n .sort((x, y) => (y[1].importance || 0) - (x[1].importance || 0))\n .forEach(([q, v]) => addFrage(q, 'topic_matrix', v.importance || null));\n((n.ideas || {}).people_also_ask || []).forEach(x => addFrage(x.q, 'people_also_ask', null));\n((n.ideas || {}).suggest_questions || []).forEach(x => addFrage(x.q, 'suggest_questions', null));\n((n.ideas || {}).content_questions || []).forEach(x => addFrage(x.q, 'content_questions', null));\n\nconst brief = {\n // --- die vom Auftrag geforderten Felder ---\n query_id: n.query,\n keyword: n.keyword,\n begriffe_pflicht: liste((n.terms || {}).content_basic),\n begriffe_erweitert: liste((n.terms || {}).content_extended),\n fragen,\n wortzahl_ziel: ((n.metrics || {}).word_count || {}).target ?? null,\n\n // --- Artikelkontext aus der Tabelle ---\n artikel: { slug: a.slug, titel: a.titel, kategorie: a.kategorie, cta_ziel: a.cta_ziel,\n praxis: Array.isArray(a.praxis) ? a.praxis : [] },\n\n // --- Zusatz fuer die Schreibvorlage ---\n zusatz: {\n wortzahl_median: ((n.metrics || {}).word_count || {}).median ?? null,\n lesbarkeit_ziel: ((n.metrics || {}).readability || {}).target ?? null,\n begriffe_titel: ((n.terms || {}).title || []).map(x => x.t),\n begriffe_description: ((n.terms || {}).desc || []).map(x => x.t),\n begriffe_h1: ((n.terms || {}).h1 || []).map(x => x.t),\n begriffe_h2: ((n.terms || {}).h2 || []).map(x => x.t),\n entitaeten: ((n.terms || {}).entities || []).slice(0, 15).map(x => ({ name: x.t, wichtigkeit: x.importance })),\n suchintention: (n.serp_summary || {}).top_intent ?? null,\n haeufigster_inhaltstyp: (n.serp_summary || {}).top_content_type ?? null,\n gliederungen_top5: (n.competitors || []).slice(0, 5).map(c => ({\n rang: c.rank, url: c.url, score: c.content_score, wortzahl: c.word_count,\n ueberschriften: (c.headers || []).map(h => `${h[0]}: ${h[1]}`).slice(0, 25),\n })),\n terms_txt_content_basic: ((n.terms_txt || {}).content_basic_w_ranges) ?? null,\n },\n\n schreibregeln: {\n preise: 'Preise niemals ausschreiben. Immer den Shortcode einsetzen: '\n + '[skrift_preis key=\"SCHLUESSEL\" ab=\"0|1\" brutto=\"0|1\"]. '\n + 'key ist der Produktschluessel (z. B. briefe, postkarten, porto_inland). '\n + 'ab=1 liefert den guenstigsten Staffelpreis, ab=0 den Standardpreis. '\n + 'brutto=1 inkl. MwSt., brutto=0 netto. Das Wort \"ab\" selbst gehoert in den Fliesstext, '\n + 'der Shortcode liefert nur die Zahl. Beispiel: '\n + 'Handschriftliche Briefe ab [skrift_preis key=\"briefe\" ab=\"1\"] pro Stueck. '\n + 'Ein unbekannter key ergibt eine leere Ausgabe, keinen Fehler.',\n faq: 'FAQ nicht in content_html schreiben, sondern als Feld faq mit Frage-Antwort-Paaren. '\n + 'Die Ausgabe im Beitrag uebernimmt der Shortcode [skrift_faq].',\n kurzantwort: 'Ein Absatz, der die Hauptfrage direkt beantwortet, als eigenes Feld kurzantwort.',\n praxis: 'Der Artikel baut auf den Stichpunkten in artikel.praxis auf. Jeder Stichpunkt kommt '\n + 'erkennbar im Text vor, als Beispiel, Erfahrung oder klare Empfehlung. Ist die Liste leer, '\n + 'wird der Artikel nicht geschrieben, sondern zuerst Praxiswissen bei Lucas erfragt.',\n begriffe: 'begriffe_pflicht sind verbindlich. begriffe_erweitert nur verwenden, wenn der Satz '\n + 'dadurch nicht schlechter wird. Keine Saetze, die nur fuer den NeuronWriter-Score da sind. '\n + 'Keine Wettbewerbernamen. Zielwert im Import ist 65.',\n stil: 'Kurze und lange Saetze mischen. Absaetze unterschiedlich lang. Keine Formeln wie '\n + '\"Genau das\", \"Der Grund ist\", \"nicht nur, sondern auch\", \"entscheidend ist\", '\n + '\"der groesste Hebel\", \"macht den Unterschied\". Keine Aufzaehlung mit erstens, zweitens. '\n + 'Keine rhetorische Frage mit Sofortantwort. Keine Behauptung ohne Beleg, also kein '\n + '\"erfahrungsgemaess\" oder \"in aller Regel\". Wo es passt, auch abraten: wann sich '\n + 'Handschrift nicht lohnt.',\n },\n\n meta: {\n erzeugt_am: new Date().toISOString(),\n analyse_url: n.query_url || a.query_url || null,\n freigabe_url: n.share_url || a.share_url || null,\n sprache: n.language, suchmaschine: n.engine, projekt: n.project,\n },\n};\n\nreturn [{ json: { slug: a.slug, zeilen_index: a.zeilen_index, query: n.query,\n keyword: n.keyword, brief } }];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -526,7 +526,7 @@ 2220, 300 ], - "id": "e7c0babd-988d-4a5a-9cdd-ba6de4ecf07e", + "id": "e205635c-b327-4cc2-abf8-585485a73d68", "name": "Brief bauen" }, { @@ -551,7 +551,7 @@ 2440, 300 ], - "id": "537e73ab-e8a5-44a5-b88d-1052ccd1bc61", + "id": "dcfe6d64-d36d-4730-b33a-2f8f5693d770", "name": "Brief vorhanden?", "credentials": { "httpHeaderAuth": { @@ -571,7 +571,7 @@ 2660, 300 ], - "id": "8a18d86d-fda2-4f65-bed9-7deda83a1acb", + "id": "39308915-ea33-42d4-bbe3-67786dfbd2ed", "name": "Commit vorbereiten" }, { @@ -601,7 +601,7 @@ 2880, 300 ], - "id": "db44ac17-59f2-437a-bb7d-94aa4da5a074", + "id": "972ef68d-55db-45f0-9b32-0647607e535f", "name": "Brief committen", "credentials": { "httpHeaderAuth": { @@ -625,7 +625,7 @@ 2880, 520 ], - "id": "31a3c38b-c79a-42cb-9ac1-6710deb2fd02", + "id": "e9beac3d-0306-45d1-a547-0fcddb091810", "name": "Fehler Brief committen", "onError": "continueRegularOutput" }, @@ -640,7 +640,7 @@ 3100, 300 ], - "id": "5d243ba5-1e7e-4f16-8037-9eed05687643", + "id": "b740cc47-e748-4ff4-be7f-9128b06ad303", "name": "Erfolg merken" }, { @@ -654,9 +654,9 @@ 3320, 400 ], - "id": "997a2d3f-ddca-4081-9ded-2eb521163b1b", + "id": "dc146423-d4db-4a68-8061-cd123d286199", "name": "Pause Rate Limit", - "webhookId": "3f510a7a-22c1-4248-a7c1-60fe57786eb2" + "webhookId": "cd90d1e6-1533-4c3b-bc64-46e9e69546b0" }, { "parameters": { @@ -669,7 +669,7 @@ 900, 140 ], - "id": "8f0de534-6cea-40af-bf64-e63651648df7", + "id": "9d684894-aae3-4fe5-a312-ee6daa993ece", "name": "Ergebnisse sammeln" }, { @@ -683,7 +683,7 @@ 1120, 140 ], - "id": "9597c915-36d3-4f94-af9d-60351fa4ac41", + "id": "e6350ae8-2d5d-47f6-ae57-b038ade331ac", "name": "Statusupdate vorbereiten" }, { @@ -713,7 +713,7 @@ 1340, 140 ], - "id": "a81137d7-c063-4f56-ac56-2ac6fffa9560", + "id": "a2212ee0-aa8a-41e2-a684-f30db2eadf16", "name": "Status zurueckschreiben", "credentials": { "httpHeaderAuth": { @@ -737,7 +737,7 @@ 1340, -60 ], - "id": "35cff298-1691-452f-bc25-a225ee241a6e", + "id": "f9fb0933-c769-4722-b8e9-02b4169a8046", "name": "Fehler Statusupdate" }, { @@ -751,7 +751,7 @@ 1560, 140 ], - "id": "ad2d0735-fd21-4181-bb7f-8bc51bd01435", + "id": "3f84a5c1-eedf-4a0d-be53-d0969c687aa5", "name": "Meldung formulieren" }, { @@ -769,7 +769,7 @@ 1780, 140 ], - "id": "03d899b0-4d69-4105-a755-eaa1792d7497", + "id": "c7d18957-008d-4492-87fc-bdb547cc8823", "name": "Benachrichtigung", "disabled": true, "notes": "SMTP-Credential zuweisen, Absender eintragen, dann Node aktivieren." diff --git a/workflows/pruefen-und-importieren.json b/workflows/pruefen-und-importieren.json index c1df659..a9b44f9 100644 --- a/workflows/pruefen-und-importieren.json +++ b/workflows/pruefen-und-importieren.json @@ -9,7 +9,7 @@ -460, 100 ], - "id": "793c9880-e989-44d5-b514-d6850b64fd2e", + "id": "633f2486-1cde-41d1-b4cd-4e24a1a1d75f", "name": "Manuell starten" }, { @@ -25,9 +25,9 @@ -460, 280 ], - "id": "0bb95de2-96dd-4f8b-af1e-3bab20eca241", + "id": "21be5d6d-b7fd-4268-a79e-c5203e0d1864", "name": "Webhook Start", - "webhookId": "53c83399-7059-4816-8f7c-c606015c02c0" + "webhookId": "daec5bda-055a-416e-85a7-085b1a644a70" }, { "parameters": { @@ -35,91 +35,91 @@ "assignments": { "assignments": [ { - "id": "f161eded-c23e-4d60-9d8b-d4db483ec3d9", + "id": "77fdf721-4ee6-4f16-86aa-a44ccd53c30e", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "0544c6b7-e388-4bb4-b418-2dc97bc56bb6", + "id": "833ef6fb-7143-4a87-9f21-e734d263dba1", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "46b15bb0-7e1a-48f5-addb-e8ca1a46491a", + "id": "48ba9170-6828-4986-b0bb-9a8ff1b96f39", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "46eb7849-83d0-4cd2-b978-6a49c4ddb31e", + "id": "58ba48b6-3e92-4796-a905-760b4d49fabd", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "84f7d295-04fa-4f95-b723-98f0dc1a042b", + "id": "8adca23e-46c1-4431-95b8-39836573b510", "name": "branch", "value": "main", "type": "string" }, { - "id": "1bd8cf46-e184-47c5-897a-aa4b4d08d591", + "id": "e131a743-ace6-41b2-a676-3265b3c1adba", "name": "artikel_ordner", "value": "artikel", "type": "string" }, { - "id": "1c6a8050-c17c-4635-b371-cc5a40fda4e5", + "id": "8e16537a-e986-4b7d-9e13-19fed65045b1", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "9eb50d4b-0ac5-4bd9-8733-fe1bcb82b601", + "id": "0d4e2fcb-a677-4be7-8f92-4cad4cdb2a6a", "name": "nachbessern_ordner", "value": "nachbessern", "type": "string" }, { - "id": "91ec4a89-c907-403d-aaa4-77e074a85992", + "id": "8081b290-1356-4d17-a4da-9aec977846ae", "name": "status_pfad", "value": "status-importe.json", "type": "string" }, { - "id": "7e6d86e6-4195-4cdd-b3b2-1ee1fceb9805", + "id": "58a53681-c4b6-41db-8ee0-ef2192c11801", "name": "wp_base", "value": "https://skrift.de", "type": "string" }, { - "id": "405fcf78-a5a0-4fb8-9f04-d8041906f2b3", + "id": "f48b0d70-1acb-432f-8773-768c2cd708fe", "name": "score_ziel", - "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 72 }}", + "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}", "type": "number" }, { - "id": "4e9becf4-a380-407b-b4b9-b8b821e2992f", + "id": "1b83a8b2-ab79-4da1-a6aa-60e932a0e239", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "11c7167c-f60e-47c6-9832-9ef0124628c2", + "id": "da8882a2-1c90-4d3e-80c2-fcf4553033c6", "name": "elementor_ausgabe", "value": false, "type": "boolean" }, { - "id": "0a4b7c2b-14f5-43cb-ae2a-64d30d58a1f4", + "id": "06e2454a-7586-4810-8511-dae3cd4c6fad", "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string" }, { - "id": "debc59ab-4e8e-44de-80c2-334c15fc55ac", + "id": "b22de2bd-ccd9-4933-87cc-0f30af3da4f4", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -134,7 +134,7 @@ -240, 190 ], - "id": "86f2b355-8a1a-4336-b2c9-1a2cbb0aa6f1", + "id": "04845694-bb6d-45e7-b215-0eb26088e59c", "name": "Konfiguration" }, { @@ -159,7 +159,7 @@ -20, 190 ], - "id": "144a0300-78e5-4f50-828c-0e0dc617947c", + "id": "3a9a66ee-3f95-474a-9d6c-19848c19e2f9", "name": "Artikelordner lesen", "credentials": { "httpHeaderAuth": { @@ -182,7 +182,7 @@ 200, 190 ], - "id": "03942d18-af94-439b-89a9-d9734d845f83", + "id": "39b7d343-8133-49b9-937e-a207772217c0", "name": "Artikeldateien filtern" }, { @@ -196,7 +196,7 @@ }, "conditions": [ { - "id": "dd545030-181b-40c1-ba1c-563e93f9cdf9", + "id": "9cb6ba6d-2a92-4422-812b-f7f6f9f7aab0", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -217,7 +217,7 @@ 420, 190 ], - "id": "ac2420c7-c498-4e3e-a26e-542f7d585a35", + "id": "dbdc7d6a-1ff5-4038-bc36-9d34e28d10bf", "name": "Artikel vorhanden?" }, { @@ -228,7 +228,7 @@ 640, 40 ], - "id": "1f77ee2a-d8e9-4bcf-9ac9-5f1c1772d5ac", + "id": "4808a51f-d327-416f-a1f2-f197718d7d38", "name": "Nichts zu tun" }, { @@ -244,7 +244,7 @@ 640, 320 ], - "id": "11279052-fb96-4d7b-b03a-4a64b524ab30", + "id": "4a19481b-6597-408b-a3ce-c709d425a81f", "name": "Artikel nacheinander" }, { @@ -268,7 +268,7 @@ 900, 420 ], - "id": "d36af43a-e39d-4e20-9ef8-00101a0dfef5", + "id": "8dd49c8e-760a-49a7-94ab-05b60973dce3", "name": "Artikeldatei laden", "credentials": { "httpHeaderAuth": { @@ -292,7 +292,7 @@ 900, 640 ], - "id": "49f78b05-7b28-4ca6-be18-fb0fb27cfcb9", + "id": "767789d0-15bc-4cb1-b15e-7f8c3941e4ec", "name": "Fehler Artikeldatei", "onError": "continueRegularOutput" }, @@ -318,7 +318,7 @@ 1120, 420 ], - "id": "88ea6484-2c70-4e73-870f-404eb5d43676", + "id": "d7ef6a1e-ff5e-4e58-88bf-9a941202d796", "name": "Brief laden", "credentials": { "httpHeaderAuth": { @@ -330,7 +330,7 @@ { "parameters": { "mode": "runOnceForAllItems", - "jsCode": "\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 => `

${f.frage || ''}

${f.antwort || ''}

`).join('\\n');\nconst bewertungs_html = [\n artikel.kurzantwort ? `

${artikel.kurzantwort}

` : '',\n artikel.content_html || '',\n faqText ? `

Häufige Fragen

\\n${faqText}` : '',\n].filter(Boolean).join('\\n');\n\nreturn [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief,\n bewertungs_html,\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" + "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(/ /g, ' ').replace(/\\s+/g, ' ').trim();\n\n const html = String(artikel.content_html || '');\n const absaetze = [...html.matchAll(/

([\\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 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 => `

${f.frage || ''}

${f.antwort || ''}

`).join('\\n');\nconst bewertungs_html = [\n artikel.kurzantwort ? `

${artikel.kurzantwort}

` : '',\n artikel.content_html || '',\n faqText ? `

Häufige Fragen

\\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, @@ -338,7 +338,7 @@ 1340, 420 ], - "id": "d0f641c2-78a7-4b25-addb-05044edbed6e", + "id": "c80f6298-4952-4fb3-a739-923dc94ef20f", "name": "Artikel pruefen" }, { @@ -368,7 +368,7 @@ 1560, 420 ], - "id": "a7eb667d-7eec-4a65-8d90-206084368755", + "id": "cce9d7ae-6b36-4990-84a7-08bdc224ce9b", "name": "Score ermitteln", "credentials": { "httpHeaderAuth": { @@ -392,7 +392,7 @@ 1560, 640 ], - "id": "902fc43d-9265-43ac-9c0b-a75a9c7da079", + "id": "a6d0d95f-44b7-4af6-9164-854759f98827", "name": "Fehler Score", "onError": "continueRegularOutput" }, @@ -407,7 +407,7 @@ 1780, 420 ], - "id": "8e45bb62-1738-4149-bd65-96b671fc3186", + "id": "98f0d1f9-be01-43fd-8d6b-84b5466f7b67", "name": "Score auswerten" }, { @@ -421,7 +421,7 @@ }, "conditions": [ { - "id": "b6a9240e-c1a2-4178-ae65-05534281a1a9", + "id": "10c43145-9910-4bee-b483-18a415053cd5", "leftValue": "={{ $json.bestanden }}", "rightValue": "", "operator": { @@ -442,13 +442,13 @@ 2000, 420 ], - "id": "d240d88f-009d-423b-a781-6fd6317f0c99", + "id": "9c9e30a0-78c2-48e7-a7f9-357f27f8b491", "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(//gi, ' ')\n .replace(//gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /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 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};\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" + "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(//gi, ' ')\n .replace(//gi, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/ /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, @@ -456,7 +456,7 @@ 2220, 640 ], - "id": "3dfd0f0e-f104-44d2-a44b-3cd7705d4833", + "id": "40728217-116b-435a-b758-4a2e6fe1ccba", "name": "Fehlende Begriffe ermitteln" }, { @@ -481,7 +481,7 @@ 2440, 640 ], - "id": "2320c737-09a9-4d44-8e70-9be83b3d2008", + "id": "be1e9b88-7028-4be3-a244-b6250cc6ccfe", "name": "Nachbesserung vorhanden?", "credentials": { "httpHeaderAuth": { @@ -501,7 +501,7 @@ 2660, 640 ], - "id": "ce049aa8-6b87-4fd3-be30-ff5e9ae85e88", + "id": "2e63f51d-9f55-4131-8cd6-dafb4853f299", "name": "Nachbesserung vorbereiten" }, { @@ -531,7 +531,7 @@ 2880, 640 ], - "id": "e16e2245-e642-42f3-91e1-1149d52dd502", + "id": "daba34d3-b8a9-411c-b4e4-a7d6e2e35857", "name": "Nachbesserung committen", "credentials": { "httpHeaderAuth": { @@ -555,14 +555,14 @@ 2880, 860 ], - "id": "88172a57-71cf-4c76-a148-3e625df08e72", + "id": "126ce968-3042-4146-b7b5-26989b1963f4", "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 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" + "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, @@ -570,7 +570,7 @@ 3100, 640 ], - "id": "3d34a3d6-1ce3-41a2-aa66-b9e5d9065708", + "id": "f7c474ee-61fe-4579-b24c-f4d0fae505bf", "name": "Nachbesserung gemerkt" }, { @@ -600,7 +600,7 @@ 2220, 240 ], - "id": "8bd24b62-3161-4596-9dc0-9c73a434ffe8", + "id": "a66b7ee4-3c32-4872-a4d8-8e0ca7358e7c", "name": "Revision zu NeuronWriter", "credentials": { "httpHeaderAuth": { @@ -624,7 +624,7 @@ 2220, 60 ], - "id": "295a6ba5-75f4-4807-8090-23720da69936", + "id": "06e563e9-5029-49ab-a0ab-c644615f7955", "name": "Fehler Revision", "onError": "continueRegularOutput" }, @@ -650,7 +650,7 @@ 2440, 240 ], - "id": "01befefc-e4ef-48a5-943f-6aa732736c99", + "id": "964f3bc4-4ed7-4a1c-9c75-497d967f6203", "name": "Beitrag vorhanden?", "credentials": { "httpBasicAuth": { @@ -684,7 +684,7 @@ 2660, 240 ], - "id": "70e77372-0d3f-4c68-815b-ddb13d7f18e8", + "id": "010c6447-49f0-4347-9e47-26a1b9b2599e", "name": "Kategorie suchen", "credentials": { "httpBasicAuth": { @@ -707,7 +707,7 @@ 2880, 240 ], - "id": "a0b6cc70-8253-4317-af8c-52da2a11d384", + "id": "ab01a3a4-a5ea-420b-8815-2a7f3853ac97", "name": "Kategorie pruefen" }, { @@ -721,7 +721,7 @@ }, "conditions": [ { - "id": "88bdb3bb-b27e-4f62-a3e3-81ff95173268", + "id": "2a54ed83-87d2-4927-9351-585a15b086fe", "leftValue": "={{ $json.kategorie_fehlt }}", "rightValue": "", "operator": { @@ -742,7 +742,7 @@ 3100, 240 ], - "id": "fd48f6d4-961a-4219-a245-55582aefbbce", + "id": "2ca6b6c9-c48f-4003-8e5c-d5c558e26554", "name": "Kategorie anlegen?" }, { @@ -772,7 +772,7 @@ 3320, 140 ], - "id": "4a56f882-d5f3-463c-aaa3-c94414f8c214", + "id": "897b9835-b709-4a52-9d55-0a3e8265bc9e", "name": "Kategorie anlegen", "credentials": { "httpBasicAuth": { @@ -795,7 +795,7 @@ 3540, 240 ], - "id": "4f8157e0-d898-49dd-a112-863e63ffa550", + "id": "be2c0e3e-9700-4fab-a3af-685fbcd711c1", "name": "Kategorie festlegen" }, { @@ -809,7 +809,7 @@ 3760, 240 ], - "id": "fdb757fc-b0bc-4def-8a93-0e679a669b9e", + "id": "601294c9-7988-41c4-b0c7-dea7354b4f1e", "name": "WordPress-Body bauen" }, { @@ -839,7 +839,7 @@ 3980, 240 ], - "id": "af5dc8ab-bcae-4b0c-975e-e7535ab616f4", + "id": "63f22031-35bc-4ff4-8fc4-c6de41d1b776", "name": "Beitrag anlegen oder aktualisieren", "credentials": { "httpBasicAuth": { @@ -854,7 +854,7 @@ { "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 preis_shortcodes: v.preis_shortcodes || 0,\n}}];\n" + "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, @@ -862,7 +862,7 @@ 4200, 240 ], - "id": "d1513f84-dd96-41dd-981d-068eb0e015bb", + "id": "06bdadc1-6c10-4383-9f1d-cc8c312256e9", "name": "Ergebnis pruefen" }, { @@ -893,7 +893,7 @@ 4420, 240 ], - "id": "3039ffe1-f6a5-43e7-bd80-a7818a75f458", + "id": "97bb7a3f-3f36-4423-9156-d276cd9676f8", "name": "Cache leeren", "credentials": { "httpBasicAuth": { @@ -913,7 +913,7 @@ 4640, 240 ], - "id": "061fc5f3-7a21-4dc0-87bb-e1c789ca31fc", + "id": "57f9fd78-0437-4097-b7ed-ac16c26a3739", "name": "Importergebnis merken" }, { @@ -927,9 +927,9 @@ 4860, 420 ], - "id": "92a3b5c9-ffb9-4db8-90b0-664c7a252f55", + "id": "878e3e55-af85-476e-8fcb-a30a2d995d8c", "name": "Pause Rate Limit", - "webhookId": "6c1f5f05-129e-4a6f-88c2-18d3580177ac" + "webhookId": "a6852e0e-1624-45d4-90fb-e688e5ed3c82" }, { "parameters": { @@ -942,7 +942,7 @@ 900, 80 ], - "id": "b5e9fe9f-14ec-4ceb-9e90-dfc7a2157e04", + "id": "623461c0-10e9-487e-8f20-d9f0face6b66", "name": "Ergebnisse sammeln" }, { @@ -967,7 +967,7 @@ 1120, 80 ], - "id": "0e50466e-1a53-4df6-9e69-6de44bc287a6", + "id": "35ab12d7-6af2-4286-9bc7-2a46c8a53cc5", "name": "Statustabelle lesen", "credentials": { "httpHeaderAuth": { @@ -987,7 +987,7 @@ 1340, 80 ], - "id": "d1ff39f4-f404-4857-bb5d-43261f32299e", + "id": "7549b60e-d346-4153-a734-838f5988cd30", "name": "Statustabelle fortschreiben" }, { @@ -1001,7 +1001,7 @@ }, "conditions": [ { - "id": "95cee571-76cd-4c6b-bdff-cbee60c28afc", + "id": "14a7b5fe-74f7-41ee-a6f5-48600ec9100b", "leftValue": "={{ $json.ueberspringen }}", "rightValue": "", "operator": { @@ -1022,7 +1022,7 @@ 1560, 80 ], - "id": "627c4837-2d98-430f-9b81-ad04d9b671c7", + "id": "9a16f5ca-599b-49f2-9d41-c8dc32c311db", "name": "Gab es Importe?" }, { @@ -1052,7 +1052,7 @@ 1780, 160 ], - "id": "6de88f1b-0e69-4e99-bf3d-5ec74563da7d", + "id": "7c77256e-630c-4f66-a2f2-c9c2e85c9770", "name": "Statustabelle schreiben", "credentials": { "httpHeaderAuth": { @@ -1076,14 +1076,14 @@ 1780, 340 ], - "id": "ae7ea0ae-b523-42a4-8716-6c1e183407e8", + "id": "685e45ab-4efe-4c15-81a1-3a467842059e", "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.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 });\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" + "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, @@ -1091,7 +1091,7 @@ 2000, 80 ], - "id": "ab3897b8-80dc-4efe-99ec-fd7205a9017f", + "id": "72b474fe-d5dc-44d1-b384-b03ddb3fff04", "name": "Meldung formulieren" }, { @@ -1109,7 +1109,7 @@ 2220, 80 ], - "id": "3856017f-eb4c-463b-ac03-af1d33e478c1", + "id": "373fcd15-abe9-464e-9233-289c523e4eb2", "name": "Benachrichtigung", "disabled": true, "notes": "SMTP-Credential zuweisen, Absender eintragen, dann aktivieren."