From 7dcf77489651a5c89049d07c5ca0a520d85e1f13 Mon Sep 17 00:00:00 2001 From: Lucas Orth Date: Sun, 20 Sep 2026 13:14:28 +0200 Subject: [PATCH] Elementor-Ausgabe, Referenzartikel, Redaktionsplan --- README.md | 31 ++++- build/elementor_bauen.py | 91 +++++++++++++++ build/gen_wf2.py | 90 ++++++++++++++- workflows/pruefen-und-importieren.json | 152 +++++++++++++------------ 4 files changed, 290 insertions(+), 74 deletions(-) create mode 100644 build/elementor_bauen.py diff --git a/README.md b/README.md index 3906dea..be24f6c 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,24 @@ der Workflow fuer diesen Artikel ab. Fehlt `stand_datum`, setzt der Workflow das heutige Datum. Fehlt `seo_titel`, wird `titel` verwendet. +### Ausgabeform in WordPress + +Der Node **Konfiguration** hat den Schalter `elementor_ausgabe`. + +Steht er auf `true` (Standard), baut der Workflow aus dem Artikel eine +Elementor-Struktur: Der Text wird an den H2-Grenzen in einzelne +Text-Editor-Widgets zerlegt, die Kurzantwort bekommt einen eigenen Block, und +das FAQ wird zu einem **nested-accordion** mit aktiviertem `faq_schema`. +Das ist dasselbe Widget, das die Leistungsseiten von skrift.de verwenden, der +Beitrag laesst sich also im Builder wie jede andere Seite bearbeiten und +WordPress gibt automatisch FAQPage-Schema aus. + +Steht er auf `false`, landet der Artikel als klassischer Beitragsinhalt. Das FAQ +gibt dann der Shortcode `[skrift_faq]` aus, den das Mu-Plugin mitbringt. + +Die Meta-Felder `kurzantwort`, `faq`, `cta_ziel` und `stand_datum` werden in +beiden Faellen geschrieben, unabhaengig von der Darstellung. + ### Schreibregeln fuer content_html **Preise niemals ausschreiben.** Sie werden zentral gepflegt und per Shortcode @@ -192,10 +210,21 @@ eingesetzt: | Parameter | Bedeutung | |---|---| -| `key` | Produkt- oder Positionsschluessel, z. B. `briefe`, `postkarten`, `porto_inland` | +| `key` | Produktschluessel, siehe Liste unten | | `ab=1` | guenstigster Staffelpreis; `ab=0` der Standardpreis | | `brutto=1` | inkl. MwSt.; `brutto=0` netto | +Gueltige Schluessel, am 20.09.2026 gegen das Plugin geprueft: +`briefe`, `postkarten`, `muster`, `unterschriftenservice`, `follow_ups`. +Fuer die letzten drei liefert das Plugin 0,00 Euro, dort gehoert kein Preis in +den Text. + +**Nicht verwechseln:** `[skrift_preis_exkl produkt="..."]` und +`[skrift_preis_inkl produkt="..."]` rendern den kompletten interaktiven +Preisrechner mit Mengenfeld. Die gehoeren auf Leistungsseiten, nicht in den +Fliesstext eines Ratgebers. Nur `[skrift_preis key="..."]` liefert eine +reine Zahl. + Das Wort "ab" gehoert in den Fliesstext, der Shortcode liefert nur die Zahl: ``` diff --git a/build/elementor_bauen.py b/build/elementor_bauen.py new file mode 100644 index 0000000..8817982 --- /dev/null +++ b/build/elementor_bauen.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""Baut aus content_html und faq eine Elementor-Struktur (_elementor_data). + +Der Artikel wird an den H2-Grenzen in einzelne Text-Editor-Widgets zerlegt, +damit er im Builder abschnittsweise bearbeitbar bleibt. Das FAQ wird zu einem +nested-accordion mit aktiviertem faq_schema, also demselben Widget, das die +Leistungsseiten von skrift.de bereits verwenden. +""" +import json, re, secrets + + +def eid(): + return secrets.token_hex(4)[:7] + + +def text_widget(html): + return {"id": eid(), "elType": "widget", "settings": {"editor": html}, + "elements": [], "widgetType": "text-editor"} + + +def container(kinder, innen=False): + s = {"content_width": "full"} + if innen: + s.update({"_title": "Element", "padding": {"unit": "px", "top": "8", "right": "8", + "bottom": "8", "left": "8", "isLinked": True}, + "flex_gap": {"column": "20", "row": "20", "isLinked": True, + "unit": "px", "size": 20}}) + c = {"id": eid(), "elType": "container", "settings": s, "elements": kinder} + if innen: + c["isInner"] = True + c["isLocked"] = True + return c + + +def accordion(faq, titel="Häufige Fragen"): + """nested-accordion: settings.items und elements laufen parallel.""" + items, kinder = [], [] + for f in faq: + if not f.get("frage"): + continue + items.append({"item_title": f["frage"], "_id": eid()}) + kinder.append(container([text_widget(f"

