diff --git a/README.md b/README.md index 07c3235..8c31ee1 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,10 @@ und sollte dort bleiben. ### Schreibregeln fuer content_html -**Preise niemals ausschreiben.** Sie werden zentral gepflegt und per Shortcode -eingesetzt: +**Preise niemals ausschreiben, immer inkl. MwSt.** Sie werden zentral gepflegt +und per Shortcode eingesetzt, immer mit `brutto="1"`. Hinter dem Preis steht +"inkl. 19 % MwSt.", nie "netto", "brutto" oder "zzgl.". Die Stilpruefung meldet +Shortcodes ohne `brutto="1"` und jedes "netto"/"brutto" im Text. ``` [skrift_preis key="SCHLUESSEL" ab="0|1" brutto="0|1"] @@ -246,7 +248,7 @@ reine Zahl. Das Wort "ab" gehoert in den Fliesstext, der Shortcode liefert nur die Zahl: ``` -Handschriftliche Briefe ab [skrift_preis key="briefe" ab="1"] pro Stueck. +Handschriftliche Briefe ab [skrift_preis key="briefe" ab="1" brutto="1"] pro Stueck inkl. 19 % MwSt. ``` Ein unbekannter `key` erzeugt eine leere Ausgabe, keinen Fehler. diff --git a/build/gen_wf1.py b/build/gen_wf1.py index 3bf28c5..6ae37c9 100644 --- a/build/gen_wf1.py +++ b/build/gen_wf1.py @@ -298,14 +298,12 @@ const brief = { }, schreibregeln: { - preise: 'Preise niemals ausschreiben. Immer den Shortcode einsetzen: ' - + '[skrift_preis key="SCHLUESSEL" ab="0|1" brutto="0|1"]. ' - + 'key ist der Produktschluessel (z. B. briefe, postkarten, porto_inland). ' - + 'ab=1 liefert den guenstigsten Staffelpreis, ab=0 den Standardpreis. ' - + 'brutto=1 inkl. MwSt., brutto=0 netto. Das Wort "ab" selbst gehoert in den Fliesstext, ' - + 'der Shortcode liefert nur die Zahl. Beispiel: ' - + 'Handschriftliche Briefe ab [skrift_preis key="briefe" ab="1"] pro Stueck. ' - + 'Ein unbekannter key ergibt eine leere Ausgabe, keinen Fehler.', + preise: 'Preise niemals ausschreiben. Immer den Shortcode einsetzen, immer inklusive MwSt.: ' + + '[skrift_preis key="SCHLUESSEL" ab="0|1" brutto="1"]. ' + + 'key ist der Produktschluessel (briefe, postkarten). ab=1 liefert den guenstigsten Staffelpreis, ' + + 'ab=0 den Standardpreis. brutto ist immer 1. Hinter dem Preis steht immer "inkl. 19 % MwSt.", ' + + 'nie "netto", "brutto" oder "zzgl.". Das Wort "ab" gehoert in den Fliesstext. Beispiel: ' + + 'Handschriftliche Briefe ab [skrift_preis key="briefe" ab="1" brutto="1"] pro Stueck inkl. 19 % MwSt.', 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.', diff --git a/build/stilpruefung.js b/build/stilpruefung.js index c6c9dd6..5ba23f2 100644 --- a/build/stilpruefung.js +++ b/build/stilpruefung.js @@ -84,6 +84,15 @@ function stilpruefung(artikel) { add('Kein Praxiswissen hinterlegt', 'Feld "praxis" in artikel-index.json fuellen, dann den Artikel darauf aufbauen'); } + // 7b) Preise: immer inkl. MwSt., Formulierung "inkl. 19 % MwSt." + const alleTexte = [String(artikel.kurzantwort || ''), html, + ...(Array.isArray(artikel.faq) ? artikel.faq.map(f => String(f.antwort || '')) : [])].join(' '); + (alleTexte.match(/\[skrift_preis[^\]]*\]/g) || []).forEach(s => { + if (!/brutto="1"/.test(s)) add('Preis ohne MwSt.', `${s} braucht brutto="1"`); + }); + const nb = ohneTags(alleTexte.replace(/\[skrift_preis[^\]]*\]/g, 'PREIS')).match(/[^.]{0,40}\b(netto|brutto|zzgl\.?|zuzüglich)\b[^.]{0,30}/gi) || []; + nb.forEach(s => add('Preisangabe: "netto"/"brutto" statt "inkl. 19 % MwSt."', s.trim())); + // 8) Interne Links: primaer auf Business-Leistungsseiten const B = 'https://skrift.de/'; const BUSINESS = ['handgeschriebene-businessbriefe', 'handgeschriebene-postkarten', diff --git a/workflows/brief-vorbereiten.json b/workflows/brief-vorbereiten.json index b25b9e0..f8170a9 100644 --- a/workflows/brief-vorbereiten.json +++ b/workflows/brief-vorbereiten.json @@ -9,7 +9,7 @@ -460, 40 ], - "id": "e753db60-1139-4873-bc3f-33ce61502a0c", + "id": "4fdca0a5-7851-4f7b-9f3c-276418464202", "name": "Manuell starten" }, { @@ -33,7 +33,7 @@ -460, 200 ], - "id": "99319fb1-8e86-4e82-b192-3cb1dfc387cd", + "id": "f9adf55b-7fff-42e9-b6da-d57af4835257", "name": "Zeitplan Montag 07:00", "disabled": true }, @@ -50,9 +50,9 @@ -460, 360 ], - "id": "657d9657-3a90-4a83-b9db-adcc26ca3276", + "id": "208692e2-bdc6-4d22-95e7-bad567ee6b74", "name": "Webhook Start", - "webhookId": "84a8da24-b2b3-43b6-b0ae-f61e0b09b798" + "webhookId": "75113a14-9f13-4021-a15d-db58b2a57fd8" }, { "parameters": { @@ -60,85 +60,85 @@ "assignments": { "assignments": [ { - "id": "4cd0bc00-7d77-4297-92f2-2615b8e3ee30", + "id": "4ddbf6e7-68d3-4e8b-bb73-d1f63f26d6e2", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "7652c6df-df16-48d9-bc02-970407a0056c", + "id": "a35095d9-6dfd-4f04-a506-d7694a65f570", "name": "nw_project", "value": "3da4d745e0e223cb", "type": "string" }, { - "id": "2df2405a-8a94-412d-99f0-c03734764ff9", + "id": "24dcf02e-1af6-4275-bc00-f602b4b84356", "name": "nw_engine", "value": "google.de", "type": "string" }, { - "id": "29a4fac5-c86c-452a-9534-388fee8f0a00", + "id": "b35bfea0-9e67-4844-973d-a7ba42853675", "name": "nw_language", "value": "German", "type": "string" }, { - "id": "d62d3a56-9f07-49f0-9dee-4016c0d0c59c", + "id": "399912b4-6998-4882-9dc6-da19fe6863a2", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "c8918f84-6424-48bb-8153-daec31e5d3fc", + "id": "7729e907-6ba5-419e-b90d-82cba600bd1f", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "f316e4e7-757b-4163-b061-339796429248", + "id": "0315283f-06eb-460b-af29-5ba407f35e5e", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "74912d70-7d7c-41e2-9ac2-84865e460cf6", + "id": "1bef1534-8459-4915-8b19-889c1103cf15", "name": "branch", "value": "main", "type": "string" }, { - "id": "66c2f30e-0458-4251-974b-a8695c7e8b04", + "id": "cf49930f-99c3-4889-9894-850a21c174b2", "name": "index_pfad", "value": "artikel-index.json", "type": "string" }, { - "id": "1ce974ed-b971-497d-a246-eec21bfd6d11", + "id": "2a3ab33d-64bf-466e-85f3-fafb1df95b4d", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "e1dafef4-0eb6-45c0-acf2-8c7b543b7a02", + "id": "c8a33c09-b371-4fa6-adb8-1c4fb831739b", "name": "poll_max", "value": 30, "type": "number" }, { - "id": "d010e1c9-54a7-4ef9-b0db-517fe2d74b9b", + "id": "a2d67260-b199-4a4c-a2c2-5f32d93a80ff", "name": "poll_sekunden", "value": 20, "type": "number" }, { - "id": "8525830a-5432-455c-98a2-25c158c685a6", + "id": "5bddb3d2-44c6-4a5a-987e-697a303d8ba7", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "8353a3fc-a8d3-421a-8ae7-f400ab6e0fba", + "id": "bd4fd63e-5a72-4892-a2e0-481ed234ba11", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -153,7 +153,7 @@ -240, 200 ], - "id": "8aa9183c-7dfe-4bbb-8000-40c04b4edd53", + "id": "4e6479bc-7e41-432e-b38b-abd3905ded3a", "name": "Konfiguration" }, { @@ -177,7 +177,7 @@ -20, 200 ], - "id": "00c9281c-69fe-4f0e-87fc-c5588d4c5349", + "id": "748c30f8-635c-4f7a-9a7c-e2045e8ce423", "name": "Artikeltabelle lesen", "credentials": { "httpHeaderAuth": { @@ -201,7 +201,7 @@ -20, 420 ], - "id": "df2817d6-08d6-4c2f-9760-beabec0b1b6f", + "id": "5a72faa0-c8df-4a53-8981-aab5e7e1cf7e", "name": "Fehler Artikeltabelle" }, { @@ -215,7 +215,7 @@ 200, 200 ], - "id": "b8e741f7-f470-4047-8e46-a08641aa2ba6", + "id": "30c7c0d8-e179-41b1-9291-6ca1e158f232", "name": "Tabelle auswerten" }, { @@ -229,7 +229,7 @@ }, "conditions": [ { - "id": "37d71caa-2ff5-4431-b4cb-6a0cbfbdf78f", + "id": "b8fe7382-0842-4295-b498-b0a9c7021226", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -250,7 +250,7 @@ 420, 200 ], - "id": "f2611812-b7b0-4b3d-aef1-797baffaa2de", + "id": "42fa04a3-7b12-4a2e-b646-f22316415ac6", "name": "Offene Artikel vorhanden?" }, { @@ -261,7 +261,7 @@ 640, 60 ], - "id": "cfaa62aa-bb54-4b44-9739-436b1cffad88", + "id": "baa73a73-d519-4722-8a6f-22e17f37dcbb", "name": "Nichts zu tun" }, { @@ -277,7 +277,7 @@ 640, 320 ], - "id": "fd897525-a406-457a-ae4c-778194fd1f0a", + "id": "976aa62b-ce93-4677-88a4-48584933aad7", "name": "Artikel nacheinander" }, { @@ -307,7 +307,7 @@ 900, 420 ], - "id": "eba9ec08-14d7-4c79-9383-64ed222e135a", + "id": "d0b0e12c-2e55-4e8b-abb9-678716695c6c", "name": "Analyse starten", "credentials": { "httpHeaderAuth": { @@ -331,7 +331,7 @@ 900, 640 ], - "id": "3668b98c-65db-405c-9d21-537ad4df4d3f", + "id": "4d94bedf-6bbe-4f90-897d-2302a68c2abf", "name": "Fehler Analyse starten", "onError": "continueRegularOutput" }, @@ -346,7 +346,7 @@ 1120, 420 ], - "id": "d2430eff-230f-4be5-ba0d-e4d1a4ab4ba9", + "id": "47460438-b146-4705-bf0a-3719ca66af05", "name": "Poll vorbereiten" }, { @@ -360,9 +360,9 @@ 1340, 420 ], - "id": "becfbd1d-291f-4294-b1dd-a0a224b22de8", + "id": "4ec0f013-a163-46da-b7b1-02e4b73dd9dd", "name": "Warten", - "webhookId": "83211d11-5e8d-4c8e-91ec-b5112d663862" + "webhookId": "ac9b591c-58ac-40b4-bd94-716826479d88" }, { "parameters": { @@ -391,7 +391,7 @@ 1560, 420 ], - "id": "4d66a76e-5b27-4880-9b95-6ac38203e371", + "id": "aaa1c12a-7d47-41ef-903d-109f847ff356", "name": "Analyse abfragen", "credentials": { "httpHeaderAuth": { @@ -415,7 +415,7 @@ 1560, 640 ], - "id": "689dd568-3c1d-4af4-b5d3-50e537fdf119", + "id": "23a1f2d5-49a3-4f69-85f3-456c0dcccaba", "name": "Fehler Analyse abfragen", "onError": "continueRegularOutput" }, @@ -430,7 +430,7 @@ }, "conditions": [ { - "id": "c6958335-389e-4fde-8d82-9da8a837fea6", + "id": "4bb7114a-807a-4487-a612-0835fb9260ca", "leftValue": "={{ $json.status }}", "rightValue": "ready", "operator": { @@ -450,7 +450,7 @@ 1780, 420 ], - "id": "beb96c7d-17a0-459a-9083-b3ffb1da2b35", + "id": "0d337016-36a1-492a-8b29-5e1d290be140", "name": "Analyse fertig?" }, { @@ -464,7 +464,7 @@ 1780, 660 ], - "id": "7d393c99-27f8-4a7c-9066-d2ccf81d5cbf", + "id": "a22a0c31-e340-435d-8663-0ab865ebd394", "name": "Versuch zaehlen" }, { @@ -478,7 +478,7 @@ }, "conditions": [ { - "id": "8ee3ea55-c784-4a43-8042-15869c607ad8", + "id": "1e0b93da-c082-406f-8642-56ff4a421c07", "leftValue": "={{ $json.versuch }}", "rightValue": "={{ $('Konfiguration').first().json.poll_max }}", "operator": { @@ -498,7 +498,7 @@ 2000, 660 ], - "id": "431e705f-2b52-4525-ba34-fb47b8011498", + "id": "5603ea35-5166-4234-be4b-7791bcba1d95", "name": "Poll-Limit erreicht?" }, { @@ -512,13 +512,13 @@ 2220, 780 ], - "id": "f6e65544-0e98-44e2-b698-d7d688829d61", + "id": "96ab606e-48a9-400e-96e7-857b042acffb", "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 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 links: 'Interne Links primaer auf Business-Leistungsseiten: handgeschriebene-businessbriefe, '\n + 'handgeschriebene-postkarten, unterschriftenservice, automatisierte-follow-ups. '\n + 'artikel.cta_ziel mindestens einmal im Fliesstext verlinken, mindestens zwei Business-Links insgesamt. '\n + 'papier-schriftmuster nur ergaenzend. Keine Links auf Privatkunden-Seiten, review, agb, datenschutz, '\n + 'impressum, zahlungsmethoden, lieferung-versand.',\n bausteine: 'Kein Block \"Auch fuer Privatpersonen.\" und kein Abschnitt \"Kostenloses Handschriftmuster '\n + 'anfordern.\" im Artikeltext, beides liefert das Seitenlayout. Der Artikel endet mit dem letzten '\n + 'inhaltlichen Abschnitt, nie mit einer Zusammenfassung.',\n faq_ueberschrift: 'Eigene FAQ-Ueberschrift als Feld faq_ueberschrift, konkret zum Thema, z. B. '\n + '\"Fragen zu den Kosten\". Satzfall, ohne Punkt am Ende.',\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" + "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, immer inklusive MwSt.: '\n + '[skrift_preis key=\"SCHLUESSEL\" ab=\"0|1\" brutto=\"1\"]. '\n + 'key ist der Produktschluessel (briefe, postkarten). ab=1 liefert den guenstigsten Staffelpreis, '\n + 'ab=0 den Standardpreis. brutto ist immer 1. Hinter dem Preis steht immer \"inkl. 19 % MwSt.\", '\n + 'nie \"netto\", \"brutto\" oder \"zzgl.\". Das Wort \"ab\" gehoert in den Fliesstext. Beispiel: '\n + 'Handschriftliche Briefe ab [skrift_preis key=\"briefe\" ab=\"1\" brutto=\"1\"] pro Stueck inkl. 19 % MwSt.',\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 links: 'Interne Links primaer auf Business-Leistungsseiten: handgeschriebene-businessbriefe, '\n + 'handgeschriebene-postkarten, unterschriftenservice, automatisierte-follow-ups. '\n + 'artikel.cta_ziel mindestens einmal im Fliesstext verlinken, mindestens zwei Business-Links insgesamt. '\n + 'papier-schriftmuster nur ergaenzend. Keine Links auf Privatkunden-Seiten, review, agb, datenschutz, '\n + 'impressum, zahlungsmethoden, lieferung-versand.',\n bausteine: 'Kein Block \"Auch fuer Privatpersonen.\" und kein Abschnitt \"Kostenloses Handschriftmuster '\n + 'anfordern.\" im Artikeltext, beides liefert das Seitenlayout. Der Artikel endet mit dem letzten '\n + 'inhaltlichen Abschnitt, nie mit einer Zusammenfassung.',\n faq_ueberschrift: 'Eigene FAQ-Ueberschrift als Feld faq_ueberschrift, konkret zum Thema, z. B. '\n + '\"Fragen zu den Kosten\". Satzfall, ohne Punkt am Ende.',\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": "6081e30a-4127-4af7-8af7-2b2446711a71", + "id": "19c85690-97f0-4eb7-b1e0-a83348ceefe1", "name": "Brief bauen" }, { @@ -551,7 +551,7 @@ 2440, 300 ], - "id": "4cefe611-3c98-4c4f-bf72-db5b8a05a5bd", + "id": "c43d02ff-1236-45fc-bcb3-5098accee588", "name": "Brief vorhanden?", "credentials": { "httpHeaderAuth": { @@ -571,7 +571,7 @@ 2660, 300 ], - "id": "ed3e6ae2-94a0-462c-98a2-e00c5a1c77ab", + "id": "8d222315-472d-44f2-953b-494074b5d482", "name": "Commit vorbereiten" }, { @@ -601,7 +601,7 @@ 2880, 300 ], - "id": "2331dd6c-8595-482e-abd4-e7ec9f155dd2", + "id": "1a784408-d09d-420b-a6d3-7f808bbd806d", "name": "Brief committen", "credentials": { "httpHeaderAuth": { @@ -625,7 +625,7 @@ 2880, 520 ], - "id": "19ea007b-0521-423a-bbf0-c1318f1b1cd0", + "id": "47ec95be-d567-4685-8ea7-aa447a91103d", "name": "Fehler Brief committen", "onError": "continueRegularOutput" }, @@ -640,7 +640,7 @@ 3100, 300 ], - "id": "c9e0a373-8df8-4b4a-9dea-7b204d84b97a", + "id": "07a1e471-d6bb-4327-a812-266bca634263", "name": "Erfolg merken" }, { @@ -654,9 +654,9 @@ 3320, 400 ], - "id": "22e97356-a573-465a-8847-7c4967923fa0", + "id": "9e4fc985-8c3d-4736-9585-1ecaf29a4b01", "name": "Pause Rate Limit", - "webhookId": "10b98341-b2a3-4bee-9158-670abd71eefe" + "webhookId": "380a5713-2767-4992-aa4a-053605517480" }, { "parameters": { @@ -669,7 +669,7 @@ 900, 140 ], - "id": "abf1aba9-cac5-4556-8230-798373a16c30", + "id": "d80eaa2b-a228-4f93-a608-c4bc48804cea", "name": "Ergebnisse sammeln" }, { @@ -683,7 +683,7 @@ 1120, 140 ], - "id": "a378732e-63fb-4137-aad3-e15a80768b1b", + "id": "f52ac21c-d921-4b65-b0ca-0232f6c212e6", "name": "Statusupdate vorbereiten" }, { @@ -713,7 +713,7 @@ 1340, 140 ], - "id": "4160f022-aba5-4057-a179-8e632da542e2", + "id": "ace29136-8155-46df-93d4-4ebba0b909eb", "name": "Status zurueckschreiben", "credentials": { "httpHeaderAuth": { @@ -737,7 +737,7 @@ 1340, -60 ], - "id": "c77421d1-349f-4cb6-8b8a-a1ead23bfb47", + "id": "8cd217a6-ad58-44dd-bef9-7183c6a6ccad", "name": "Fehler Statusupdate" }, { @@ -751,7 +751,7 @@ 1560, 140 ], - "id": "09faecc1-3eb8-4d31-a616-8286c1ef5b46", + "id": "b331d639-2de7-4329-8b11-350d227a59a5", "name": "Meldung formulieren" }, { @@ -769,7 +769,7 @@ 1780, 140 ], - "id": "9e6b8af6-6231-4838-9a04-ba8bde15decc", + "id": "8a6f1d29-5114-4e70-b539-18d50475ec94", "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 72e5fed..1452985 100644 --- a/workflows/pruefen-und-importieren.json +++ b/workflows/pruefen-und-importieren.json @@ -9,7 +9,7 @@ -460, 100 ], - "id": "265263b1-e44a-4457-9478-eab95f7e5a84", + "id": "5538d2bd-2e94-4f42-a389-9384ef625de8", "name": "Manuell starten" }, { @@ -25,9 +25,9 @@ -460, 280 ], - "id": "94759c20-c550-4bbf-8139-aa61ca8d49e6", + "id": "3e3c342f-b4ee-4565-b1d3-a9d845707842", "name": "Webhook Start", - "webhookId": "f14cad07-6ef2-4eda-b35c-cd42e5182987" + "webhookId": "9c08dcef-5acc-462b-bca4-7340ee75ed8e" }, { "parameters": { @@ -35,115 +35,115 @@ "assignments": { "assignments": [ { - "id": "093a0548-c164-4341-8728-d4e268ed4c91", + "id": "390bf7df-f505-4ef0-b187-16f28a42e8dc", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "4bef6ac1-2c7a-4c03-b2e3-0d71953ef88b", + "id": "526b22c9-127b-4df5-8ffc-eeffd4fdaafb", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "6d54a36c-0259-4ebb-a92b-c247ea98c7ea", + "id": "dc41acbd-71a6-4d54-901e-89b1b3c42cc8", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "f26c6607-7a45-4f1f-b29f-71a4da8bf754", + "id": "0ee5976c-0df1-4267-9f74-de284d60f74c", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "922c32f4-0897-4834-9640-c96b9af398af", + "id": "475ceb04-ce9c-4471-94a2-780d10ada7c9", "name": "branch", "value": "main", "type": "string" }, { - "id": "03f6d42d-170e-469e-88cd-986c1b4e11c8", + "id": "88eebf2d-27d8-4127-a189-7e348bbf999e", "name": "artikel_ordner", "value": "artikel", "type": "string" }, { - "id": "555e09dc-ac0e-47ce-a939-e12f790d6e0d", + "id": "6eb13140-4748-4344-a731-2d808e8986d7", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "99916e7b-ef56-48dc-bda1-83a1e4cc0c13", + "id": "783fe66a-75a8-4d8e-b2f0-2aebf80e087e", "name": "nachbessern_ordner", "value": "nachbessern", "type": "string" }, { - "id": "e6259d17-c43d-42e4-8710-be43daaad6ff", + "id": "2ed923be-06ad-48c4-b927-041f6e2f2552", "name": "status_pfad", "value": "status-importe.json", "type": "string" }, { - "id": "5cbe77f0-5ef3-4082-bdc6-2e7d1bf0427a", + "id": "a56e81d3-a9c3-46e9-8f34-ecf8b002ac9c", "name": "wp_base", "value": "https://skrift.de", "type": "string" }, { - "id": "5195f907-2616-4f97-9579-891001d25d23", + "id": "cfd8b1d7-6ee5-4a4a-85a2-724ea800968e", "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}", "type": "number" }, { - "id": "c015b3d5-97b5-4c3c-a61e-33fce56c1a32", + "id": "2e91b780-c50a-4413-b3a7-b03b1810bd31", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "fa40fab3-f5d1-4b11-9d8c-6c9660e783e9", + "id": "46cd6c5e-3ea8-4f8d-acd8-8cfc737c46dd", "name": "elementor_ausgabe", "value": false, "type": "boolean" }, { - "id": "a929c610-3968-4fb3-8522-798575e1e273", + "id": "e469e1f3-6366-4a91-ba25-5df62337228c", "name": "bild_aktiv", "value": true, "type": "boolean" }, { - "id": "31d925cf-a15c-4edb-9fcb-9f017f63e7f6", + "id": "6e75aa5e-71cf-432c-811e-0901c721268e", "name": "bild_modell", "value": "gpt-image-2", "type": "string" }, { - "id": "9ccb1885-18e6-4094-8b3f-ca603d9dfb16", + "id": "d5ffc52c-befa-451d-a82c-62a90bb95d6a", "name": "bild_groesse", "value": "1200x1200", "type": "string" }, { - "id": "f0730564-27da-4443-821c-606c3e12d174", + "id": "480645e7-a7e3-49b8-9e97-add0f25482b9", "name": "bild_qualitaet", "value": "medium", "type": "string" }, { - "id": "92e2bdbf-47d8-4073-9a18-bdf8828c761b", + "id": "06d23bd2-7a0a-41ac-b70b-9cc1dac08b87", "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string" }, { - "id": "037987f1-5636-4544-a3da-734e00269da8", + "id": "dbf57836-aa98-4f62-ad93-75b6978ed979", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -158,7 +158,7 @@ -240, 190 ], - "id": "e619d11b-46f8-41d3-abb5-9f170ca5ac69", + "id": "8354444e-f8b0-4895-b9ef-78895a058245", "name": "Konfiguration" }, { @@ -183,7 +183,7 @@ -20, 190 ], - "id": "3f1a8c4e-4928-4e64-abe7-f8e67a322bd1", + "id": "8a150411-fc15-4549-9cac-9fd750998ba7", "name": "Artikelordner lesen", "credentials": { "httpHeaderAuth": { @@ -206,7 +206,7 @@ 200, 190 ], - "id": "8611694a-ff31-4964-b185-0c8a8abd9b5d", + "id": "e91525e5-1fe9-468e-9bc9-7c865542ac81", "name": "Artikeldateien filtern" }, { @@ -220,7 +220,7 @@ }, "conditions": [ { - "id": "fb30d9cc-c4d4-4323-a1e1-b5cb144d6597", + "id": "3d273a9f-f283-4790-aacb-f1d615f08728", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -241,7 +241,7 @@ 420, 190 ], - "id": "5e83eb61-eb78-437c-a57d-7358ccd84312", + "id": "1c86b8dc-2590-47ea-aeda-3cc69e374f4c", "name": "Artikel vorhanden?" }, { @@ -252,7 +252,7 @@ 640, 40 ], - "id": "2d130159-cf3c-4932-aca1-bfc5e59ebe84", + "id": "abe7df94-da3e-409d-ac97-2aab7a446635", "name": "Nichts zu tun" }, { @@ -268,7 +268,7 @@ 640, 320 ], - "id": "abdbec06-3537-4dc5-8dd6-019e7aa2b24e", + "id": "527a5591-0e90-4495-afa5-df2a2666adc4", "name": "Artikel nacheinander" }, { @@ -292,7 +292,7 @@ 900, 420 ], - "id": "906172c7-45ac-4348-aee7-197d23d4304d", + "id": "204918b5-23e4-4e74-bce5-322c96e1675c", "name": "Artikeldatei laden", "credentials": { "httpHeaderAuth": { @@ -316,7 +316,7 @@ 900, 640 ], - "id": "25977511-3ca4-4467-9014-ae013bbc839c", + "id": "722cca8e-1303-41ff-ab50-27d18fe873df", "name": "Fehler Artikeldatei", "onError": "continueRegularOutput" }, @@ -342,7 +342,7 @@ 1120, 420 ], - "id": "df42b00e-6e07-4ffd-83ff-65c061b160af", + "id": "5d42b7af-3125-4445-a579-665b0ea2a1c8", "name": "Brief laden", "credentials": { "httpHeaderAuth": { @@ -354,7 +354,7 @@ { "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(/ /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 // 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(/]*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 < 1) add('Links: CTA-Ziel nicht verlinkt', 'cta_ziel mindestens einmal im Fliesstext verlinken');\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) add('Links: Privatkunden-Link im Text', 'Privatkunden-Seiten nicht aus dem Artikeltext verlinken, das uebernimmt das Seitenlayout');\n // Bausteine, die das Seitenlayout bereits liefert\n if (/Auch für Privatpersonen/i.test(html)) add('Baustein doppelt: \"Auch für Privatpersonen.\"', 'Block aus dem Artikeltext entfernen, er steht im Seitenlayout');\n if (/]*>[^<]*(Muster anfordern|Handschriftmuster)/i.test(html)) add('Baustein doppelt: Muster-CTA', 'Abschnitt \"Kostenloses Handschriftmuster anfordern.\" entfernen, er steht im Seitenlayout');\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 => `

