]*>[^<]*(Muster anfordern|Handschriftmuster)/i.test(html)) add('Baustein doppelt: Muster-CTA', 'Abschnitt "Kostenloses Handschriftmuster anfordern." entfernen, er steht im Seitenlayout');
+ // Querverweise auf andere Ratgeberartikel
+ const quer = [...html.matchAll(/\[ratgeber_link\s+slug="([^"]+)"\]([\s\S]*?)\[\/ratgeber_link\]/g)];
+ if (quer.length < 2) add('Querverweise: zu wenige', `${quer.length} [ratgeber_link], mindestens 2 auf verwandte Ratgeberartikel`);
+ quer.filter(q => q[1] === artikel.slug).forEach(() => add('Querverweis auf sich selbst', artikel.slug));
+ quer.filter(q => !q[2].trim()).forEach(q => add('Querverweis ohne Ankertext', q[1]));
+ (html.match(/href="https?:\/\/(www\.)?skrift\.de\/(?!handgeschriebene-|unterschriftenservice|automatisierte-follow-ups|papier-schriftmuster|kontakt|handschrift-fuer)[a-z0-9-]+\/?"/g) || [])
+ .forEach(h => add('Ratgeber-Link als harter Link', `${h} als [ratgeber_link slug="..."] setzen, sonst droht ein toter Link`));
+
const unbekannt = [...new Set(slugs.filter(s => s && !BUSINESS.includes(s) && !NEUTRAL.includes(s) && !PRIVAT.includes(s)))];
unbekannt.forEach(s => add('Links: kein Linkziel fuer Ratgeber', `/${s}/ (z. B. Dankeseite, Rechtstexte)`));
diff --git a/wordpress/skrift-ratgeber-felder.php b/wordpress/skrift-ratgeber-felder.php
index 67177dd..eef289f 100644
--- a/wordpress/skrift-ratgeber-felder.php
+++ b/wordpress/skrift-ratgeber-felder.php
@@ -2,7 +2,7 @@
/**
* Plugin Name: Skrift Ratgeber Felder
* Description: Meta-Felder fuer Ratgeberartikel (kurzantwort, faq, faq_ueberschrift, cta_ziel, stand_datum) mit Eingabebox im Editor, Shortcodes, Elementor-Widgets fuer FAQ und Kurzantwort sowie Dynamic Tags fuer Lesezeit, FAQ-Ueberschrift und Stand.
- * Version: 1.3.0
+ * Version: 1.4.0
* Author: Lucas Orth
*
* Ablage: wp-content/mu-plugins/skrift-ratgeber-felder.php
@@ -362,6 +362,39 @@ add_shortcode( 'skrift_faq', function ( $atts ) {
) );
} );
+/**
+ * Querverweis auf einen anderen Ratgeberartikel.
+ * [ratgeber_link slug="kaltakquise-per-brief"]Ankertext[/ratgeber_link]
+ * Veroeffentlichter Zielbeitrag: normaler Link auf den aktuellen Permalink.
+ * Noch nicht veroeffentlicht: nur der Ankertext, damit kein toter Link entsteht.
+ * Wer den Beitrag bearbeiten darf, sieht in der Vorschau einen gestrichelten
+ * Hinweis auf den noch fehlenden Zielartikel.
+ */
+add_shortcode( 'ratgeber_link', function ( $atts, $inhalt = '' ) {
+ $atts = shortcode_atts( array( 'slug' => '' ), $atts, 'ratgeber_link' );
+ $slug = sanitize_title( $atts['slug'] );
+ $text = '' !== trim( (string) $inhalt ) ? do_shortcode( $inhalt ) : $slug;
+ if ( '' === $slug ) {
+ return $text;
+ }
+ $ziel = get_posts( array(
+ 'name' => $slug,
+ 'post_type' => 'post',
+ 'post_status' => array( 'publish', 'draft', 'pending', 'future', 'private' ),
+ 'numberposts' => 1,
+ 'suppress_filters' => true,
+ ) );
+ if ( $ziel && 'publish' === $ziel[0]->post_status ) {
+ return '' . $text . '';
+ }
+ if ( is_user_logged_in() && current_user_can( 'edit_posts' ) ) {
+ $status = $ziel ? $ziel[0]->post_status : 'fehlt';
+ return '' . $text . '';
+ }
+ return $text;
+} );
+
add_shortcode( 'skrift_kurzantwort', function ( $atts ) {
$atts = shortcode_atts( array( 'post_id' => 0 ), $atts, 'skrift_kurzantwort' );
return skrift_kurzantwort_html( $atts['post_id'] );
diff --git a/workflows/brief-vorbereiten.json b/workflows/brief-vorbereiten.json
index f8170a9..7b259d9 100644
--- a/workflows/brief-vorbereiten.json
+++ b/workflows/brief-vorbereiten.json
@@ -9,7 +9,7 @@
-460,
40
],
- "id": "4fdca0a5-7851-4f7b-9f3c-276418464202",
+ "id": "1dab9a40-e8e4-4ed9-9d08-f2fb190e84e9",
"name": "Manuell starten"
},
{
@@ -33,7 +33,7 @@
-460,
200
],
- "id": "f9adf55b-7fff-42e9-b6da-d57af4835257",
+ "id": "97b85f88-70a1-4809-9d9f-c4c42bd3e9e3",
"name": "Zeitplan Montag 07:00",
"disabled": true
},
@@ -50,9 +50,9 @@
-460,
360
],
- "id": "208692e2-bdc6-4d22-95e7-bad567ee6b74",
+ "id": "31c56760-74e3-47ce-8a4d-6bd2c7457b57",
"name": "Webhook Start",
- "webhookId": "75113a14-9f13-4021-a15d-db58b2a57fd8"
+ "webhookId": "a8ccffdc-60e5-44cf-a265-36a8adc3f8e3"
},
{
"parameters": {
@@ -60,85 +60,85 @@
"assignments": {
"assignments": [
{
- "id": "4ddbf6e7-68d3-4e8b-bb73-d1f63f26d6e2",
+ "id": "f6274e65-1885-4e17-9705-000b0e6278e8",
"name": "nw_base",
"value": "https://app.neuronwriter.com/neuron-api/0.5/writer",
"type": "string"
},
{
- "id": "a35095d9-6dfd-4f04-a506-d7694a65f570",
+ "id": "d147d08b-df1e-4c2c-9c61-ca8b98964e04",
"name": "nw_project",
"value": "3da4d745e0e223cb",
"type": "string"
},
{
- "id": "24dcf02e-1af6-4275-bc00-f602b4b84356",
+ "id": "f19059cd-b921-4e8d-b858-7a82f3b7cd13",
"name": "nw_engine",
"value": "google.de",
"type": "string"
},
{
- "id": "b35bfea0-9e67-4844-973d-a7ba42853675",
+ "id": "fe8977fb-eda6-4822-906e-2a540af9800c",
"name": "nw_language",
"value": "German",
"type": "string"
},
{
- "id": "399912b4-6998-4882-9dc6-da19fe6863a2",
+ "id": "c0e4d454-74d6-47a1-aeaa-afe02be159df",
"name": "gitea_base",
"value": "http://gitea:3000",
"type": "string"
},
{
- "id": "7729e907-6ba5-419e-b90d-82cba600bd1f",
+ "id": "1b412202-179b-435d-bb0d-91e6457b0e77",
"name": "gitea_owner",
"value": "lucas.orth",
"type": "string"
},
{
- "id": "0315283f-06eb-460b-af29-5ba407f35e5e",
+ "id": "dc874ab8-3902-4f47-8ce5-eb942b6ed0a3",
"name": "gitea_repo",
"value": "Skrift-Auto-Article",
"type": "string"
},
{
- "id": "1bef1534-8459-4915-8b19-889c1103cf15",
+ "id": "15d0ffcf-3553-469a-ae47-4611dba81385",
"name": "branch",
"value": "main",
"type": "string"
},
{
- "id": "cf49930f-99c3-4889-9894-850a21c174b2",
+ "id": "aef6b9cb-6e4e-431f-9170-3348609db189",
"name": "index_pfad",
"value": "artikel-index.json",
"type": "string"
},
{
- "id": "2a3ab33d-64bf-466e-85f3-fafb1df95b4d",
+ "id": "558a0e5e-dedb-4980-ae3e-8b9d5ebc2073",
"name": "brief_ordner",
"value": "briefe",
"type": "string"
},
{
- "id": "c8a33c09-b371-4fa6-adb8-1c4fb831739b",
+ "id": "d19f00fe-1f58-4481-a4a9-ff0c37a1575b",
"name": "poll_max",
"value": 30,
"type": "number"
},
{
- "id": "a2d67260-b199-4a4c-a2c2-5f32d93a80ff",
+ "id": "a586e75e-cf55-4058-bde0-91ae53405566",
"name": "poll_sekunden",
"value": 20,
"type": "number"
},
{
- "id": "5bddb3d2-44c6-4a5a-987e-697a303d8ba7",
+ "id": "bb43c442-b040-473e-a77b-d6b785785563",
"name": "pause_zwischen_artikeln",
"value": 5,
"type": "number"
},
{
- "id": "bd4fd63e-5a72-4892-a2e0-481ed234ba11",
+ "id": "ad45dd10-9b09-4113-bd0e-1e2625bd4c2f",
"name": "benachrichtigung_an",
"value": "privat@lucas-orth.de",
"type": "string"
@@ -153,7 +153,7 @@
-240,
200
],
- "id": "4e6479bc-7e41-432e-b38b-abd3905ded3a",
+ "id": "8d2693c2-31b1-421a-83aa-9c04f898105b",
"name": "Konfiguration"
},
{
@@ -177,7 +177,7 @@
-20,
200
],
- "id": "748c30f8-635c-4f7a-9a7c-e2045e8ce423",
+ "id": "c00ff479-1649-419a-b084-07dc1468838f",
"name": "Artikeltabelle lesen",
"credentials": {
"httpHeaderAuth": {
@@ -201,7 +201,7 @@
-20,
420
],
- "id": "5a72faa0-c8df-4a53-8981-aab5e7e1cf7e",
+ "id": "1463b817-36ba-4442-97c1-552752bfde17",
"name": "Fehler Artikeltabelle"
},
{
@@ -215,7 +215,7 @@
200,
200
],
- "id": "30c7c0d8-e179-41b1-9291-6ca1e158f232",
+ "id": "27ac6860-1b4d-4aef-b96c-02638881474d",
"name": "Tabelle auswerten"
},
{
@@ -229,7 +229,7 @@
},
"conditions": [
{
- "id": "b8fe7382-0842-4295-b498-b0a9c7021226",
+ "id": "5d2351d2-9aeb-48b9-b505-34bf9399d7ab",
"leftValue": "={{ $json.leer === true }}",
"rightValue": "",
"operator": {
@@ -250,7 +250,7 @@
420,
200
],
- "id": "42fa04a3-7b12-4a2e-b646-f22316415ac6",
+ "id": "a73f8011-23c7-4e03-8544-28d06f66c463",
"name": "Offene Artikel vorhanden?"
},
{
@@ -261,7 +261,7 @@
640,
60
],
- "id": "baa73a73-d519-4722-8a6f-22e17f37dcbb",
+ "id": "157f932d-c655-4374-b6df-0618b9591f63",
"name": "Nichts zu tun"
},
{
@@ -277,7 +277,7 @@
640,
320
],
- "id": "976aa62b-ce93-4677-88a4-48584933aad7",
+ "id": "86fafde7-9cdb-48f0-a003-931e12db12ab",
"name": "Artikel nacheinander"
},
{
@@ -307,7 +307,7 @@
900,
420
],
- "id": "d0b0e12c-2e55-4e8b-abb9-678716695c6c",
+ "id": "a697e29c-a103-4b9d-8eae-599a66ef29cb",
"name": "Analyse starten",
"credentials": {
"httpHeaderAuth": {
@@ -331,7 +331,7 @@
900,
640
],
- "id": "4d94bedf-6bbe-4f90-897d-2302a68c2abf",
+ "id": "4df6b64a-d30d-4723-aac7-f91a011b9c20",
"name": "Fehler Analyse starten",
"onError": "continueRegularOutput"
},
@@ -346,7 +346,7 @@
1120,
420
],
- "id": "47460438-b146-4705-bf0a-3719ca66af05",
+ "id": "8cca16df-9dcd-4597-841f-97a582a51b95",
"name": "Poll vorbereiten"
},
{
@@ -360,9 +360,9 @@
1340,
420
],
- "id": "4ec0f013-a163-46da-b7b1-02e4b73dd9dd",
+ "id": "f9574347-0e39-418c-b944-d09862bb80c9",
"name": "Warten",
- "webhookId": "ac9b591c-58ac-40b4-bd94-716826479d88"
+ "webhookId": "b6229a99-62d1-42e0-8478-cdecca73f05c"
},
{
"parameters": {
@@ -391,7 +391,7 @@
1560,
420
],
- "id": "aaa1c12a-7d47-41ef-903d-109f847ff356",
+ "id": "42891afa-983e-4634-a59d-2beb65e57b7a",
"name": "Analyse abfragen",
"credentials": {
"httpHeaderAuth": {
@@ -415,7 +415,7 @@
1560,
640
],
- "id": "23a1f2d5-49a3-4f69-85f3-456c0dcccaba",
+ "id": "db41d8b6-9542-4201-b5cd-c8374bc40e6c",
"name": "Fehler Analyse abfragen",
"onError": "continueRegularOutput"
},
@@ -430,7 +430,7 @@
},
"conditions": [
{
- "id": "4bb7114a-807a-4487-a612-0835fb9260ca",
+ "id": "382e7432-7a07-4a83-b6c2-41aecc147b3c",
"leftValue": "={{ $json.status }}",
"rightValue": "ready",
"operator": {
@@ -450,13 +450,13 @@
1780,
420
],
- "id": "0d337016-36a1-492a-8b29-5e1d290be140",
+ "id": "01c6c412-0c34-40bd-a3a5-70437fa97662",
"name": "Analyse fertig?"
},
{
"parameters": {
"mode": "runOnceForAllItems",
- "jsCode": "\nconst poll = $('Poll vorbereiten').first().json;\nconst antwort = $input.first().json;\nconst versuch = (poll.versuch || 0) + $runIndex + 1;\nreturn [{ json: { ...poll, versuch, letzter_status: antwort.status || 'unbekannt' } }];\n"
+ "jsCode": "\n// Zaehler je Query in den Workflow-Daten. $runIndex zaehlt ueber alle Artikel\n// eines Laufs hinweg und taugt deshalb nicht als Zaehler pro Artikel.\nconst poll = $('Poll vorbereiten').first().json;\nconst antwort = $input.first().json;\nconst sd = $getWorkflowStaticData('global');\nsd.poll = sd.poll || {};\nsd.poll[poll.query] = (sd.poll[poll.query] || 0) + 1;\nconst versuch = sd.poll[poll.query];\nreturn [{ json: { ...poll, versuch, letzter_status: antwort.status || 'unbekannt' } }];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
@@ -464,7 +464,7 @@
1780,
660
],
- "id": "a22a0c31-e340-435d-8663-0ab865ebd394",
+ "id": "80e0be69-80e5-4127-a514-3bd7d0b6c047",
"name": "Versuch zaehlen"
},
{
@@ -478,7 +478,7 @@
},
"conditions": [
{
- "id": "1e0b93da-c082-406f-8642-56ff4a421c07",
+ "id": "8192b3f4-6ecb-4546-8403-d7152be847d7",
"leftValue": "={{ $json.versuch }}",
"rightValue": "={{ $('Konfiguration').first().json.poll_max }}",
"operator": {
@@ -498,13 +498,13 @@
2000,
660
],
- "id": "5603ea35-5166-4234-be4b-7791bcba1d95",
+ "id": "d68208d2-b9c1-40ae-868e-c6160f3361c0",
"name": "Poll-Limit erreicht?"
},
{
"parameters": {
"mode": "runOnceForAllItems",
- "jsCode": "\nconst a = $input.first().json;\nreturn [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',\n stufe: 'poll-timeout',\n meldung: 'NeuronWriter-Analyse fuer \"' + a.keyword + '\" war nach ' + a.versuch\n + ' Versuchen noch nicht fertig (letzter Status: ' + a.letzter_status\n + '). Analyse laeuft ggf. weiter, query merken: ' + a.query }}];\n"
+ "jsCode": "\nconst a = $input.first().json;\n{ const sd = $getWorkflowStaticData('global'); if (sd.poll) delete sd.poll[a.query]; }\nreturn [{ json: { slug: a.slug, keyword: a.keyword, query: a.query, ergebnis: 'fehler',\n stufe: 'poll-timeout',\n meldung: 'NeuronWriter-Analyse fuer \"' + a.keyword + '\" war nach ' + a.versuch\n + ' Versuchen noch nicht fertig (letzter Status: ' + a.letzter_status\n + '). Analyse laeuft ggf. weiter, query merken: ' + a.query }}];\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
@@ -512,13 +512,13 @@
2220,
780
],
- "id": "96ab606e-48a9-400e-96e7-857b042acffb",
+ "id": "6c92837b-f313-44ab-871c-e5a930a13906",
"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, 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"
+ "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{ const sd = $getWorkflowStaticData('global'); if (sd.poll) delete sd.poll[a.query]; }\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 querverweise: 'Mindestens zwei, hoechstens vier Querverweise auf verwandte Ratgeberartikel, immer als '\n + '[ratgeber_link slug=\"SLUG\"]Ankertext[/ratgeber_link], nie als harter Link. Slugs aus artikel-index.json. '\n + 'Der Shortcode verlinkt erst, wenn der Zielartikel veroeffentlicht ist. Ankertext beschreibt das Ziel, '\n + 'nie \"hier\" oder \"mehr dazu\".',\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": "19c85690-97f0-4eb7-b1e0-a83348ceefe1",
+ "id": "fdb0ddb4-5218-4598-9b74-dcc2f84499f1",
"name": "Brief bauen"
},
{
@@ -551,7 +551,7 @@
2440,
300
],
- "id": "c43d02ff-1236-45fc-bcb3-5098accee588",
+ "id": "b0bafe2a-60ab-47fb-a006-6115edaf4256",
"name": "Brief vorhanden?",
"credentials": {
"httpHeaderAuth": {
@@ -571,7 +571,7 @@
2660,
300
],
- "id": "8d222315-472d-44f2-953b-494074b5d482",
+ "id": "4a67bbcf-ce4f-4b76-b4f4-4ba6c8236b10",
"name": "Commit vorbereiten"
},
{
@@ -601,7 +601,7 @@
2880,
300
],
- "id": "1a784408-d09d-420b-a6d3-7f808bbd806d",
+ "id": "a60a0aac-449d-4990-872d-c2a848ebcd08",
"name": "Brief committen",
"credentials": {
"httpHeaderAuth": {
@@ -625,7 +625,7 @@
2880,
520
],
- "id": "47ec95be-d567-4685-8ea7-aa447a91103d",
+ "id": "1dda0054-24af-465d-bd47-8f5693968c80",
"name": "Fehler Brief committen",
"onError": "continueRegularOutput"
},
@@ -640,7 +640,7 @@
3100,
300
],
- "id": "07a1e471-d6bb-4327-a812-266bca634263",
+ "id": "e8741dc7-728d-4409-97f4-1fa91f219b2c",
"name": "Erfolg merken"
},
{
@@ -654,9 +654,9 @@
3320,
400
],
- "id": "9e4fc985-8c3d-4736-9585-1ecaf29a4b01",
+ "id": "cea1c3e2-f571-4c9f-8753-ab3c10d9a348",
"name": "Pause Rate Limit",
- "webhookId": "380a5713-2767-4992-aa4a-053605517480"
+ "webhookId": "ef9b2ac6-fb6a-4727-99f9-173c3c8af25f"
},
{
"parameters": {
@@ -669,7 +669,7 @@
900,
140
],
- "id": "d80eaa2b-a228-4f93-a608-c4bc48804cea",
+ "id": "40489909-6d34-488c-acff-c22d3cce020d",
"name": "Ergebnisse sammeln"
},
{
@@ -683,7 +683,7 @@
1120,
140
],
- "id": "f52ac21c-d921-4b65-b0ca-0232f6c212e6",
+ "id": "b2acfa6d-d78a-4d92-937e-af3c5ce44d10",
"name": "Statusupdate vorbereiten"
},
{
@@ -713,7 +713,7 @@
1340,
140
],
- "id": "ace29136-8155-46df-93d4-4ebba0b909eb",
+ "id": "fcb0a473-d7c5-477b-aecb-4a60341b3c5e",
"name": "Status zurueckschreiben",
"credentials": {
"httpHeaderAuth": {
@@ -737,7 +737,7 @@
1340,
-60
],
- "id": "8cd217a6-ad58-44dd-bef9-7183c6a6ccad",
+ "id": "030a3ede-19b7-4b85-a582-f17d5ccfd316",
"name": "Fehler Statusupdate"
},
{
@@ -751,7 +751,7 @@
1560,
140
],
- "id": "b331d639-2de7-4329-8b11-350d227a59a5",
+ "id": "ba124436-2af4-4873-b5b3-7e206c34a598",
"name": "Meldung formulieren"
},
{
@@ -769,7 +769,7 @@
1780,
140
],
- "id": "8a6f1d29-5114-4e70-b539-18d50475ec94",
+ "id": "99f630b2-451c-4db8-b10b-47e58dd5b69d",
"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 1452985..d89f370 100644
--- a/workflows/pruefen-und-importieren.json
+++ b/workflows/pruefen-und-importieren.json
@@ -9,7 +9,7 @@
-460,
100
],
- "id": "5538d2bd-2e94-4f42-a389-9384ef625de8",
+ "id": "77876018-7154-4c9d-b1dd-5dd786cf8aae",
"name": "Manuell starten"
},
{
@@ -25,9 +25,9 @@
-460,
280
],
- "id": "3e3c342f-b4ee-4565-b1d3-a9d845707842",
+ "id": "57a30ac3-70e9-4cd3-b898-2f2b4ac669ac",
"name": "Webhook Start",
- "webhookId": "9c08dcef-5acc-462b-bca4-7340ee75ed8e"
+ "webhookId": "7a157b66-1824-459c-9dfb-0b52145d33a5"
},
{
"parameters": {
@@ -35,115 +35,115 @@
"assignments": {
"assignments": [
{
- "id": "390bf7df-f505-4ef0-b187-16f28a42e8dc",
+ "id": "30e392d0-59d5-4ede-bd7a-2e007e477c0a",
"name": "nw_base",
"value": "https://app.neuronwriter.com/neuron-api/0.5/writer",
"type": "string"
},
{
- "id": "526b22c9-127b-4df5-8ffc-eeffd4fdaafb",
+ "id": "68bbc028-b18a-49ad-9032-5c0b0b3de53a",
"name": "gitea_base",
"value": "http://gitea:3000",
"type": "string"
},
{
- "id": "dc41acbd-71a6-4d54-901e-89b1b3c42cc8",
+ "id": "61a631f8-1840-45ab-ad22-3090d5de8e84",
"name": "gitea_owner",
"value": "lucas.orth",
"type": "string"
},
{
- "id": "0ee5976c-0df1-4267-9f74-de284d60f74c",
+ "id": "0efb15dc-4dbf-4e51-b7d6-7e0fd1535ee6",
"name": "gitea_repo",
"value": "Skrift-Auto-Article",
"type": "string"
},
{
- "id": "475ceb04-ce9c-4471-94a2-780d10ada7c9",
+ "id": "6f442b85-68aa-4723-8f20-0c4804863d94",
"name": "branch",
"value": "main",
"type": "string"
},
{
- "id": "88eebf2d-27d8-4127-a189-7e348bbf999e",
+ "id": "f26f278d-f9ce-4b2e-a9b4-17a528823163",
"name": "artikel_ordner",
"value": "artikel",
"type": "string"
},
{
- "id": "6eb13140-4748-4344-a731-2d808e8986d7",
+ "id": "0a60485f-7117-4736-a8e9-a180d5f66ea3",
"name": "brief_ordner",
"value": "briefe",
"type": "string"
},
{
- "id": "783fe66a-75a8-4d8e-b2f0-2aebf80e087e",
+ "id": "fb4029bc-aaa5-4a2b-ac39-74e0a6b18ef6",
"name": "nachbessern_ordner",
"value": "nachbessern",
"type": "string"
},
{
- "id": "2ed923be-06ad-48c4-b927-041f6e2f2552",
+ "id": "e255b391-5f12-4bde-84b4-c508bab8dac5",
"name": "status_pfad",
"value": "status-importe.json",
"type": "string"
},
{
- "id": "a56e81d3-a9c3-46e9-8f34-ecf8b002ac9c",
+ "id": "5785c32f-ef1c-45c3-bde1-61eccb439105",
"name": "wp_base",
"value": "https://skrift.de",
"type": "string"
},
{
- "id": "cfd8b1d7-6ee5-4a4a-85a2-724ea800968e",
+ "id": "b973a4ea-3b85-4c2f-abbd-8cf6d26025bc",
"name": "score_ziel",
"value": "={{ $json.body && $json.body.score_ziel ? Number($json.body.score_ziel) : 65 }}",
"type": "number"
},
{
- "id": "2e91b780-c50a-4413-b3a7-b03b1810bd31",
+ "id": "ff314c6a-5748-4de2-8e2a-7c05ba864085",
"name": "pause_zwischen_artikeln",
"value": 5,
"type": "number"
},
{
- "id": "46cd6c5e-3ea8-4f8d-acd8-8cfc737c46dd",
+ "id": "66d24a08-85e1-4f85-b365-e34283f3b3a8",
"name": "elementor_ausgabe",
"value": false,
"type": "boolean"
},
{
- "id": "e469e1f3-6366-4a91-ba25-5df62337228c",
+ "id": "afc4788f-4558-489f-8b3c-7df902791200",
"name": "bild_aktiv",
"value": true,
"type": "boolean"
},
{
- "id": "6e75aa5e-71cf-432c-811e-0901c721268e",
+ "id": "fde4d84a-6f31-40a4-88c3-863e796aa45d",
"name": "bild_modell",
"value": "gpt-image-2",
"type": "string"
},
{
- "id": "d5ffc52c-befa-451d-a82c-62a90bb95d6a",
+ "id": "ef031954-d296-4068-872a-6fbe54e1ccc3",
"name": "bild_groesse",
"value": "1200x1200",
"type": "string"
},
{
- "id": "480645e7-a7e3-49b8-9e97-add0f25482b9",
+ "id": "94990856-3ac7-47df-9636-b51dfe6d5e1e",
"name": "bild_qualitaet",
"value": "medium",
"type": "string"
},
{
- "id": "06d23bd2-7a0a-41ac-b70b-9cc1dac08b87",
+ "id": "d51af1a1-fc5a-4a60-ac9a-9ce2df113dd1",
"name": "faq_ueberschrift",
"value": "Häufige Fragen",
"type": "string"
},
{
- "id": "dbf57836-aa98-4f62-ad93-75b6978ed979",
+ "id": "b8366f1f-fe40-4ca7-b052-1bee24d543be",
"name": "benachrichtigung_an",
"value": "privat@lucas-orth.de",
"type": "string"
@@ -158,7 +158,7 @@
-240,
190
],
- "id": "8354444e-f8b0-4895-b9ef-78895a058245",
+ "id": "01d2991f-f38d-485d-8871-af8267952501",
"name": "Konfiguration"
},
{
@@ -183,7 +183,7 @@
-20,
190
],
- "id": "8a150411-fc15-4549-9cac-9fd750998ba7",
+ "id": "b9f2b463-32ec-4250-bfbc-ad262f3901d3",
"name": "Artikelordner lesen",
"credentials": {
"httpHeaderAuth": {
@@ -206,7 +206,7 @@
200,
190
],
- "id": "e91525e5-1fe9-468e-9bc9-7c865542ac81",
+ "id": "c89a4237-836d-44c3-8572-2bf939a873af",
"name": "Artikeldateien filtern"
},
{
@@ -220,7 +220,7 @@
},
"conditions": [
{
- "id": "3d273a9f-f283-4790-aacb-f1d615f08728",
+ "id": "c7bbd13f-b4c6-48f9-8b79-4efcb95ab7a2",
"leftValue": "={{ $json.leer === true }}",
"rightValue": "",
"operator": {
@@ -241,7 +241,7 @@
420,
190
],
- "id": "1c86b8dc-2590-47ea-aeda-3cc69e374f4c",
+ "id": "946944dc-a368-46f7-9af0-dd93cf2972a1",
"name": "Artikel vorhanden?"
},
{
@@ -252,7 +252,7 @@
640,
40
],
- "id": "abe7df94-da3e-409d-ac97-2aab7a446635",
+ "id": "6171f836-f34c-4528-b8df-2fe872055632",
"name": "Nichts zu tun"
},
{
@@ -268,7 +268,7 @@
640,
320
],
- "id": "527a5591-0e90-4495-afa5-df2a2666adc4",
+ "id": "9c1acd2c-c3ec-4dc9-8359-011552d42f61",
"name": "Artikel nacheinander"
},
{
@@ -292,7 +292,7 @@
900,
420
],
- "id": "204918b5-23e4-4e74-bce5-322c96e1675c",
+ "id": "baf3e0c2-38c8-45c4-834e-db4d09a570a9",
"name": "Artikeldatei laden",
"credentials": {
"httpHeaderAuth": {
@@ -316,7 +316,7 @@
900,
640
],
- "id": "722cca8e-1303-41ff-ab50-27d18fe873df",
+ "id": "c0751d4b-193c-4942-86c3-f588b7cca5c6",
"name": "Fehler Artikeldatei",
"onError": "continueRegularOutput"
},
@@ -342,7 +342,7 @@
1120,
420
],
- "id": "5d42b7af-3125-4445-a579-665b0ea2a1c8",
+ "id": "998ea097-3d46-4bd5-9afb-9727c58b119d",
"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 // 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"
+ "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 // Querverweise auf andere Ratgeberartikel\n const quer = [...html.matchAll(/\\[ratgeber_link\\s+slug=\"([^\"]+)\"\\]([\\s\\S]*?)\\[\\/ratgeber_link\\]/g)];\n if (quer.length < 2) add('Querverweise: zu wenige', `${quer.length} [ratgeber_link], mindestens 2 auf verwandte Ratgeberartikel`);\n quer.filter(q => q[1] === artikel.slug).forEach(() => add('Querverweis auf sich selbst', artikel.slug));\n quer.filter(q => !q[2].trim()).forEach(q => add('Querverweis ohne Ankertext', q[1]));\n (html.match(/href=\"https?:\\/\\/(www\\.)?skrift\\.de\\/(?!handgeschriebene-|unterschriftenservice|automatisierte-follow-ups|papier-schriftmuster|kontakt|handschrift-fuer)[a-z0-9-]+\\/?\"/g) || [])\n .forEach(h => add('Ratgeber-Link als harter Link', `${h} als [ratgeber_link slug=\"...\"] setzen, sonst droht ein toter Link`));\n\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": "0004630a-561d-47e0-8482-fad45ae3f1c3",
+ "id": "abfc66c7-c1ef-4640-9e5e-1f7268173605",
"name": "Artikel pruefen"
},
{
@@ -392,7 +392,7 @@
1560,
420
],
- "id": "1b15da2c-b93d-4fbd-b803-f104f8f88b94",
+ "id": "9158e17f-118f-4e28-b4bd-27c2f38ab958",
"name": "Score ermitteln",
"credentials": {
"httpHeaderAuth": {
@@ -416,7 +416,7 @@
1560,
640
],
- "id": "422c4168-3898-4d52-bec7-d02ea6bc03e0",
+ "id": "e5155865-6106-494e-b238-de63ec2fcf23",
"name": "Fehler Score",
"onError": "continueRegularOutput"
},
@@ -431,7 +431,7 @@
1780,
420
],
- "id": "af056083-199f-4412-8eb4-1711eeb5cc14",
+ "id": "c670cfc5-5c64-46c4-b9d9-da2f9f7d998a",
"name": "Score auswerten"
},
{
@@ -445,7 +445,7 @@
},
"conditions": [
{
- "id": "8512f8e1-81a8-473d-8690-0bb0167d0aee",
+ "id": "da564c6f-a42b-4cf5-82bf-da3d41877da7",
"leftValue": "={{ $json.bestanden }}",
"rightValue": "",
"operator": {
@@ -466,7 +466,7 @@
2000,
420
],
- "id": "22d68dd7-afad-42c9-8d5a-b6ba3326c3ab",
+ "id": "3dfb6f30-0274-49c2-84c0-a010d862298c",
"name": "Score erreicht?"
},
{
@@ -480,7 +480,7 @@
2220,
640
],
- "id": "6ba8665b-d17e-4eb5-88ef-f2fec20bf53b",
+ "id": "bac5edd8-d56a-4533-b56c-f3b24bab3263",
"name": "Fehlende Begriffe ermitteln"
},
{
@@ -505,7 +505,7 @@
2440,
640
],
- "id": "45709753-0ee1-47eb-a43a-f74bac69b8a3",
+ "id": "3d514218-62c0-439e-8594-7ccc5ebebdae",
"name": "Nachbesserung vorhanden?",
"credentials": {
"httpHeaderAuth": {
@@ -525,7 +525,7 @@
2660,
640
],
- "id": "f8b6d020-9d26-4011-889b-7c4baf01a5a7",
+ "id": "a6b0f5d9-361d-4b09-bba3-e169b33b85ca",
"name": "Nachbesserung vorbereiten"
},
{
@@ -555,7 +555,7 @@
2880,
640
],
- "id": "e2a6ab12-9d70-4721-bfe7-33908c3907db",
+ "id": "4a9a76dc-68fa-4c8e-93a6-6c01f1ede8ce",
"name": "Nachbesserung committen",
"credentials": {
"httpHeaderAuth": {
@@ -579,7 +579,7 @@
2880,
860
],
- "id": "c15887c3-f8d6-4d1a-a384-758336cf1611",
+ "id": "b675c23e-1826-4d4c-ace8-a339efef67bf",
"name": "Fehler Nachbesserung",
"onError": "continueRegularOutput"
},
@@ -594,7 +594,7 @@
3100,
640
],
- "id": "39611df3-08b6-4714-8913-d6d4a182691c",
+ "id": "48d2bbc1-5cc0-496e-80ba-d002ee7ea687",
"name": "Nachbesserung gemerkt"
},
{
@@ -624,7 +624,7 @@
2220,
240
],
- "id": "fb72faca-a385-410f-9ae9-bea8497cb8ee",
+ "id": "b84af3b2-4638-4713-81c1-c8d8747c0363",
"name": "Revision zu NeuronWriter",
"credentials": {
"httpHeaderAuth": {
@@ -648,7 +648,7 @@
2220,
60
],
- "id": "5200499c-316d-4abf-8fa5-68b8855a8213",
+ "id": "c881c38e-ecdc-4a9c-a25f-e05e40c3e5ef",
"name": "Fehler Revision",
"onError": "continueRegularOutput"
},
@@ -674,7 +674,7 @@
2440,
240
],
- "id": "d150e8c2-5e08-453e-91c1-88d20142085d",
+ "id": "3fd00ae7-8bf9-4027-9119-bb8ba00d6e11",
"name": "Beitrag vorhanden?",
"credentials": {
"httpBasicAuth": {
@@ -708,7 +708,7 @@
2660,
240
],
- "id": "703611e3-5820-4f90-86cb-0d15950cec22",
+ "id": "78586dc9-923d-49be-b5df-11f27a0cfbea",
"name": "Kategorie suchen",
"credentials": {
"httpBasicAuth": {
@@ -731,7 +731,7 @@
2880,
240
],
- "id": "b4cd58dc-ab41-44d6-934c-9f40d5064653",
+ "id": "e198abd7-2f0b-49f5-adc5-3c4a9ee3ab3a",
"name": "Kategorie pruefen"
},
{
@@ -745,7 +745,7 @@
},
"conditions": [
{
- "id": "1418e966-d8bc-4344-a20e-947e9a74a471",
+ "id": "b0e30459-6b54-4b74-8470-9520c5f3a9bb",
"leftValue": "={{ $json.kategorie_fehlt }}",
"rightValue": "",
"operator": {
@@ -766,7 +766,7 @@
3100,
240
],
- "id": "a37d9e34-17a8-4f1c-83c7-4364c2a6d7a3",
+ "id": "bcbe3d18-9ecc-497e-886b-32cdab8263e7",
"name": "Kategorie anlegen?"
},
{
@@ -796,7 +796,7 @@
3320,
140
],
- "id": "5a1285a8-cf43-417d-afb5-4d4b7f2bc215",
+ "id": "5a6a0165-5d7b-42f6-9818-2b76ebc1376b",
"name": "Kategorie anlegen",
"credentials": {
"httpBasicAuth": {
@@ -819,7 +819,7 @@
3540,
240
],
- "id": "5f58cee4-82cd-49d8-9796-6b0e4cefde27",
+ "id": "c0e0e90e-e0d2-4a9d-8f9e-74fabf3a0561",
"name": "Kategorie festlegen"
},
{
@@ -833,7 +833,7 @@
3760,
240
],
- "id": "c5325756-6ff4-4d95-8bda-2371858927ca",
+ "id": "5712410d-e1ce-4147-a575-df8147fcdf86",
"name": "WordPress-Body bauen"
},
{
@@ -863,7 +863,7 @@
3980,
240
],
- "id": "3a509a0b-0e18-4d66-85be-e11f98c1b6f3",
+ "id": "7d1c919c-bb1e-4aff-b10b-ba9703dd66d7",
"name": "Beitrag anlegen oder aktualisieren",
"credentials": {
"httpBasicAuth": {
@@ -886,7 +886,7 @@
4200,
240
],
- "id": "5b75c979-7d05-469f-a7dd-4c3f39b0cbbf",
+ "id": "553a5836-dd1b-44c5-9146-453e2fbf6bb6",
"name": "Ergebnis pruefen"
},
{
@@ -917,7 +917,7 @@
4420,
240
],
- "id": "d94b6d9c-412b-4343-8a5c-934b29c52961",
+ "id": "ec545d25-2c80-4b3b-9b7b-dd30b61990d8",
"name": "Cache leeren",
"credentials": {
"httpBasicAuth": {
@@ -937,7 +937,7 @@
4640,
240
],
- "id": "73409752-8021-4e54-a47d-e4eadccbebfe",
+ "id": "db87877c-313a-434a-a1ee-fe0f0644ad86",
"name": "Importergebnis merken"
},
{
@@ -951,9 +951,9 @@
4860,
420
],
- "id": "de26ea0e-032e-4369-ad92-33937423dbc8",
+ "id": "06918dbc-a5cc-4598-ab4f-12963601622a",
"name": "Pause Rate Limit",
- "webhookId": "a21f142d-d8a7-4bd7-8b9b-1b58d7fe08ed"
+ "webhookId": "5be31916-7738-453d-b4a8-c3f58380d328"
},
{
"parameters": {
@@ -966,7 +966,7 @@
},
"conditions": [
{
- "id": "0f13a811-93b2-41b0-b008-bc49fe907c19",
+ "id": "38a37fb2-cd95-46ec-bdde-60a18b572c1c",
"leftValue": "={{ $('Konfiguration').first().json.bild_aktiv === true && !Number($json.featured_media) }}",
"rightValue": "",
"operator": {
@@ -987,7 +987,7 @@
4860,
240
],
- "id": "9b6f2199-87ea-4926-bf8f-7b9dd632260b",
+ "id": "04b068e9-ed31-4dcc-8970-04daebd61b7f",
"name": "Beitragsbild fehlt?"
},
{
@@ -1012,7 +1012,7 @@
5080,
120
],
- "id": "a885b2bf-7d43-4ff8-9c55-a10015f88181",
+ "id": "e11b4f49-0c55-4392-861c-d66a9762a623",
"name": "Bilddaten laden",
"credentials": {
"httpHeaderAuth": {
@@ -1035,7 +1035,7 @@
5300,
120
],
- "id": "26883a08-bb99-4bf7-94fa-1772db788c43",
+ "id": "ebe17d0b-3139-4aca-85cd-b5345c550c1b",
"name": "Bildprompt bauen"
},
{
@@ -1049,7 +1049,7 @@
},
"conditions": [
{
- "id": "5d1dd07d-ccc5-4af5-971f-505cdd5ef0cf",
+ "id": "6d7b129e-23c1-40b4-949d-0b8b628a4984",
"leftValue": "={{ !!$json.bild_prompt }}",
"rightValue": "",
"operator": {
@@ -1070,7 +1070,7 @@
5520,
120
],
- "id": "1d827bd8-2432-40e8-903d-79e81c77960a",
+ "id": "583dad0b-2fec-4bfb-8c50-32edd965c782",
"name": "Bildprompt vorhanden?"
},
{
@@ -1100,7 +1100,7 @@
5740,
40
],
- "id": "6e9078dd-7f68-4491-b5a9-11a25bad2ed0",
+ "id": "3d2143cc-e937-4dca-bff4-1705102f41a2",
"name": "Bild erzeugen",
"credentials": {
"httpHeaderAuth": {
@@ -1124,7 +1124,7 @@
5740,
260
],
- "id": "b22e5786-0100-4ba7-bb28-86ef4fa918f9",
+ "id": "bff3ef3d-9be6-4b71-ad55-7de2a9244440",
"name": "Fehler Bild",
"onError": "continueRegularOutput"
},
@@ -1139,7 +1139,7 @@
5960,
40
],
- "id": "c33551a0-9962-4fba-b94f-0d7540ba95a3",
+ "id": "d44f9381-0b68-4872-bd92-cb14cab24dfb",
"name": "Bild als Datei"
},
{
@@ -1174,7 +1174,7 @@
6180,
40
],
- "id": "f809110f-6dee-4d1c-802c-50dfb0ec72c3",
+ "id": "47c9c7e9-1f65-4434-b4b0-05ce2577a98b",
"name": "Bild in Mediathek",
"credentials": {
"httpBasicAuth": {
@@ -1208,7 +1208,7 @@
6400,
40
],
- "id": "a0ef7f44-c9ca-4edb-9f8b-79a634688c4d",
+ "id": "85b0be75-4b66-4af7-83a0-a00d5b9fcbd9",
"name": "Beitragsbild setzen",
"credentials": {
"httpBasicAuth": {
@@ -1231,7 +1231,7 @@
6620,
40
],
- "id": "9d7774ee-0cd1-4838-8794-c9d295f8cca1",
+ "id": "0de4b676-fd3c-4438-aeb5-d279effc2bc8",
"name": "Bild gemerkt"
},
{
@@ -1245,7 +1245,7 @@
900,
80
],
- "id": "519e24e7-6394-4448-8fbc-8b9abf2e2b5c",
+ "id": "6e3219cb-c2be-44d0-b2d0-6b182329ccc9",
"name": "Ergebnisse sammeln"
},
{
@@ -1270,7 +1270,7 @@
1120,
80
],
- "id": "db67b230-bf33-4d8b-839e-7500267ff088",
+ "id": "b368e64d-9aac-4cde-be95-4d061e76bd37",
"name": "Statustabelle lesen",
"credentials": {
"httpHeaderAuth": {
@@ -1290,7 +1290,7 @@
1340,
80
],
- "id": "d9a2ea1e-f353-4dc0-b0d3-5626f56287a0",
+ "id": "6fb9c69b-f398-4cc3-a1e5-633f91c24923",
"name": "Statustabelle fortschreiben"
},
{
@@ -1304,7 +1304,7 @@
},
"conditions": [
{
- "id": "280db96a-ce49-4a0f-88e2-30b28e5530af",
+ "id": "04e69285-96fe-4313-9a53-22c17b0a1923",
"leftValue": "={{ $json.ueberspringen }}",
"rightValue": "",
"operator": {
@@ -1325,7 +1325,7 @@
1560,
80
],
- "id": "379554bd-f08f-42b1-b5f0-8d4491081940",
+ "id": "5049ff31-926b-4c6c-a728-ba463fdd565b",
"name": "Gab es Importe?"
},
{
@@ -1355,7 +1355,7 @@
1780,
160
],
- "id": "d0aa7ca7-73d2-4373-8688-56b31c759e8a",
+ "id": "73f50d9b-9334-4f6c-a702-ced32babc406",
"name": "Statustabelle schreiben",
"credentials": {
"httpHeaderAuth": {
@@ -1379,7 +1379,7 @@
1780,
340
],
- "id": "2381caea-8d16-47d5-b7b6-512feab7e685",
+ "id": "652ccbd8-38df-40b6-8cf9-43bd9ff9ba7c",
"name": "Fehler Statustabelle",
"onError": "continueRegularOutput"
},
@@ -1394,7 +1394,7 @@
2000,
80
],
- "id": "fbd5c338-83f4-4503-8639-13288826772e",
+ "id": "80192868-178f-4eca-a313-073f06bf3ce3",
"name": "Meldung formulieren"
},
{
@@ -1412,7 +1412,7 @@
2220,
80
],
- "id": "c54274ec-b1af-458d-936d-c413d8acd1b7",
+ "id": "c81c485d-8ac3-4c5c-bf3e-e2934a349f32",
"name": "Benachrichtigung",
"disabled": true,
"notes": "SMTP-Credential zuweisen, Absender eintragen, dann aktivieren."