{f.get('antwort', '')}

")], innen=True)) + return { + "id": eid(), "elType": "widget", "widgetType": "nested-accordion", + "settings": {"items": items, "faq_schema": "yes", "default_state": "all_collapsed"}, + "elements": kinder, + } + + +def teile_an_h2(html): + """Zerlegt den Artikel an

, behält alles davor als Einleitung.""" + stellen = [m.start() for m in re.finditer(r']', html)] + if not stellen: + return [html] + bloecke = [] + if stellen[0] > 0: + kopf = html[:stellen[0]].strip() + if kopf: + bloecke.append(kopf) + for i, s in enumerate(stellen): + ende = stellen[i + 1] if i + 1 < len(stellen) else len(html) + bloecke.append(html[s:ende].strip()) + return [b for b in bloecke if b] + + +def baue(artikel, faq_ueberschrift="Häufige Fragen"): + wurzel = [] + + # Kurzantwort als eigener, hervorgehobener Block + if artikel.get("kurzantwort"): + wurzel.append(container([text_widget( + f'

{artikel["kurzantwort"]}

')])) + + for block in teile_an_h2(artikel.get("content_html", "")): + wurzel.append(container([text_widget(block)])) + + faq = artikel.get("faq") or [] + if faq: + wurzel.append(container([ + text_widget(f"

{faq_ueberschrift}

"), + accordion(faq), + ])) + + return wurzel + + +if __name__ == "__main__": + import sys + a = json.load(open(sys.argv[1], encoding="utf-8")) + baum = baue(a) + print(json.dumps(baum, ensure_ascii=False)) diff --git a/build/gen_wf2.py b/build/gen_wf2.py index 05f4c0c..0a2c3f0 100644 --- a/build/gen_wf2.py +++ b/build/gen_wf2.py @@ -68,6 +68,8 @@ KONF = node("Konfiguration", "n8n-nodes-base.set", 3.4, [-240, 190], { {"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": "pause_zwischen_artikeln", "value": 5, "type": "number"}, + {"id": nid(), "name": "elementor_ausgabe", "value": True, "type": "boolean"}, + {"id": nid(), "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string"}, {"id": nid(), "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string"}, ]}, "options": {}}) for t in ["Manuell starten", "Webhook Start"]: link(t, KONF) @@ -170,7 +172,19 @@ while ((m = re.exec(String(artikel.content_html || ''))) !== null) { } const shortcodes = (String(artikel.content_html || '').match(/\[skrift_preis[^\]]*\]/g) || []); +// NeuronWriter bewertet nur den uebergebenen Text. Kurzantwort und FAQ stehen +// aber auf der fertigen Seite, also gehoeren sie in die Bewertung. Der gespeicherte +// Artikel bleibt unveraendert, das hier ist nur der Pruefkoerper. +const faqText = (Array.isArray(artikel.faq) ? artikel.faq : []) + .map(f => `

${f.frage || ''}

${f.antwort || ''}

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

${artikel.kurzantwort}

` : '', + artikel.content_html || '', + faqText ? `

Häufige Fragen

\n${faqText}` : '', +].filter(Boolean).join('\n'); + return [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief, + bewertungs_html, 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.' @@ -182,7 +196,7 @@ link(BRIEF, PRUEFEN) # ---------------------------------------------------------------- Score EVAL = http("Score ermitteln", [1560, 420], "POST", "={{ $('Konfiguration').first().json.nw_base }}/evaluate-content", CRED_NW, - body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}") + body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}") link(PRUEFEN, EVAL) FEHLER_EVAL = code("Fehler Score", [1560, 640], """ @@ -220,7 +234,7 @@ NACH = code("Fehlende Begriffe ermitteln", [2220, 640], r""" const s = $input.first().json; const brief = s.brief; -const text = String(s.artikel.content_html || '') +const text = String(s.bewertungs_html || s.artikel.content_html || '') .replace(//gi, ' ') .replace(//gi, ' ') .replace(/<[^>]+>/g, ' ') @@ -333,7 +347,7 @@ link(NACH_COMMIT, NACH_OK, out=0) # ================================================================ ERFOLGS-PFAD IMPORT = http("Revision zu NeuronWriter", [2220, 240], "POST", "={{ $('Konfiguration').first().json.nw_base }}/import-content", CRED_NW, - body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}") + body="={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}") link(GATE, IMPORT, out=0) IMPORT_FEHLER = code("Fehler Revision", [2220, 60], """ @@ -422,6 +436,71 @@ const meta = { _seopress_analysis_target_kw: a.keyword || '', }; +// --- Elementor-Struktur aus Artikel und FAQ --- +// Zerlegt den Text an den H2-Grenzen in Text-Editor-Widgets und haengt das FAQ +// als nested-accordion an, dasselbe Widget wie auf den Leistungsseiten. +const konf = $('Konfiguration').first().json; +const eid = () => Math.random().toString(16).slice(2, 9); +const textWidget = (h) => ({ id: eid(), elType: 'widget', settings: { editor: h }, + elements: [], widgetType: 'text-editor' }); +const behaelter = (kinder, innen) => { + const s = { content_width: 'full' }; + if (innen) Object.assign(s, { + _title: 'Element', + padding: { unit: 'px', top: '8', right: '8', bottom: '8', left: '8', isLinked: true }, + flex_gap: { column: '20', row: '20', isLinked: true, unit: 'px', size: 20 }, + }); + const c = { id: eid(), elType: 'container', settings: s, elements: kinder }; + if (innen) { c.isInner = true; c.isLocked = true; } + return c; +}; + +function elementorBauen() { + const wurzel = []; + if (a.kurzantwort) { + wurzel.push(behaelter([textWidget( + `

${a.kurzantwort}

`)])); + } + const html = String(a.content_html || ''); + const stellen = []; + const re2 = /]/g; + let mm; + while ((mm = re2.exec(html)) !== null) stellen.push(mm.index); + const bloecke = []; + if (!stellen.length) { + if (html.trim()) bloecke.push(html.trim()); + } else { + if (stellen[0] > 0 && html.slice(0, stellen[0]).trim()) bloecke.push(html.slice(0, stellen[0]).trim()); + stellen.forEach((s, i) => { + const ende = i + 1 < stellen.length ? stellen[i + 1] : html.length; + const b = html.slice(s, ende).trim(); + if (b) bloecke.push(b); + }); + } + bloecke.forEach(b => wurzel.push(behaelter([textWidget(b)]))); + + if (meta.faq.length) { + const items = [], kinder = []; + meta.faq.forEach(f => { + items.push({ item_title: f.frage, _id: eid() }); + kinder.push(behaelter([textWidget(`

${f.antwort || ''}

`)], true)); + }); + wurzel.push(behaelter([ + textWidget(`

${konf.faq_ueberschrift || 'Häufige Fragen'}

`), + { id: eid(), elType: 'widget', widgetType: 'nested-accordion', + settings: { items, faq_schema: 'yes', default_state: 'all_collapsed' }, + elements: kinder }, + ])); + } + return wurzel; +} + +if (konf.elementor_ausgabe) { + meta._elementor_data = JSON.stringify(elementorBauen()); + meta._elementor_edit_mode = 'builder'; + meta._elementor_template_type = 'wp-post'; +} + const body = { title: a.titel, slug: s.slug, @@ -483,6 +562,10 @@ return [{ json: { + '. Mu-Plugin skrift-ratgeber-felder.php installiert?' : null, faq_eintraege: faq_ok, + elementor: (p.meta || {})._elementor_edit_mode === 'builder' + ? (() => { try { return JSON.parse((p.meta || {})._elementor_data || '[]').length + ' Container'; } + catch (e) { return 'ungueltig'; } })() + : 'klassischer Inhalt', seo_titel: (p.meta || {})._seopress_titles_title || null, preis_warnung: v.preis_warnung || null, preis_shortcodes: v.preis_shortcodes || 0, @@ -600,6 +683,7 @@ if (s.importiert.length) { z.push(` ${r.slug} (Score ${r.score}, ${r.aktion})`); z.push(` Vorschau: ${r.vorschau_url}`); z.push(` Bearbeiten: ${r.bearbeiten_url}`); + 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.cache_hinweis) z.push(` Hinweis: ${r.cache_hinweis}`); diff --git a/workflows/pruefen-und-importieren.json b/workflows/pruefen-und-importieren.json index dfd0f79..078a2b9 100644 --- a/workflows/pruefen-und-importieren.json +++ b/workflows/pruefen-und-importieren.json @@ -9,7 +9,7 @@ -460, 100 ], - "id": "b6be91ee-7c33-4990-8f11-3a8a5d7378ba", + "id": "91d71cf1-2650-4e1a-a053-9e01f04cb78a", "name": "Manuell starten" }, { @@ -25,9 +25,9 @@ -460, 280 ], - "id": "e8af0078-9f50-4318-9764-83c85099cfb8", + "id": "b8826586-3f30-40f9-9f4d-3566606a9b3a", "name": "Webhook Start", - "webhookId": "5292db0f-071e-42c6-893f-acfbd2e1d4d7" + "webhookId": "7ed025cf-5099-4378-a2d2-51fd49e119af" }, { "parameters": { @@ -35,79 +35,91 @@ "assignments": { "assignments": [ { - "id": "8b6e2da3-da8b-40cc-9f52-33cd801a497b", + "id": "4b4628e5-2614-49aa-a5af-7c7d8e0c6c07", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "3c92a320-321c-416b-a4f0-548e37a82d19", + "id": "664c1556-dec1-4d5d-9652-e891ebf08b26", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "8f62200d-a0f9-4fa0-b182-becb6121a0ef", + "id": "d8cfbc45-e4f2-4160-89db-a5400c808315", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "08a88597-c4bc-4aff-bac9-929f0e5ec617", + "id": "2ff01e0d-dbc6-403d-84d4-59ab22d9a08c", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "0a37c1bf-c4df-475a-bd0e-8e5799b7348c", + "id": "d40df7d1-29ad-4a1b-b67f-4fa1715b9604", "name": "branch", "value": "main", "type": "string" }, { - "id": "66ce6e02-7b4c-4337-acc8-0497eca716b2", + "id": "181b4931-53cb-4b3d-93df-4fc681901e05", "name": "artikel_ordner", "value": "artikel", "type": "string" }, { - "id": "f56d64e1-e73b-4aa8-b70f-a39b875bd41c", + "id": "f39283ce-c479-4566-aaab-4a2a1f58d802", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "7151540f-8cc2-4d3c-858d-68a92ffebda2", + "id": "f90a205e-fdc1-4079-bf9b-b4ee32663d00", "name": "nachbessern_ordner", "value": "nachbessern", "type": "string" }, { - "id": "2f1e3867-f450-4455-bb7f-4e739511a092", + "id": "6ff168b7-4cb4-42be-990d-7193915a81da", "name": "status_pfad", "value": "status-importe.json", "type": "string" }, { - "id": "26572729-abbc-4f56-8c4c-10d82053d209", + "id": "9135d2b6-887e-4f75-b150-6c66bc83738b", "name": "wp_base", "value": "https://skrift.de", "type": "string" }, { - "id": "f79e769d-4349-4312-a771-75003e1dc06f", + "id": "4350f279-2b06-4b2d-98c8-66318a09c399", "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 72 }}", "type": "number" }, { - "id": "ab009d25-5581-4b97-9f62-2479ebbb4eb6", + "id": "0bd22beb-337c-4245-b702-182c6774474d", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "21d61ca0-8b9e-472b-9980-5f9c9c992333", + "id": "2a18eec7-58de-43d0-a2c2-7e6c6961f27a", + "name": "elementor_ausgabe", + "value": true, + "type": "boolean" + }, + { + "id": "24a9e76c-f761-439a-a8b4-97092dff1814", + "name": "faq_ueberschrift", + "value": "Häufige Fragen", + "type": "string" + }, + { + "id": "f75796f9-4e41-434e-812f-7edfd84cd248", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -122,7 +134,7 @@ -240, 190 ], - "id": "78ef6b35-5a81-4f5d-a6c5-8587d8f5011f", + "id": "02d8abd1-fb76-472c-b763-a297cf9b3d9e", "name": "Konfiguration" }, { @@ -147,7 +159,7 @@ -20, 190 ], - "id": "261803f2-492f-40d5-9025-dd8b4f4ed321", + "id": "b4f7e1e9-dc21-4f98-9271-bba61b1b1d39", "name": "Artikelordner lesen", "credentials": { "httpHeaderAuth": { @@ -170,7 +182,7 @@ 200, 190 ], - "id": "6ad02233-4cbb-48d3-be1a-0e97dcc65650", + "id": "70da53d6-0889-4d9e-8c79-268aeab7c55f", "name": "Artikeldateien filtern" }, { @@ -184,7 +196,7 @@ }, "conditions": [ { - "id": "7f80b65e-4821-4071-a37f-3d47313c2d8f", + "id": "d8907dfa-944b-4e3c-826b-04ea4aa3a690", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -205,7 +217,7 @@ 420, 190 ], - "id": "3f559f53-d0db-47a1-a093-19652fff8f5f", + "id": "cee6dd86-6638-4139-9cbe-a9b2f1c010db", "name": "Artikel vorhanden?" }, { @@ -216,7 +228,7 @@ 640, 40 ], - "id": "db75bced-d1c9-4656-95fb-13054ae70099", + "id": "762e3de3-d0dc-4f7a-b33f-575329df610e", "name": "Nichts zu tun" }, { @@ -232,7 +244,7 @@ 640, 320 ], - "id": "c6453647-01d7-40cb-a987-832f64f88dcf", + "id": "56d2a5ce-4069-4bb0-8fc7-9ea81aaecf1d", "name": "Artikel nacheinander" }, { @@ -256,7 +268,7 @@ 900, 420 ], - "id": "99498cef-39e8-4072-ad37-935f88776a7f", + "id": "cffa46f3-dcec-4165-bedc-c0258ff786d5", "name": "Artikeldatei laden", "credentials": { "httpHeaderAuth": { @@ -280,7 +292,7 @@ 900, 640 ], - "id": "ceeef477-d968-4734-a1e7-541063617fdd", + "id": "3ad8a166-e94f-40d0-97dc-c66efe7315cc", "name": "Fehler Artikeldatei", "onError": "continueRegularOutput" }, @@ -306,7 +318,7 @@ 1120, 420 ], - "id": "4378f17c-fffb-4909-8a0c-329aab212806", + "id": "7bcc8532-384b-4b80-bde5-5019b8b13067", "name": "Brief laden", "credentials": { "httpHeaderAuth": { @@ -318,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\nreturn [{ json: { slug: meta.slug, artikel, brief, brief_vorhanden: !!brief,\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": "\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" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -326,7 +338,7 @@ 1340, 420 ], - "id": "cd588edb-dccb-42b4-9e52-07ee612667e7", + "id": "573a808f-5d24-4aec-adc3-31b5c1710e2d", "name": "Artikel pruefen" }, { @@ -345,7 +357,7 @@ }, "sendBody": true, "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}", + "jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}", "sendHeaders": true, "specifyHeaders": "json", "jsonHeaders": "={\"Content-Type\":\"application/json\"}" @@ -356,7 +368,7 @@ 1560, 420 ], - "id": "5e57d3cd-065f-4371-ae2b-62bf0190d389", + "id": "ba5aeeaa-78b2-4cd4-9df1-eb5709214071", "name": "Score ermitteln", "credentials": { "httpHeaderAuth": { @@ -380,7 +392,7 @@ 1560, 640 ], - "id": "bc9cdc94-249f-43d6-bb73-3a232bdc4d3f", + "id": "98617eeb-9cca-416e-b31e-3f72118972d7", "name": "Fehler Score", "onError": "continueRegularOutput" }, @@ -395,7 +407,7 @@ 1780, 420 ], - "id": "61d351aa-fae8-4b83-b383-e9e0eabe35ce", + "id": "bbb33e59-c2e0-4ace-9dd2-e1e75d9859bb", "name": "Score auswerten" }, { @@ -409,7 +421,7 @@ }, "conditions": [ { - "id": "5f9081ad-6a56-41cf-ac40-98df6e63d862", + "id": "160a4ec1-ada5-45fa-860e-77a612991f75", "leftValue": "={{ $json.bestanden }}", "rightValue": "", "operator": { @@ -430,13 +442,13 @@ 2000, 420 ], - "id": "e932d985-5556-452f-9315-56fbc87c9af9", + "id": "83cba8ad-255b-4065-9bac-486c22473d2b", "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.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 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" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -444,7 +456,7 @@ 2220, 640 ], - "id": "71a32ae7-9fa4-4c5f-b01b-c7498c5fcb82", + "id": "7443696e-d930-428a-9016-a8157d1a3c6e", "name": "Fehlende Begriffe ermitteln" }, { @@ -469,7 +481,7 @@ 2440, 640 ], - "id": "6ab382ae-462a-4638-91ce-bb6eb581729c", + "id": "4a19ef5f-785d-4d1b-a34b-95a56db54173", "name": "Nachbesserung vorhanden?", "credentials": { "httpHeaderAuth": { @@ -489,7 +501,7 @@ 2660, 640 ], - "id": "baa5802a-e510-4d0c-ad25-c41f6add1a41", + "id": "8e776ef8-83c0-4252-b1df-0988dcac30df", "name": "Nachbesserung vorbereiten" }, { @@ -519,7 +531,7 @@ 2880, 640 ], - "id": "fab0cf4b-3bfa-4999-922e-710eeeb4e3e5", + "id": "eedf1218-7b12-4dc9-b36c-3cd1845aadc7", "name": "Nachbesserung committen", "credentials": { "httpHeaderAuth": { @@ -543,7 +555,7 @@ 2880, 860 ], - "id": "e996cf98-d502-445c-96f4-f10cd24bcb2d", + "id": "f2e2311e-23fe-4de3-825e-2520bea60aa6", "name": "Fehler Nachbesserung", "onError": "continueRegularOutput" }, @@ -558,7 +570,7 @@ 3100, 640 ], - "id": "b9804e4e-7d67-4a69-8eff-db2eef089d31", + "id": "e73f8277-d65b-4ae0-8632-38cca861107f", "name": "Nachbesserung gemerkt" }, { @@ -577,7 +589,7 @@ }, "sendBody": true, "specifyBody": "json", - "jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}", + "jsonBody": "={{ JSON.stringify({ query: $json.artikel.query_id, html: $json.bewertungs_html || $json.artikel.content_html, title: $json.artikel.seo_titel || $json.artikel.titel, description: $json.artikel.seo_beschreibung || '' }) }}", "sendHeaders": true, "specifyHeaders": "json", "jsonHeaders": "={\"Content-Type\":\"application/json\"}" @@ -588,7 +600,7 @@ 2220, 240 ], - "id": "6a9a0e6d-ac62-449e-9423-0b8cce15e63a", + "id": "7b29edb4-7203-4272-8294-28bc65b91b43", "name": "Revision zu NeuronWriter", "credentials": { "httpHeaderAuth": { @@ -612,7 +624,7 @@ 2220, 60 ], - "id": "8164f9fa-b7a8-4054-9a2c-f858904e3c8e", + "id": "de95f5b3-cb3e-4bb4-91d0-3ce9c431e3f8", "name": "Fehler Revision", "onError": "continueRegularOutput" }, @@ -638,7 +650,7 @@ 2440, 240 ], - "id": "dbc8958d-e00a-49ff-b2ca-0ba1b21f23c6", + "id": "95d79724-0654-4883-bb8a-879293613b5c", "name": "Beitrag vorhanden?", "credentials": { "httpBasicAuth": { @@ -672,7 +684,7 @@ 2660, 240 ], - "id": "f577d6d9-6a14-4c17-8847-cd3cc5c27528", + "id": "2965f100-55d5-4a5c-b320-ae4d63906c5a", "name": "Kategorie suchen", "credentials": { "httpBasicAuth": { @@ -695,7 +707,7 @@ 2880, 240 ], - "id": "cf5e7318-28e2-4fef-8592-b6bc9e5569da", + "id": "09fa5259-508e-466d-9125-8f4d130beb63", "name": "Kategorie pruefen" }, { @@ -709,7 +721,7 @@ }, "conditions": [ { - "id": "f9af79a7-5da4-4711-a15a-683d518e4256", + "id": "a20d73bb-35db-4ecf-82d0-b62020fb9f94", "leftValue": "={{ $json.kategorie_fehlt }}", "rightValue": "", "operator": { @@ -730,7 +742,7 @@ 3100, 240 ], - "id": "e5896ca7-d4d5-4123-a98d-d9a15496187b", + "id": "923b0aac-5ca7-492b-a5c9-391acdce69d4", "name": "Kategorie anlegen?" }, { @@ -760,7 +772,7 @@ 3320, 140 ], - "id": "970d521b-0500-47e2-b7da-ee40d672c324", + "id": "fcdb245e-5788-4cb7-878c-98bfa4dd2db4", "name": "Kategorie anlegen", "credentials": { "httpBasicAuth": { @@ -783,13 +795,13 @@ 3540, 240 ], - "id": "db263677-f5a1-4a6a-a2ac-88667a2a1a5f", + "id": "09202e3f-4ace-4a1b-9eca-cb4b9ce3c3e4", "name": "Kategorie festlegen" }, { "parameters": { "mode": "runOnceForAllItems", - "jsCode": "\nconst s = $input.first().json;\nconst dup = $('Beitrag vorhanden?').first().json;\nconst a = s.artikel;\n\n// Duplikatspruefung: exakter Slug-Treffer entscheidet ueber anlegen oder aktualisieren.\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kd = K(dup);\nconst treffer = (dup.statusCode === 200 && Array.isArray(kd))\n ? kd.find(p => p.slug === s.slug) : null;\n\nconst heute = new Date().toISOString().slice(0, 10);\n\nconst meta = {\n kurzantwort: a.kurzantwort || '',\n cta_ziel: a.cta_ziel || '',\n stand_datum: a.stand_datum || heute,\n faq: Array.isArray(a.faq)\n ? a.faq.filter(f => f && f.frage).map(f => ({ frage: String(f.frage), antwort: String(f.antwort || '') }))\n : [],\n _seopress_titles_title: a.seo_titel || a.titel,\n _seopress_titles_desc: a.seo_beschreibung || '',\n _seopress_analysis_target_kw: a.keyword || '',\n};\n\nconst body = {\n title: a.titel,\n slug: s.slug,\n content: a.content_html,\n status: 'draft', // harte Vorgabe: nie automatisch veroeffentlichen\n categories: [s.kategorie_id],\n meta,\n};\nif (a.auszug) body.excerpt = a.auszug;\n\nreturn [{ json: {\n ...s,\n vorhanden: !!treffer,\n post_id: treffer ? treffer.id : null,\n alter_status: treffer ? treffer.status : null,\n wp_url: treffer\n ? `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts/${treffer.id}`\n : `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts`,\n wp_body: JSON.stringify(body),\n}}];\n" + "jsCode": "\nconst s = $input.first().json;\nconst dup = $('Beitrag vorhanden?').first().json;\nconst a = s.artikel;\n\n// Duplikatspruefung: exakter Slug-Treffer entscheidet ueber anlegen oder aktualisieren.\n// n8n liefert den Koerper bei fullResponse je nach Version als body oder data.\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kd = K(dup);\nconst treffer = (dup.statusCode === 200 && Array.isArray(kd))\n ? kd.find(p => p.slug === s.slug) : null;\n\nconst heute = new Date().toISOString().slice(0, 10);\n\nconst meta = {\n kurzantwort: a.kurzantwort || '',\n cta_ziel: a.cta_ziel || '',\n stand_datum: a.stand_datum || heute,\n faq: Array.isArray(a.faq)\n ? a.faq.filter(f => f && f.frage).map(f => ({ frage: String(f.frage), antwort: String(f.antwort || '') }))\n : [],\n _seopress_titles_title: a.seo_titel || a.titel,\n _seopress_titles_desc: a.seo_beschreibung || '',\n _seopress_analysis_target_kw: a.keyword || '',\n};\n\n// --- Elementor-Struktur aus Artikel und FAQ ---\n// Zerlegt den Text an den H2-Grenzen in Text-Editor-Widgets und haengt das FAQ\n// als nested-accordion an, dasselbe Widget wie auf den Leistungsseiten.\nconst konf = $('Konfiguration').first().json;\nconst eid = () => Math.random().toString(16).slice(2, 9);\nconst textWidget = (h) => ({ id: eid(), elType: 'widget', settings: { editor: h },\n elements: [], widgetType: 'text-editor' });\nconst behaelter = (kinder, innen) => {\n const s = { content_width: 'full' };\n if (innen) Object.assign(s, {\n _title: 'Element',\n padding: { unit: 'px', top: '8', right: '8', bottom: '8', left: '8', isLinked: true },\n flex_gap: { column: '20', row: '20', isLinked: true, unit: 'px', size: 20 },\n });\n const c = { id: eid(), elType: 'container', settings: s, elements: kinder };\n if (innen) { c.isInner = true; c.isLocked = true; }\n return c;\n};\n\nfunction elementorBauen() {\n const wurzel = [];\n if (a.kurzantwort) {\n wurzel.push(behaelter([textWidget(\n `

${a.kurzantwort}

`)]));\n }\n const html = String(a.content_html || '');\n const stellen = [];\n const re2 = /]/g;\n let mm;\n while ((mm = re2.exec(html)) !== null) stellen.push(mm.index);\n const bloecke = [];\n if (!stellen.length) {\n if (html.trim()) bloecke.push(html.trim());\n } else {\n if (stellen[0] > 0 && html.slice(0, stellen[0]).trim()) bloecke.push(html.slice(0, stellen[0]).trim());\n stellen.forEach((s, i) => {\n const ende = i + 1 < stellen.length ? stellen[i + 1] : html.length;\n const b = html.slice(s, ende).trim();\n if (b) bloecke.push(b);\n });\n }\n bloecke.forEach(b => wurzel.push(behaelter([textWidget(b)])));\n\n if (meta.faq.length) {\n const items = [], kinder = [];\n meta.faq.forEach(f => {\n items.push({ item_title: f.frage, _id: eid() });\n kinder.push(behaelter([textWidget(`

${f.antwort || ''}

`)], true));\n });\n wurzel.push(behaelter([\n textWidget(`

${konf.faq_ueberschrift || 'Häufige Fragen'}

`),\n { id: eid(), elType: 'widget', widgetType: 'nested-accordion',\n settings: { items, faq_schema: 'yes', default_state: 'all_collapsed' },\n elements: kinder },\n ]));\n }\n return wurzel;\n}\n\nif (konf.elementor_ausgabe) {\n meta._elementor_data = JSON.stringify(elementorBauen());\n meta._elementor_edit_mode = 'builder';\n meta._elementor_template_type = 'wp-post';\n}\n\nconst body = {\n title: a.titel,\n slug: s.slug,\n content: a.content_html,\n status: 'draft', // harte Vorgabe: nie automatisch veroeffentlichen\n categories: [s.kategorie_id],\n meta,\n};\nif (a.auszug) body.excerpt = a.auszug;\n\nreturn [{ json: {\n ...s,\n vorhanden: !!treffer,\n post_id: treffer ? treffer.id : null,\n alter_status: treffer ? treffer.status : null,\n wp_url: treffer\n ? `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts/${treffer.id}`\n : `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts`,\n wp_body: JSON.stringify(body),\n}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -797,7 +809,7 @@ 3760, 240 ], - "id": "c3163328-1964-44eb-ae44-1d32e6bad6e9", + "id": "2fb8e360-a7b5-409c-8688-5aa04851cc75", "name": "WordPress-Body bauen" }, { @@ -827,7 +839,7 @@ 3980, 240 ], - "id": "5bd4153d-b67b-4c71-88d1-bbd365c0409c", + "id": "614cf4f3-11a5-44c0-9342-ed36827cd5f0", "name": "Beitrag anlegen oder aktualisieren", "credentials": { "httpBasicAuth": { @@ -842,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 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 preis_shortcodes: v.preis_shortcodes || 0,\n}}];\n" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -850,7 +862,7 @@ 4200, 240 ], - "id": "f56e1f46-0255-4056-b8ef-b456d910b603", + "id": "3ad3d285-d1cf-4dc0-a2e2-bf3f95fd4c34", "name": "Ergebnis pruefen" }, { @@ -881,7 +893,7 @@ 4420, 240 ], - "id": "ca4cbfd2-2dca-4179-9c3b-80eff70a2ee3", + "id": "a7caa3d3-a44b-4d3f-9d74-895ad99e8f5d", "name": "Cache leeren", "credentials": { "httpBasicAuth": { @@ -901,7 +913,7 @@ 4640, 240 ], - "id": "05502f6a-0303-40c2-9292-f363102b6ae6", + "id": "7b8662eb-1bf1-464f-a153-976c4bafb905", "name": "Importergebnis merken" }, { @@ -915,9 +927,9 @@ 4860, 420 ], - "id": "783cac50-2646-45fd-9884-30e3497686d1", + "id": "e1588d91-d590-4092-b17b-62a0f4bd091d", "name": "Pause Rate Limit", - "webhookId": "308902ac-3e3c-429a-977e-bac53612e3a7" + "webhookId": "c135194f-02f6-4034-a27a-80412f32e2d8" }, { "parameters": { @@ -930,7 +942,7 @@ 900, 80 ], - "id": "ea85b7b5-9ad0-44f9-a4ff-eea1d99a8a66", + "id": "c3906d3f-17c2-44c4-ae4a-1fded6ecb063", "name": "Ergebnisse sammeln" }, { @@ -955,7 +967,7 @@ 1120, 80 ], - "id": "2e01f634-c7f4-477e-97c2-e2e460db0bb5", + "id": "65f716cc-6b5f-49fe-870c-43333ae12de6", "name": "Statustabelle lesen", "credentials": { "httpHeaderAuth": { @@ -975,7 +987,7 @@ 1340, 80 ], - "id": "d803d2e6-9508-40cc-b039-14caee2fe57a", + "id": "26726765-3fb4-4a97-bd5f-b54e1b2acbcd", "name": "Statustabelle fortschreiben" }, { @@ -989,7 +1001,7 @@ }, "conditions": [ { - "id": "374ba8a9-06b2-4606-b926-690758c243c2", + "id": "85e7ddc2-927d-4ffb-a25e-4da81fec7e0a", "leftValue": "={{ $json.ueberspringen }}", "rightValue": "", "operator": { @@ -1010,7 +1022,7 @@ 1560, 80 ], - "id": "19504889-db25-4c59-bdf5-e9867c76372c", + "id": "9a6ec416-dd7c-4904-bd56-3580d5bf6138", "name": "Gab es Importe?" }, { @@ -1040,7 +1052,7 @@ 1780, 160 ], - "id": "35dd3192-8726-431a-a08d-c8248085604d", + "id": "a3575c97-ef97-47f0-9c01-1d75df71c878", "name": "Statustabelle schreiben", "credentials": { "httpHeaderAuth": { @@ -1064,14 +1076,14 @@ 1780, 340 ], - "id": "1343e9d1-c1d8-4983-92c7-825b204601b4", + "id": "d4de1918-7f49-45b9-948a-d22ab2c36473", "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.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.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" }, "type": "n8n-nodes-base.code", "typeVersion": 2, @@ -1079,7 +1091,7 @@ 2000, 80 ], - "id": "f500deba-3ff8-4224-b4ba-1ceea76055b9", + "id": "255f8dee-0294-47e3-aaea-fc094a423dfa", "name": "Meldung formulieren" }, { @@ -1097,7 +1109,7 @@ 2220, 80 ], - "id": "4c8c5d7b-967a-4a41-8c04-b07d8efe1e1a", + "id": "e19c27c0-de80-4318-b965-cf2eb86d1824", "name": "Benachrichtigung", "disabled": true, "notes": "SMTP-Credential zuweisen, Absender eintragen, dann aktivieren."