diff --git a/build/artikel_schreiben.py b/build/artikel_schreiben.py new file mode 100644 index 0000000..0723e3c --- /dev/null +++ b/build/artikel_schreiben.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +"""Schreibt einen Artikel aus einer Python-Datei mit dict A und prueft ihn. Aufruf: python3 build/artikel_schreiben.py datei.py""" +import json, subprocess, sys +ns = {} +exec(open(sys.argv[1], encoding='utf-8').read(), ns) +a = ns['A'] +a.setdefault('stand_datum', '2026-09-22') +pfad = f"artikel/artikel-{a['slug']}.json" +json.dump(a, open(pfad, 'w', encoding='utf-8'), ensure_ascii=False, indent=2) +print('SEO', len(a['seo_titel']), len(a['seo_beschreibung'])) +subprocess.run(['python3', 'build/pruef_lokal.py', pfad]) +subprocess.run(['python3', 'build/nw_score.py', pfad]) diff --git a/build/gen_wf2.py b/build/gen_wf2.py index 6da8d6f..c4ec3cf 100644 --- a/build/gen_wf2.py +++ b/build/gen_wf2.py @@ -71,6 +71,10 @@ 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) : 65 }}", "type": "number"}, {"id": nid(), "name": "pause_zwischen_artikeln", "value": 5, "type": "number"}, + # Zielstatus fuer neue Beitraege und fuer Entwuerfe: publish (Vorgabe seit 22.09.2026) oder draft + {"id": nid(), "name": "ziel_status", "value": "={{ $json.body && ['draft','publish'].includes($json.body.status) ? $json.body.status : 'publish' }}", "type": "string"}, + # Veroeffentlichte Beitraege werden nie angefasst, ausser ausdruecklich per Webhook erlaubt + {"id": nid(), "name": "veroeffentlichte_ueberschreiben", "value": "={{ !!($json.body && $json.body.veroeffentlichte_ueberschreiben === true) }}", "type": "boolean"}, {"id": nid(), "name": "elementor_ausgabe", "value": False, "type": "boolean"}, {"id": nid(), "name": "bild_aktiv", "value": True, "type": "boolean"}, {"id": nid(), "name": "bild_modell", "value": "gpt-image-2", "type": "string"}, @@ -383,7 +387,30 @@ KAT_SUCHE = http("Kategorie suchen", [2660, 240], "GET", "={{ $('Konfiguration').first().json.wp_base }}/wp-json/wp/v2/categories?search={{ encodeURIComponent($('Score auswerten').first().json.artikel.kategorie) }}&per_page=100", CRED_WP, auth_typ="httpBasicAuth", error_out=False, retry=True, extra={"options": {"response": {"response": {"neverError": True, "fullResponse": True}}, "timeout": 60000}}) -link(DUP, KAT_SUCHE) + +# Schutz: Ein bereits veroeffentlichter Beitrag wird weder ueberschrieben noch auf Entwurf gesetzt. +SCHUTZ = code("Veroeffentlicht pruefen", [2440, 440], r""" +const s = $('Score auswerten').first().json; +const dup = $input.first().json; +const K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data); +const kd = K(dup); +const treffer = (dup.statusCode === 200 && Array.isArray(kd)) ? kd.find(p => p.slug === s.slug) : null; +const konf = $('Konfiguration').first().json; +const geschuetzt = !!treffer && treffer.status !== 'draft' && konf.veroeffentlichte_ueberschreiben !== true; +return [{ json: { slug: s.slug, score: s.score, geschuetzt, + post_id: treffer ? treffer.id : null, alter_status: treffer ? treffer.status : null } }]; +""") +link(DUP, SCHUTZ) +SCHUTZ_IF = iff("Schon veroeffentlicht?", [2660, 440], "={{ $json.geschuetzt }}", "boolean", "true") +link(SCHUTZ, SCHUTZ_IF) +SCHUTZ_NOTIZ = code("Uebersprungen merken", [2880, 540], """ +const v = $input.first().json; +return [{ json: { slug: v.slug, ergebnis: 'uebersprungen', score: v.score, post_id: v.post_id, + status: v.alter_status, + meldung: 'Beitrag ' + v.post_id + ' ist bereits ' + v.alter_status + ' und bleibt unveraendert.' } }]; +""") +link(SCHUTZ_IF, SCHUTZ_NOTIZ, out=0) +link(SCHUTZ_IF, KAT_SUCHE, out=1) KAT_PRUEF = code("Kategorie pruefen", [2880, 240], """ const s = $('Score auswerten').first().json; @@ -429,6 +456,7 @@ WP_VOR = code("WordPress-Body bauen", [3760, 240], r""" const s = $input.first().json; const dup = $('Beitrag vorhanden?').first().json; const a = s.artikel; +const zielStatus = $('Konfiguration').first().json.ziel_status === 'draft' ? 'draft' : 'publish'; // Duplikatspruefung: exakter Slug-Treffer entscheidet ueber anlegen oder aktualisieren. // n8n liefert den Koerper bei fullResponse je nach Version als body oder data. @@ -522,7 +550,7 @@ const body = { title: a.titel, slug: s.slug, content: a.content_html, - status: 'draft', // harte Vorgabe: nie automatisch veroeffentlichen + status: zielStatus, // publish seit 22.09.2026, per Webhook {"status":"draft"} umschaltbar categories: [s.kategorie_id], meta, }; @@ -536,6 +564,7 @@ return [{ json: { wp_url: treffer ? `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts/${treffer.id}` : `${$('Konfiguration').first().json.wp_base}/wp-json/wp/v2/posts`, + ziel_status: zielStatus, wp_body: JSON.stringify(body), }}]; """) @@ -555,9 +584,9 @@ if (!p.id) { throw new Error('WordPress lieferte keine Beitrags-ID zurueck. Antwort: ' + JSON.stringify(p).slice(0, 400)); } -if (p.status !== 'draft') { +if (p.status !== v.ziel_status) { throw new Error('Beitrag ' + p.id + ' hat Status "' + p.status - + '" statt "draft". Der Workflow darf nie veroeffentlichen. Bitte pruefen.'); + + '" statt "' + v.ziel_status + '". Bitte Rechte des Benutzers pruefen.'); } // Gegenprobe: sind die Meta-Felder wirklich angekommen? @@ -612,7 +641,7 @@ link(CACHE, CACHE_NOTIZ) PAUSE = node("Pause Rate Limit", "n8n-nodes-base.wait", 1.1, [4860, 420], {"amount": "={{ $('Konfiguration').first().json.pause_zwischen_artikeln }}", "unit": "seconds"}, webhookId=str(uuid.uuid4())) -for q in [NACH_OK, NACH_FEHLER, FEHLER_ART, FEHLER_EVAL]: +for q in [NACH_OK, NACH_FEHLER, FEHLER_ART, FEHLER_EVAL, SCHUTZ_NOTIZ]: link(q, PAUSE) link(PAUSE, LOOP) @@ -750,6 +779,7 @@ const alle = $input.all().map(i => i.json); const importiert = alle.filter(r => r.ergebnis === 'importiert'); const nachbessern = alle.filter(r => r.ergebnis === 'nachbessern'); const fehler = alle.filter(r => r.ergebnis === 'fehler'); +const uebersprungen = alle.filter(r => r.ergebnis === 'uebersprungen'); const zeilen = importiert.map(r => ({ slug: r.slug, score: r.score, datum: new Date().toISOString(), @@ -757,11 +787,11 @@ const zeilen = importiert.map(r => ({ aktion: r.aktion, kategorie: r.kategorie, })); -return [{ json: { alle, importiert, nachbessern, fehler, +return [{ json: { alle, importiert, nachbessern, fehler, uebersprungen, neue_zeilen: zeilen, anzahl_importiert: importiert.length, anzahl_nachbessern: nachbessern.length, - anzahl_fehler: fehler.length }}]; + anzahl_fehler: fehler.length, anzahl_uebersprungen: uebersprungen.length }}]; """) link(LOOP, SAMMELN, out=0) @@ -820,13 +850,13 @@ link(ST_SCHREIB, ST_FEHLER, out=1) MAIL = code("Meldung formulieren", [2000, 80], r""" const s = $('Ergebnisse sammeln').first().json; const z = []; -z.push(`Importiert: ${s.anzahl_importiert} | Nachbessern: ${s.anzahl_nachbessern} | Fehler: ${s.anzahl_fehler}`); +z.push(`Importiert: ${s.anzahl_importiert} | Unveraendert (veroeffentlicht): ${s.anzahl_uebersprungen} | Nachbessern: ${s.anzahl_nachbessern} | Fehler: ${s.anzahl_fehler}`); z.push(''); if (s.importiert.length) { - z.push('Als Entwurf in WordPress:'); + z.push('In WordPress (Status je Beitrag):'); s.importiert.forEach(r => { - z.push(` ${r.slug} (Score ${r.score}, ${r.aktion})`); + z.push(` ${r.slug} (Score ${r.score}, ${r.aktion}, ${r.status})`); z.push(` Vorschau: ${r.vorschau_url}`); z.push(` Bearbeiten: ${r.bearbeiten_url}`); if (r.elementor) z.push(` Layout: ${r.elementor}`); @@ -867,10 +897,11 @@ if (s.fehler.length) { s.fehler.forEach(r => z.push(` ${r.slug || '?'} [${r.stufe}]: ${r.meldung}`)); z.push(''); } -z.push('Kein Beitrag wurde veroeffentlicht. Alle Importe stehen auf Entwurf.'); +if (s.uebersprungen.length) { z.push('Bereits veroeffentlicht, nicht angefasst:'); s.uebersprungen.forEach(r => z.push(` ${r.slug} (Beitrag ${r.post_id})`)); z.push(''); } +z.push('Veroeffentlichte Beitraege werden nie ueberschrieben oder auf Entwurf gesetzt.'); return [{ json: { - betreff: `Skrift Ratgeber: ${s.anzahl_importiert} Entwurf/Entwuerfe` + betreff: `Skrift Ratgeber: ${s.anzahl_importiert} importiert` + (s.anzahl_nachbessern ? `, ${s.anzahl_nachbessern} zum Nachbessern` : '') + (s.anzahl_fehler ? `, ${s.anzahl_fehler} Fehler` : ''), text: z.join('\n') }}]; diff --git a/build/pruef_lokal.py b/build/pruef_lokal.py index 88c4774..bf7c4f9 100644 --- a/build/pruef_lokal.py +++ b/build/pruef_lokal.py @@ -28,7 +28,7 @@ if re.search('[–—]', c + a['kurzantwort'] + faqt): probleme.append("Gedanken if re.search(r'\bich\b', plain, re.I): probleme.append("Ich-Form") for s in re.findall(r'\[ratgeber_link slug="([^"]+)"', c): if s not in slugs: probleme.append(f"unbekannter Slug {s}") -if a['slug'] in ('kaltakquise-per-brief',) and 'Keine Rechtsberatung: Dieser Beitrag gibt den Stand unserer Praxiserfahrung wieder' not in plain: +if a['slug'] in ('kaltakquise-per-brief', 'serienbriefe-unterschreiben', 'dsgvo-briefwerbung') and 'Keine Rechtsberatung: Dieser Beitrag gibt den Stand unserer Praxiserfahrung wieder' not in plain: probleme.append("Disclaimer fehlt") if os.path.exists(bp): b = json.load(open(bp, encoding='utf-8')) diff --git a/build/wf2_deploy.py b/build/wf2_deploy.py new file mode 100644 index 0000000..59e14a8 --- /dev/null +++ b/build/wf2_deploy.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +"""Spielt workflows/pruefen-und-importieren.json in n8n ein, Credentials und Webhook-IDs bleiben erhalten.""" +import json, os, urllib.request +w = json.load(open('workflows/pruefen-und-importieren.json', encoding='utf-8')) +wid_ = json.load(open('build/workflow-ids.json'))['pruefen-und-importieren'] +H = {'X-N8N-API-KEY': os.environ['N8N_API_KEY'], 'Content-Type': 'application/json'} +base = os.environ['N8N_BASE_URL'].rstrip('/') + '/api/v1/workflows/' + wid_ +alt = json.load(urllib.request.urlopen(urllib.request.Request(base, headers=H))) +cred = {n['name']: n.get('credentials') for n in alt['nodes'] if n.get('credentials')} +hook = {n['name']: n.get('webhookId') for n in alt['nodes'] if n.get('webhookId')} +for n in w['nodes']: + if n['name'] in cred: n['credentials'] = cred[n['name']] + if n['name'] in hook: n['webhookId'] = hook[n['name']] +erlaubt = ('executionOrder', 'saveManualExecutions', 'callerPolicy', 'errorWorkflow', 'timezone', + 'saveDataErrorExecution', 'saveDataSuccessExecution', 'saveExecutionProgress', 'executionTimeout') +body = {'name': alt['name'], 'nodes': w['nodes'], 'connections': w['connections'], + 'settings': {k: v for k, v in (alt.get('settings') or {}).items() if k in erlaubt}} +d = json.load(urllib.request.urlopen(urllib.request.Request(base, data=json.dumps(body).encode(), headers=H, method='PUT'))) +print('aktualisiert', d['id'], 'aktiv' if d['active'] else 'inaktiv', len(d['nodes']), 'Nodes') diff --git a/workflows/pruefen-und-importieren.json b/workflows/pruefen-und-importieren.json index a53be79..068314c 100644 --- a/workflows/pruefen-und-importieren.json +++ b/workflows/pruefen-und-importieren.json @@ -9,7 +9,7 @@ -460, 100 ], - "id": "be3731b7-58d4-44e6-acc5-6272dbb3b681", + "id": "8dfcd4cf-3e46-472f-9169-206da188aa5e", "name": "Manuell starten" }, { @@ -25,9 +25,9 @@ -460, 280 ], - "id": "0755c10f-e537-4a17-a15a-9e405f5947ce", + "id": "6a8c3125-ee00-4db4-a5bc-41643290c852", "name": "Webhook Start", - "webhookId": "9866920d-2335-4c01-a289-c2b9aca47d8c" + "webhookId": "e5916b78-4476-423f-86af-837692d9491b" }, { "parameters": { @@ -35,115 +35,127 @@ "assignments": { "assignments": [ { - "id": "2f0d5d8d-4f12-4a78-8fb4-f66aa4a0d01a", + "id": "636f1ae6-aa7b-48d5-be02-363cb71d0d90", "name": "nw_base", "value": "https://app.neuronwriter.com/neuron-api/0.5/writer", "type": "string" }, { - "id": "d5bc9cac-5db5-40e3-bdad-2ceca39ac584", + "id": "b4331f4c-ff38-4525-9864-30cac5f9280b", "name": "gitea_base", "value": "http://gitea:3000", "type": "string" }, { - "id": "b91fd120-5168-4712-90a9-0b4049a47cdb", + "id": "0aad1f49-e77f-4c24-bc51-b8ed77df52ec", "name": "gitea_owner", "value": "lucas.orth", "type": "string" }, { - "id": "c757cc8c-f990-4e22-ab6a-7e7fda31e720", + "id": "70b0b497-86e6-4960-8ff0-6823c60a76fe", "name": "gitea_repo", "value": "Skrift-Auto-Article", "type": "string" }, { - "id": "b0fdc07a-a7d4-4e78-8b8f-db50fb5d8a00", + "id": "f3ca9037-0560-4e1f-ac62-cce8829f2e74", "name": "branch", "value": "main", "type": "string" }, { - "id": "84922b0c-84de-4d50-8bda-f8562594ea00", + "id": "cc272b17-7954-4732-ac36-83827206eba1", "name": "artikel_ordner", "value": "artikel", "type": "string" }, { - "id": "18d46746-ae2c-44c7-aab2-918133d4a29d", + "id": "fc1fa5dd-ff86-4358-a265-2cecb1726411", "name": "brief_ordner", "value": "briefe", "type": "string" }, { - "id": "24d34020-9424-47ea-8a2a-6ff1805c9424", + "id": "93011f19-b5a0-47c0-8263-457db34c0d50", "name": "nachbessern_ordner", "value": "nachbessern", "type": "string" }, { - "id": "cc559866-a0b2-46be-b3cb-f27d2b9ff47f", + "id": "19eac279-1305-4319-bf49-a467789e4708", "name": "status_pfad", "value": "status-importe.json", "type": "string" }, { - "id": "e5a30c70-dde6-4250-af13-1d49a8e4ab76", + "id": "20fe4cd8-5578-4841-b0a4-693af0a0b839", "name": "wp_base", "value": "https://skrift.de", "type": "string" }, { - "id": "af531179-ad15-40f9-bc3e-7ef6051653ba", + "id": "81c3304a-ba48-446f-b671-168f4d0dff1b", "name": "score_ziel", "value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}", "type": "number" }, { - "id": "794423e7-cd48-448d-94c6-787f66bb6af7", + "id": "e3c1042c-1615-4c8e-b414-01d10bd8ecad", "name": "pause_zwischen_artikeln", "value": 5, "type": "number" }, { - "id": "d046a1fc-c6c2-4b19-8fc6-744ecaded14a", + "id": "93592e6b-2ec9-4e47-8652-0414e901e0f4", + "name": "ziel_status", + "value": "={{ $json.body && ['draft','publish'].includes($json.body.status) ? $json.body.status : 'publish' }}", + "type": "string" + }, + { + "id": "0519f374-c7a8-490f-8cfe-d5c5223dc363", + "name": "veroeffentlichte_ueberschreiben", + "value": "={{ !!($json.body && $json.body.veroeffentlichte_ueberschreiben === true) }}", + "type": "boolean" + }, + { + "id": "d0a2c6d2-600b-4591-b926-32ee99b5d9e4", "name": "elementor_ausgabe", "value": false, "type": "boolean" }, { - "id": "8c21a814-4f0f-4d47-afdd-d17efa005a98", + "id": "b7b839d6-cff2-418d-8c59-dbe53190336c", "name": "bild_aktiv", "value": true, "type": "boolean" }, { - "id": "9e35d60a-4922-41be-bf71-a1d63da06ab6", + "id": "d7de3a65-b360-472a-9bad-12c0c2d4ac97", "name": "bild_modell", "value": "gpt-image-2", "type": "string" }, { - "id": "1103be64-00cd-4948-9a9f-2bc65af5b6cd", + "id": "4ee8a4a0-b50d-4d24-9554-effdcdb2cf52", "name": "bild_groesse", "value": "1200x1200", "type": "string" }, { - "id": "2da63505-15cc-43dc-9430-5091c8f4cc0c", + "id": "81f02b0c-4cf2-4f54-88a9-d067f1743d60", "name": "bild_qualitaet", "value": "medium", "type": "string" }, { - "id": "459d8553-1df2-4141-b9c1-45c505ef2b77", + "id": "e64d7ce1-be63-490a-a748-41eb61633a93", "name": "faq_ueberschrift", "value": "Häufige Fragen", "type": "string" }, { - "id": "bc459271-c4a2-408b-a64f-7945f943e785", + "id": "107f2b9a-8bed-4373-9f55-1f62966352e9", "name": "benachrichtigung_an", "value": "privat@lucas-orth.de", "type": "string" @@ -158,7 +170,7 @@ -240, 190 ], - "id": "59fcd646-0bc1-4ace-b073-dd3b634fc13a", + "id": "07896cf2-5b62-431b-9dc6-b8e1aa176bd5", "name": "Konfiguration" }, { @@ -183,7 +195,7 @@ -20, 190 ], - "id": "cc17c686-84fc-4ebc-84d1-09d39dedb732", + "id": "d0343c7f-f598-4121-9733-1db8828a3491", "name": "Artikelordner lesen", "credentials": { "httpHeaderAuth": { @@ -206,7 +218,7 @@ 200, 190 ], - "id": "25ffe9be-454d-4ce6-8ded-e1cbc29ae3ca", + "id": "dc764574-996e-4c3b-8f2a-7b666e691d65", "name": "Artikeldateien filtern" }, { @@ -220,7 +232,7 @@ }, "conditions": [ { - "id": "05d539f6-ce93-45de-adf5-d2833aa28cd1", + "id": "bdc37de0-462a-4dd9-9211-1938ca859828", "leftValue": "={{ $json.leer === true }}", "rightValue": "", "operator": { @@ -241,7 +253,7 @@ 420, 190 ], - "id": "f5084550-fb1d-4a97-8dc0-e0edea4f78a2", + "id": "3eb55a51-9521-4978-8a71-68fb3a79587a", "name": "Artikel vorhanden?" }, { @@ -252,7 +264,7 @@ 640, 40 ], - "id": "302a85d0-4238-4cf8-83d6-f508850fb265", + "id": "68d0b86b-2fe1-4e82-956a-d5e1fbee8ccd", "name": "Nichts zu tun" }, { @@ -268,7 +280,7 @@ 640, 320 ], - "id": "387aa2cd-4dfc-440c-8cdc-54c6bdb0f92f", + "id": "00d03ba5-ccca-4951-9eb6-a802c5c29844", "name": "Artikel nacheinander" }, { @@ -292,7 +304,7 @@ 900, 420 ], - "id": "08d2b6c6-e75b-49ea-a303-5f299977eb68", + "id": "f67b6e20-9ce9-4077-82b7-764acfec37c4", "name": "Artikeldatei laden", "credentials": { "httpHeaderAuth": { @@ -316,7 +328,7 @@ 900, 640 ], - "id": "b8dff49e-5773-4438-b2ea-806c0728667d", + "id": "e805470f-5857-4512-b739-a0f2900c8409", "name": "Fehler Artikeldatei", "onError": "continueRegularOutput" }, @@ -342,7 +354,7 @@ 1120, 420 ], - "id": "34db62e0-4b60-4db1-94cc-4d4634e77048", + "id": "fbe56e62-b6df-40e4-b654-fab5489bf953", "name": "Brief laden", "credentials": { "httpHeaderAuth": { @@ -362,7 +374,7 @@ 1340, 420 ], - "id": "f43fa611-de36-49c4-844b-7d127fbc5585", + "id": "29e2df6f-9ffd-405e-a19d-b4c21926c894", "name": "Artikel pruefen" }, { @@ -392,7 +404,7 @@ 1560, 420 ], - "id": "8baae2de-d269-4004-8a65-b56a3df52980", + "id": "e4321b66-6deb-4e66-b32c-ca891611c8e4", "name": "Score ermitteln", "credentials": { "httpHeaderAuth": { @@ -416,7 +428,7 @@ 1560, 640 ], - "id": "2345ad56-70bf-4e9e-95e5-078f32873908", + "id": "a8a17e68-41ed-4ae6-9815-65f108f1496a", "name": "Fehler Score", "onError": "continueRegularOutput" }, @@ -431,7 +443,7 @@ 1780, 420 ], - "id": "bb5f3c85-3ce9-4ef6-9472-5c5d1041119a", + "id": "6572a555-9bfd-42d8-824a-18cd74097b8e", "name": "Score auswerten" }, { @@ -445,7 +457,7 @@ }, "conditions": [ { - "id": "d6bb630a-83c0-4ef6-b58f-b3f04c7e0420", + "id": "7bca083e-4477-43c1-b3bf-0ddfb065199c", "leftValue": "={{ $json.bestanden }}", "rightValue": "", "operator": { @@ -466,7 +478,7 @@ 2000, 420 ], - "id": "77d875da-9aa0-4e70-84e5-14f73ec2a275", + "id": "72102d42-db0c-446d-84fd-f932028349ab", "name": "Score erreicht?" }, { @@ -480,7 +492,7 @@ 2220, 640 ], - "id": "fead1ab1-34ed-476e-96b9-0fac9d7b6758", + "id": "0d0e4e26-b6f9-47dd-8be0-646859bc5963", "name": "Fehlende Begriffe ermitteln" }, { @@ -505,7 +517,7 @@ 2440, 640 ], - "id": "c8629336-a50d-4b58-9ae4-da4ebfcb22d0", + "id": "26082611-adc6-4891-9fe6-689ddde0136a", "name": "Nachbesserung vorhanden?", "credentials": { "httpHeaderAuth": { @@ -525,7 +537,7 @@ 2660, 640 ], - "id": "946d4b02-8c02-43eb-b6c9-1347691487a5", + "id": "cba1c6f3-bb0f-4fd2-9b0c-c6c64792ba5a", "name": "Nachbesserung vorbereiten" }, { @@ -555,7 +567,7 @@ 2880, 640 ], - "id": "aeff7f66-195f-4434-92d9-ec9ddea02495", + "id": "7f74cc97-71d4-4cdb-af9f-daf78d2ce6f7", "name": "Nachbesserung committen", "credentials": { "httpHeaderAuth": { @@ -579,7 +591,7 @@ 2880, 860 ], - "id": "b868dae0-af73-442f-8962-f028ee5919ae", + "id": "8bf3f1d3-b23d-43ad-a99e-3593ade4b817", "name": "Fehler Nachbesserung", "onError": "continueRegularOutput" }, @@ -594,7 +606,7 @@ 3100, 640 ], - "id": "cfb38044-2ef6-4a04-98d5-e3b100c1abe3", + "id": "bc911e6b-c477-451a-a8d5-dc50a6e21588", "name": "Nachbesserung gemerkt" }, { @@ -624,7 +636,7 @@ 2220, 240 ], - "id": "87024225-d5c5-4f6e-8de0-2dc8c8779c18", + "id": "ee9645b0-1d05-4713-a0db-908fa9c55668", "name": "Revision zu NeuronWriter", "credentials": { "httpHeaderAuth": { @@ -648,7 +660,7 @@ 2220, 60 ], - "id": "477ca965-5538-4f95-ad0a-4f303a628ddf", + "id": "baa41bfa-a7f1-49e3-875a-7b9849b0d628", "name": "Fehler Revision", "onError": "continueRegularOutput" }, @@ -674,7 +686,7 @@ 2440, 240 ], - "id": "d66ed8ec-6c8c-4341-b470-5481b84ea3b9", + "id": "a87edaed-7b56-4d09-869e-7d24cd36864d", "name": "Beitrag vorhanden?", "credentials": { "httpBasicAuth": { @@ -708,7 +720,7 @@ 2660, 240 ], - "id": "93bbd3d1-f4d2-4e3a-b725-b439c04a0743", + "id": "bf18af8e-0c19-4126-b301-0860923e0090", "name": "Kategorie suchen", "credentials": { "httpBasicAuth": { @@ -720,6 +732,69 @@ "maxTries": 3, "waitBetweenTries": 3000 }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst s = $('Score auswerten').first().json;\nconst dup = $input.first().json;\nconst K = (r) => (r && r.body !== undefined) ? r.body : ((r || {}).data);\nconst kd = K(dup);\nconst treffer = (dup.statusCode === 200 && Array.isArray(kd)) ? kd.find(p => p.slug === s.slug) : null;\nconst konf = $('Konfiguration').first().json;\nconst geschuetzt = !!treffer && treffer.status !== 'draft' && konf.veroeffentlichte_ueberschreiben !== true;\nreturn [{ json: { slug: s.slug, score: s.score, geschuetzt,\n post_id: treffer ? treffer.id : null, alter_status: treffer ? treffer.status : null } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2440, + 440 + ], + "id": "5d8d994d-23dd-428e-9aa9-6ad8ad532f22", + "name": "Veroeffentlicht pruefen" + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "e6f8c006-bd95-426a-9a80-9edb2cdacffd", + "leftValue": "={{ $json.geschuetzt }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "looseTypeValidation": true, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + 2660, + 440 + ], + "id": "3c3cc9f1-11e2-4e58-94d8-dded22d2c71f", + "name": "Schon veroeffentlicht?" + }, + { + "parameters": { + "mode": "runOnceForAllItems", + "jsCode": "\nconst v = $input.first().json;\nreturn [{ json: { slug: v.slug, ergebnis: 'uebersprungen', score: v.score, post_id: v.post_id,\n status: v.alter_status,\n meldung: 'Beitrag ' + v.post_id + ' ist bereits ' + v.alter_status + ' und bleibt unveraendert.' } }];\n" + }, + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 2880, + 540 + ], + "id": "a2e326e8-48d0-474f-92ee-1f5462e3c501", + "name": "Uebersprungen merken" + }, { "parameters": { "mode": "runOnceForAllItems", @@ -731,7 +806,7 @@ 2880, 240 ], - "id": "784c106d-1d45-4de5-9840-8e3d331c0a11", + "id": "be0d102b-3080-4783-91d2-1939e5562d0c", "name": "Kategorie pruefen" }, { @@ -745,7 +820,7 @@ }, "conditions": [ { - "id": "ae40aca1-0466-4e7f-97b5-032b36d76c33", + "id": "45a95063-b4ce-4b80-a52d-a1c6b47dabfa", "leftValue": "={{ $json.kategorie_fehlt }}", "rightValue": "", "operator": { @@ -766,7 +841,7 @@ 3100, 240 ], - "id": "8fe4d39e-a88f-420c-a35a-27e3eb5fd3e1", + "id": "62cc195c-8da1-4102-8eb1-258ea6e2a33b", "name": "Kategorie anlegen?" }, { @@ -796,7 +871,7 @@ 3320, 140 ], - "id": "be37bc13-b8a2-4a2c-ac3e-a253af1d5e05", + "id": "7cce5de6-79b6-446b-ab43-064da6bd620a", "name": "Kategorie anlegen", "credentials": { "httpBasicAuth": { @@ -819,13 +894,13 @@ 3540, 240 ], - "id": "64489c08-1877-409d-b680-5884846fffc5", + "id": "d274121c-5951-4a67-8a58-de4068309038", "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 faq_ueberschrift: a.faq_ueberschrift || '',\n cta_ziel: a.cta_ziel || '',\n stand_datum: a.stand_datum || heute,\n ratgeber_reihenfolge: Number.isFinite(Number(a.reihenfolge)) && a.reihenfolge !== null && a.reihenfolge !== '' ? Number(a.reihenfolge) : 999,\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}
${f.antwort || ''}
`)], true));\n });\n wurzel.push(behaelter([\n textWidget(`${a.kurzantwort}
${f.antwort || ''}
`)], true));\n });\n wurzel.push(behaelter([\n textWidget(`