${f.frage || ''}

${f.antwort || ''}

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

${artikel.kurzantwort}

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

${artikel.faq_ueberschrift || '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" + "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 // 7b) Preise: immer inkl. MwSt., Formulierung \"inkl. 19 % MwSt.\"\n const alleTexte = [String(artikel.kurzantwort || ''), html,\n ...(Array.isArray(artikel.faq) ? artikel.faq.map(f => String(f.antwort || '')) : [])].join(' ');\n (alleTexte.match(/\\[skrift_preis[^\\]]*\\]/g) || []).forEach(s => {\n if (!/brutto=\"1\"/.test(s)) add('Preis ohne MwSt.', `${s} braucht brutto=\"1\"`);\n });\n const nb = ohneTags(alleTexte.replace(/\\[skrift_preis[^\\]]*\\]/g, 'PREIS')).match(/[^.]{0,40}\\b(netto|brutto|zzgl\\.?|zuzüglich)\\b[^.]{0,30}/gi) || [];\n nb.forEach(s => add('Preisangabe: \"netto\"/\"brutto\" statt \"inkl. 19 % MwSt.\"', s.trim()));\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(/]*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 < 1) add('Links: CTA-Ziel nicht verlinkt', 'cta_ziel mindestens einmal im Fliesstext verlinken');\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) add('Links: Privatkunden-Link im Text', 'Privatkunden-Seiten nicht aus dem Artikeltext verlinken, das uebernimmt das Seitenlayout');\n // Bausteine, die das Seitenlayout bereits liefert\n if (/Auch für Privatpersonen/i.test(html)) add('Baustein doppelt: \"Auch für Privatpersonen.\"', 'Block aus dem Artikeltext entfernen, er steht im Seitenlayout');\n if (/]*>[^<]*(Muster anfordern|Handschriftmuster)/i.test(html)) add('Baustein doppelt: Muster-CTA', 'Abschnitt \"Kostenloses Handschriftmuster anfordern.\" entfernen, er steht im Seitenlayout');\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 => `

${f.frage || ''}

${f.antwort || ''}

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

${artikel.kurzantwort}

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

${artikel.faq_ueberschrift || '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, @@ -362,7 +362,7 @@ 1340, 420 ], - "id": "2432e9d4-90fb-4953-8521-f192cf502473", + "id": "0004630a-561d-47e0-8482-fad45ae3f1c3", "name": "Artikel pruefen" }, { @@ -392,7 +392,7 @@ 1560, 420 ], - "id": "c775a775-a975-418f-81b2-c23d44ca310c", + "id": "1b15da2c-b93d-4fbd-b803-f104f8f88b94", "name": "Score ermitteln", "credentials": { "httpHeaderAuth": { @@ -416,7 +416,7 @@ 1560, 640 ], - "id": "03075c45-9d02-48f5-b176-94cdb509d866", + "id": "422c4168-3898-4d52-bec7-d02ea6bc03e0", "name": "Fehler Score", "onError": "continueRegularOutput" }, @@ -431,7 +431,7 @@ 1780, 420 ], - "id": "3e90f007-8d26-494c-ade0-ee58db39ad39", + "id": "af056083-199f-4412-8eb4-1711eeb5cc14", "name": "Score auswerten" }, { @@ -445,7 +445,7 @@ }, "conditions": [ { - "id": "009ffbe7-01c0-461c-b28d-b745c4d60afa", + "id": "8512f8e1-81a8-473d-8690-0bb0167d0aee", "leftValue": "={{ $json.bestanden }}", "rightValue": "", "operator": { @@ -466,7 +466,7 @@ 2000, 420 ], - "id": "19ff7716-5710-41d6-bfd7-4fe08bd16cb7", + "id": "22d68dd7-afad-42c9-8d5a-b6ba3326c3ab", "name": "Score erreicht?" }, { @@ -480,7 +480,7 @@ 2220, 640 ], - "id": "702f39bc-8886-42e5-a8dd-affd032569dd", + "id": "6ba8665b-d17e-4eb5-88ef-f2fec20bf53b", "name": "Fehlende Begriffe ermitteln" }, { @@ -505,7 +505,7 @@ 2440, 640 ], - "id": "174bc5e0-168e-40ce-9486-d91f4e5cf91b", + "id": "45709753-0ee1-47eb-a43a-f74bac69b8a3", "name": "Nachbesserung vorhanden?", "credentials": { "httpHeaderAuth": { @@ -525,7 +525,7 @@ 2660, 640 ], - "id": "eaab366d-7814-47c6-9aa9-04f4637918a1", + "id": "f8b6d020-9d26-4011-889b-7c4baf01a5a7", "name": "Nachbesserung vorbereiten" }, { @@ -555,7 +555,7 @@ 2880, 640 ], - "id": "7d2e670b-6bcd-4240-90b2-27ef59f54dcc", + "id": "e2a6ab12-9d70-4721-bfe7-33908c3907db", "name": "Nachbesserung committen", "credentials": { "httpHeaderAuth": { @@ -579,7 +579,7 @@ 2880, 860 ], - "id": "c903692e-805d-4034-9555-651a6b0f2779", + "id": "c15887c3-f8d6-4d1a-a384-758336cf1611", "name": "Fehler Nachbesserung", "onError": "continueRegularOutput" }, @@ -594,7 +594,7 @@ 3100, 640 ], - "id": "971c9f49-930f-4876-913c-d5bfb060826e", + "id": "39611df3-08b6-4714-8913-d6d4a182691c", "name": "Nachbesserung gemerkt" }, { @@ -624,7 +624,7 @@ 2220, 240 ], - "id": "ad73443d-79ec-418f-b9cb-422d73d71b40", + "id": "fb72faca-a385-410f-9ae9-bea8497cb8ee", "name": "Revision zu NeuronWriter", "credentials": { "httpHeaderAuth": { @@ -648,7 +648,7 @@ 2220, 60 ], - "id": "90c1cd8b-44f7-40e9-80cc-407e6d4ccb9e", + "id": "5200499c-316d-4abf-8fa5-68b8855a8213", "name": "Fehler Revision", "onError": "continueRegularOutput" }, @@ -674,7 +674,7 @@ 2440, 240 ], - "id": "9a6eaa50-09b1-4e68-a06f-74471a6e075c", + "id": "d150e8c2-5e08-453e-91c1-88d20142085d", "name": "Beitrag vorhanden?", "credentials": { "httpBasicAuth": { @@ -708,7 +708,7 @@ 2660, 240 ], - "id": "9d7d2fca-b517-43c5-a031-c123e52985da", + "id": "703611e3-5820-4f90-86cb-0d15950cec22", "name": "Kategorie suchen", "credentials": { "httpBasicAuth": { @@ -731,7 +731,7 @@ 2880, 240 ], - "id": "f175da94-e137-4fac-b2e6-f38ef4d3e9b8", + "id": "b4cd58dc-ab41-44d6-934c-9f40d5064653", "name": "Kategorie pruefen" }, { @@ -745,7 +745,7 @@ }, "conditions": [ { - "id": "c553d779-7e48-4b1b-8808-1c2ff31737f0", + "id": "1418e966-d8bc-4344-a20e-947e9a74a471", "leftValue": "={{ $json.kategorie_fehlt }}", "rightValue": "", "operator": { @@ -766,7 +766,7 @@ 3100, 240 ], - "id": "a9b527e8-a341-4a47-a6a3-261e54d3efac", + "id": "a37d9e34-17a8-4f1c-83c7-4364c2a6d7a3", "name": "Kategorie anlegen?" }, { @@ -796,7 +796,7 @@ 3320, 140 ], - "id": "1f2a04e4-0625-4ce5-a6d9-49737ccb7c85", + "id": "5a1285a8-cf43-417d-afb5-4d4b7f2bc215", "name": "Kategorie anlegen", "credentials": { "httpBasicAuth": { @@ -819,7 +819,7 @@ 3540, 240 ], - "id": "7eb5e79b-817e-4e7a-bc4b-6d8dfc32fc61", + "id": "5f58cee4-82cd-49d8-9796-6b0e4cefde27", "name": "Kategorie festlegen" }, { @@ -833,7 +833,7 @@ 3760, 240 ], - "id": "3e24c6b8-d0b0-4983-a06c-1183918148bb", + "id": "c5325756-6ff4-4d95-8bda-2371858927ca", "name": "WordPress-Body bauen" }, { @@ -863,7 +863,7 @@ 3980, 240 ], - "id": "e05040a1-66fe-467f-87d7-f8985acb2191", + "id": "3a509a0b-0e18-4d66-85be-e11f98c1b6f3", "name": "Beitrag anlegen oder aktualisieren", "credentials": { "httpBasicAuth": { @@ -886,7 +886,7 @@ 4200, 240 ], - "id": "e6ff477c-cd6e-4d7d-8737-5cc4f69f6fb8", + "id": "5b75c979-7d05-469f-a7dd-4c3f39b0cbbf", "name": "Ergebnis pruefen" }, { @@ -917,7 +917,7 @@ 4420, 240 ], - "id": "293acc6c-2693-4cd7-a21a-3edc80a803f2", + "id": "d94b6d9c-412b-4343-8a5c-934b29c52961", "name": "Cache leeren", "credentials": { "httpBasicAuth": { @@ -937,7 +937,7 @@ 4640, 240 ], - "id": "4d8c093e-e322-4a87-a79d-77db2724bc1a", + "id": "73409752-8021-4e54-a47d-e4eadccbebfe", "name": "Importergebnis merken" }, { @@ -951,9 +951,9 @@ 4860, 420 ], - "id": "56000936-7d58-4ab7-892e-60520c4bc20e", + "id": "de26ea0e-032e-4369-ad92-33937423dbc8", "name": "Pause Rate Limit", - "webhookId": "4782cf6a-9326-4b0f-9b35-4754fa88908f" + "webhookId": "a21f142d-d8a7-4bd7-8b9b-1b58d7fe08ed" }, { "parameters": { @@ -966,7 +966,7 @@ }, "conditions": [ { - "id": "1ae82a45-edd8-44f4-8946-47adc6800d41", + "id": "0f13a811-93b2-41b0-b008-bc49fe907c19", "leftValue": "={{ $('Konfiguration').first().json.bild_aktiv === true && !Number($json.featured_media) }}", "rightValue": "", "operator": { @@ -987,7 +987,7 @@ 4860, 240 ], - "id": "ff9b0ab2-1a97-44dc-a97d-d33688cb2501", + "id": "9b6f2199-87ea-4926-bf8f-7b9dd632260b", "name": "Beitragsbild fehlt?" }, { @@ -1012,7 +1012,7 @@ 5080, 120 ], - "id": "d7a48424-743c-464d-8290-efdcb05d48d1", + "id": "a885b2bf-7d43-4ff8-9c55-a10015f88181", "name": "Bilddaten laden", "credentials": { "httpHeaderAuth": { @@ -1035,7 +1035,7 @@ 5300, 120 ], - "id": "dd13bf7d-798d-43a4-af99-fd0ee8bffcab", + "id": "26883a08-bb99-4bf7-94fa-1772db788c43", "name": "Bildprompt bauen" }, { @@ -1049,7 +1049,7 @@ }, "conditions": [ { - "id": "98fcfee0-224f-422a-b567-ec68f40b465f", + "id": "5d1dd07d-ccc5-4af5-971f-505cdd5ef0cf", "leftValue": "={{ !!$json.bild_prompt }}", "rightValue": "", "operator": { @@ -1070,7 +1070,7 @@ 5520, 120 ], - "id": "620a3df5-f36b-4192-b215-4c73b5e123d9", + "id": "1d827bd8-2432-40e8-903d-79e81c77960a", "name": "Bildprompt vorhanden?" }, { @@ -1100,7 +1100,7 @@ 5740, 40 ], - "id": "6c1873c5-117c-4fb9-9109-e9b2cee026d2", + "id": "6e9078dd-7f68-4491-b5a9-11a25bad2ed0", "name": "Bild erzeugen", "credentials": { "httpHeaderAuth": { @@ -1124,7 +1124,7 @@ 5740, 260 ], - "id": "1f440c70-8596-4c40-963b-f8643698bbd4", + "id": "b22e5786-0100-4ba7-bb28-86ef4fa918f9", "name": "Fehler Bild", "onError": "continueRegularOutput" }, @@ -1139,7 +1139,7 @@ 5960, 40 ], - "id": "d883b5e0-6185-42ff-a6e2-921bfb467ed5", + "id": "c33551a0-9962-4fba-b94f-0d7540ba95a3", "name": "Bild als Datei" }, { @@ -1174,7 +1174,7 @@ 6180, 40 ], - "id": "f8891b55-37e7-459b-969a-6f9f33c51c33", + "id": "f809110f-6dee-4d1c-802c-50dfb0ec72c3", "name": "Bild in Mediathek", "credentials": { "httpBasicAuth": { @@ -1208,7 +1208,7 @@ 6400, 40 ], - "id": "3d497e77-7911-4ed0-9f22-f20504712ef9", + "id": "a0ef7f44-c9ca-4edb-9f8b-79a634688c4d", "name": "Beitragsbild setzen", "credentials": { "httpBasicAuth": { @@ -1231,7 +1231,7 @@ 6620, 40 ], - "id": "453a90a0-5243-4d1a-9224-3e3fc3541599", + "id": "9d7774ee-0cd1-4838-8794-c9d295f8cca1", "name": "Bild gemerkt" }, { @@ -1245,7 +1245,7 @@ 900, 80 ], - "id": "3e4abd4e-4a6f-4e54-9924-c2cfa154cf82", + "id": "519e24e7-6394-4448-8fbc-8b9abf2e2b5c", "name": "Ergebnisse sammeln" }, { @@ -1270,7 +1270,7 @@ 1120, 80 ], - "id": "ca0cb16c-0af2-4b3c-aaa2-5725972703be", + "id": "db67b230-bf33-4d8b-839e-7500267ff088", "name": "Statustabelle lesen", "credentials": { "httpHeaderAuth": { @@ -1290,7 +1290,7 @@ 1340, 80 ], - "id": "78f81aa7-0ca1-4f29-815c-a5328ea514b9", + "id": "d9a2ea1e-f353-4dc0-b0d3-5626f56287a0", "name": "Statustabelle fortschreiben" }, { @@ -1304,7 +1304,7 @@ }, "conditions": [ { - "id": "d2c17929-ba0a-4080-ae1a-e9a6aad3e118", + "id": "280db96a-ce49-4a0f-88e2-30b28e5530af", "leftValue": "={{ $json.ueberspringen }}", "rightValue": "", "operator": { @@ -1325,7 +1325,7 @@ 1560, 80 ], - "id": "7ce09846-988b-490b-a1d3-1b986eb17713", + "id": "379554bd-f08f-42b1-b5f0-8d4491081940", "name": "Gab es Importe?" }, { @@ -1355,7 +1355,7 @@ 1780, 160 ], - "id": "0a900f70-93be-4997-b04a-c74061ea39e9", + "id": "d0aa7ca7-73d2-4373-8688-56b31c759e8a", "name": "Statustabelle schreiben", "credentials": { "httpHeaderAuth": { @@ -1379,7 +1379,7 @@ 1780, 340 ], - "id": "96e36221-b847-4b03-95cb-e1a103a27df7", + "id": "2381caea-8d16-47d5-b7b6-512feab7e685", "name": "Fehler Statustabelle", "onError": "continueRegularOutput" }, @@ -1394,7 +1394,7 @@ 2000, 80 ], - "id": "2899cba3-36f3-4fb1-b1cf-c99fe19b9020", + "id": "fbd5c338-83f4-4503-8639-13288826772e", "name": "Meldung formulieren" }, { @@ -1412,7 +1412,7 @@ 2220, 80 ], - "id": "12bdb6c6-d9d0-4271-ace8-e3e07c863819", + "id": "c54274ec-b1af-458d-936d-c413d8acd1b7", "name": "Benachrichtigung", "disabled": true, "notes": "SMTP-Credential zuweisen, Absender eintragen, dann aktivieren."