# Skrift Auto Article Zwei n8n-Workflows fuer die Ratgeber-Produktion von skrift.de. ``` artikel-index.json Redaktionsplan, eine Zeile je Artikel | v Workflow 1: brief-vorbereiten briefe/brief-.json NeuronWriter-Brief als Schreibvorlage | v Cowork schreibt den Artikel artikel/artikel-.json fertiger Artikel | v Workflow 2: pruefen-und-importieren WordPress-Entwurf + nachbessern/nachbessern-.json bei zu niedrigem Score ``` --- ## 1. Einmalige Einrichtung ### 1.1 Credentials in n8n Die Workflows referenzieren drei Credentials, die bereits im n8n-Credential-Store angelegt sind. In den Node-Parametern steht kein einziges Geheimnis. | Credential | Typ | Inhalt | |---|---|---| | `NeuronWriter API` | HTTP Header Auth | Name `X-API-KEY`, Wert = NeuronWriter-Key | | `Gitea Skrift Auto Article` | HTTP Header Auth | Name `Authorization`, Wert = `token ` | | `WP skrift.de Redakteur` | HTTP Basic Auth | Benutzer `ai-agent`, Passwort = Application Password | Wird ein Key ausgetauscht, nur das Credential in n8n bearbeiten. Die Workflow-Dateien bleiben unveraendert. **Gitea-Token:** braucht den Scope `write:repository`. Mit `read:repository` allein scheitert jeder Schreibvorgang mit HTTP 403 und der Meldung `token does not have at least one of required scope(s)`. **Gitea-Adresse:** beide Workflows sprechen Gitea als `http://gitea:3000` an, also ueber das Docker-Netz, nicht ueber `https://gitea.lucas-orth.de`. Grund: der oeffentliche Hostname loest auf die NetBird-Adresse `100.98.99.19` auf, die der n8n-Host nur auf Port 80 erreicht, nicht auf 443. Details in `docs/testlauf.md`. Der Docker-Weg ist ausserdem schneller und unabhaengig vom Reverse Proxy. Aendern laesst sich das im Node **Konfiguration**, Feld `gitea_base`. **WordPress-Benutzer:** eigener Benutzer mit Rolle Redakteur, kein Administrator. Die Rolle Redakteur darf technisch auch veroeffentlichen. Der Workflow setzt `status: draft` hart und bricht ab, falls WordPress etwas anderes zurueckmeldet. Wer das zusaetzlich absichern will, nimmt die Rolle Autor und entzieht per Code-Snippet `publish_posts`. Dann kann der Benutzer aber keine fremden Beitraege mehr aktualisieren, was die Idempotenz bricht. ### 1.2 Mu-Plugin auf skrift.de `wordpress/skrift-ratgeber-felder.php` nach `wp-content/mu-plugins/` kopieren. Es registriert vier Meta-Felder REST-schreibbar: | Feld | Typ | Inhalt | |---|---|---| | `kurzantwort` | string | Ein Absatz, der die Suchfrage sofort beantwortet | | `cta_ziel` | string (URL) | Zieladresse des Call-to-Action | | `stand_datum` | string | Format `YYYY-MM-DD` | | `faq` | array | `[{frage, antwort}]` | Ohne dieses Plugin nimmt die WordPress-REST-API die Felder stillschweigend nicht an. Der Workflow erkennt das und schreibt eine Warnung in die Benachrichtigung. Ausgabe der FAQ im Beitrag ueber den Shortcode `[skrift_faq titel="Haeufige Fragen"]`, zum Beispiel in einem Elementor-Shortcode-Widget. Die drei einfachen Felder lassen sich in Elementor Pro direkt als Dynamic Tag "Post Custom Field" einbinden. ### 1.3 SMTP fuer die Benachrichtigung Beide Workflows enden auf einem Node `Benachrichtigung`, der **deaktiviert** ist. Zum Aktivieren: SMTP-Credential zuweisen, Absenderadresse eintragen, Node aktivieren. Die Meldung wird vom Node davor fertig formuliert, am Text selbst muss nichts geaendert werden. --- ## 2. Workflow 1: brief-vorbereiten ### Start - Manuell in der n8n-Oberflaeche - Zeitplan Montag 07:00 (Node ist deaktiviert, bei Bedarf aktivieren) - Webhook: `POST https://n8n.lucas-orth.de/webhook/brief-vorbereiten` ### Ablauf 1. `artikel-index.json` ueber die Gitea-API lesen 2. Alle Zeilen mit `status: "offen"` herausfiltern, Pflichtfelder pruefen 3. Je Artikel `POST /new-query` an NeuronWriter 4. `POST /get-query` pollen, alle 20 Sekunden, maximal 30 Versuche 5. Antwort auf die benoetigten Felder reduzieren 6. `briefe/brief-.json` committen 7. Status auf `brief_bereit` setzen, `query_id` in die Zeile schreiben 8. Benachrichtigung ### Pflichtfelder in artikel-index.json Die Datei ist ein Array. Jede Zeile braucht: ```json { "slug": "handgeschriebene-werbebriefe", "titel": "Handgeschriebene Werbebriefe: Wann sie wirken und wann nicht", "keyword": "handgeschriebene werbebriefe", "kategorie": "Direktmarketing", "cta_ziel": "https://skrift.de/muster-anfordern", "status": "offen" } ``` Fehlt eines der Felder in einer Zeile mit `status: "offen"`, bricht der Workflow mit Angabe von Zeilennummer und fehlenden Feldern ab. Zeilen mit anderem Status werden nicht geprueft. Statuswerte: `offen` -> `brief_bereit` (durch Workflow 1) -> danach frei nutzbar. ### Was im Brief steht Die im Auftrag geforderten Felder liegen auf oberster Ebene: `query_id`, `keyword`, `begriffe_pflicht`, `begriffe_erweitert`, `fragen`, `wortzahl_ziel`. `begriffe_pflicht` und `begriffe_erweitert` enthalten je Begriff die von NeuronWriter empfohlene Spanne: ```json {"begriff": "handschriftlich", "min": 1, "max": 17, "bei_wettbewerbern_pc": 70} ``` `fragen` fasst vier NeuronWriter-Quellen zusammen, dedupliziert und nach Wichtigkeit sortiert. Fragen aus der `topic_matrix` tragen ein `gewicht` von 1 bis 10. Darunter steht `artikel` mit slug, titel, kategorie und cta_ziel aus der Tabelle, sowie ein Block `zusatz` mit Material, das beim Schreiben hilft: Ueberschriften-Begriffe, Entitaeten, Suchintention, Zielwortzahl-Median und die vollstaendigen Gliederungen der fuenf bestplatzierten Wettbewerber. --- ## 3. Workflow 2: pruefen-und-importieren ### Start - Manuell - Webhook: `POST https://n8n.lucas-orth.de/webhook/pruefen-und-importieren` ### Vertrag Cowork -> Workflow 2 Cowork legt je Artikel eine Datei `artikel/artikel-.json` an: ```json { "slug": "handgeschriebene-werbebriefe", "titel": "Handgeschriebene Werbebriefe: Wann sie wirken und wann nicht", "query_id": "565abcca58f65e52", "keyword": "handgeschriebene werbebriefe", "kategorie": "Direktmarketing", "cta_ziel": "https://skrift.de/muster-anfordern", "content_html": "

...

...

", "kurzantwort": "Ein Absatz, der die Hauptfrage direkt beantwortet.", "faq_ueberschrift": "Fragen zu den Kosten", "faq": [{"frage": "...", "antwort": "..."}], "seo_titel": "max. 60 Zeichen", "seo_beschreibung": "max. 155 Zeichen", "stand_datum": "2026-09-20" } ``` Pflicht sind `slug`, `titel`, `query_id`, `kategorie`, `content_html`. Der Slug im Dateinamen muss mit dem Slug im JSON uebereinstimmen, sonst bricht der Workflow fuer diesen Artikel ab. `query_id` stammt aus dem Brief, Workflow 1 schreibt sie zusaetzlich in `artikel-index.json`. Fehlt `stand_datum`, setzt der Workflow das heutige Datum. Fehlt `seo_titel`, wird `titel` verwendet. ### Ausgabeform in WordPress Der Artikel landet als normaler WordPress-Beitragsinhalt, nicht als Elementor-Layout. Kurzantwort, FAQ, CTA-Ziel und Stand liegen als Meta-Felder am Beitrag. Das Mu-Plugin `skrift-ratgeber-felder.php` (ab Version 1.1) bringt dafuer mit: - **Eingabebox "Ratgeber: Kurzantwort, CTA und FAQ"** unter dem Editor. Dort lassen sich alle vier Felder von Hand pflegen, das FAQ als Liste mit "Frage hinzufuegen". Speichern aus n8n per REST beruehrt die Box nicht. - **Elementor-Widget "Skrift FAQ"**. Einmal ins Single-Post-Template im Theme Builder setzen, dann zeigt jeder Beitrag automatisch seine eigenen Fragen als Umschalter. Gestaltung (Typografie, Farben, Rahmen, Abstaende) im Widget, FAQPage-Schema per Schalter. Beitraege ohne FAQ zeigen nichts an. - **Elementor-Widget "Skrift Kurzantwort"** fuer die Kurzantwort-Box, loest Preis-Shortcodes im Text auf. - **Dynamic Tags, Gruppe "Skrift"** (ab Plugin 1.3), nutzbar in jedem Text- oder Ueberschrift-Widget ueber das Symbol fuer dynamische Inhalte: - *Lesezeit*: aus Beitragsinhalt, Kurzantwort und FAQ, 200 Woerter pro Minute (einstellbar), Ausgabe z. B. "6 Min." - *FAQ-Ueberschrift*: Feld `faq_ueberschrift`, sonst der Standardtext aus dem Tag. Hat der Beitrag kein FAQ, bleibt die Ausgabe leer. - *Stand-Datum*: Feld `stand_datum` im Format `d.m.Y`, einstellbar - Shortcodes `[skrift_faq titel="..."]` und `[skrift_kurzantwort]` als Alternative, etwa im Elementor-Shortcode-Widget. - `cta_ziel` und `stand_datum` gibt Elementor Pro direkt ueber das Dynamic Tag "Beitrags-Custom-Feld" aus. Das native Elementor-Widget "Akkordeon" kann keine Liste aus einem Meta-Feld wiederholen, die Anzahl seiner Eintraege ist fest. Deshalb das eigene Widget. Der Schalter `elementor_ausgabe` im Node **Konfiguration** steht auf `false` und sollte dort bleiben. ### Schreibregeln fuer content_html **Preise niemals ausschreiben.** Sie werden zentral gepflegt und per Shortcode eingesetzt: ``` [skrift_preis key="SCHLUESSEL" ab="0|1" brutto="0|1"] ``` | Parameter | Bedeutung | |---|---| | `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: ``` Handschriftliche Briefe ab [skrift_preis key="briefe" ab="1"] pro Stueck. ``` Ein unbekannter `key` erzeugt eine leere Ausgabe, keinen Fehler. Workflow 2 durchsucht `content_html` nach ausgeschriebenen Betraegen (`2,56 EUR`, `0,95 €`) und meldet jeden Treffer in der Benachrichtigung. Der Import wird deswegen nicht abgebrochen, der Beitrag ist ja ein Entwurf. **Keine Bausteine aus dem Seitenlayout im Text.** Der Block "Auch fuer Privatpersonen." und der Abschnitt "Kostenloses Handschriftmuster anfordern." stehen im Single-Post-Template und gehoeren nicht in `content_html`. Die Stilpruefung meldet beide. **FAQ nicht in content_html schreiben.** Sie gehoeren in das Feld `faq` und werden im Beitrag ueber `[skrift_faq]` ausgegeben. Dasselbe gilt fuer die Kurzantwort, die als eigenes Feld `kurzantwort` transportiert wird. Workflow 1 legt diese Regeln als Block `schreibregeln` in jeden Brief, damit sie beim Schreiben direkt vorliegen. ### Ablauf 1. Ordner `artikel/` auflisten, alle `artikel-*.json` einsammeln 2. Je Artikel: Datei laden, zugehoerigen Brief laden 3. `POST /evaluate-content` an NeuronWriter, `content_score` auslesen 4. Score unter Zielwert: - fehlende Begriffe gegen den Brief berechnen - `nachbessern/nachbessern-.json` committen - naechster Artikel 5. Score ab Zielwert: - `POST /import-content` speichert die Revision in NeuronWriter - `GET /wp-json/wp/v2/posts?slug=` prueft auf vorhandenen Beitrag - Kategorie suchen, bei Bedarf anlegen - Beitrag anlegen oder aktualisieren, immer `status: draft` - Meta-Felder und SEOPress-Felder im selben Request - Cache leeren (siehe Hinweis unten) - Beitragsbild erzeugen, falls noch keins gesetzt ist (siehe unten) - Zeile in `status-importe.json` 6. Benachrichtigung mit Vorschaulinks ### Zielwert anpassen Im Node **Konfiguration**, Feld `score_ziel`. Standard ist 65. Der Wert gilt fuer alle Artikel eines Laufs. Fuer einen einzelnen Lauf laesst er sich ueber den Webhook uebersteuern, ohne den Workflow zu aendern: ```bash curl -X POST https://n8n.lucas-orth.de/webhook/pruefen-und-importieren \ -H "Content-Type: application/json" -d '{"score_ziel":60}' ``` Ohne Angabe bleibt es bei 65. Alle anderen Stellschrauben liegen im selben Node: Basis-URLs, Ordnernamen, Pause zwischen Artikeln, Empfaengeradresse. ### Warum 65 und nicht hoeher Ab etwa 70 verlangt NeuronWriter Wettbewerbernamen, ausgeschriebene Preise und exakte Wortfolgen, die sich nur mit schiefen Saetzen einbauen lassen. Der Referenzartikel kam mit solchen Saetzen auf 72 und ohne sie auf 71. Verbindlich sind deshalb nur die Pflichtbegriffe, Zusatzbegriffe nur, wenn der Satz dadurch nicht schlechter wird. Score, Luecken und Stilpruefung rechnen gegen Kurzantwort, Artikel und FAQ zusammen, weil alle drei auf der fertigen Seite stehen. ### Stilpruefung Workflow 2 prueft jeden Artikel auf Muster, die maschinell wirken, und meldet die Fundstellen in der Benachrichtigung und im Nachbesserungsbericht. Der Import wird dadurch nicht blockiert. Geprueft werden Formeln wie "Genau das", "Der Grund ist", "nicht nur, sondern auch", "entscheidend ist", "der groesste Hebel", "macht den Unterschied", Meta-Saetze wie "Wir zeigen Ihnen", unbelegte Behauptungen wie "erfahrungsgemaess" oder "in aller Regel", Aufzaehlungen mit erstens und zweitens, Dreierreihen aus Adjektiven, rhetorische Fragen mit Sofortantwort, gleichfoermige Satzlaengen, gleiche Satz- und Absatzanfaenge, gleich lange Absaetze und fehlendes Praxiswissen. Dazu die Linkpruefung: Das CTA-Ziel muss eine Business-Leistungsseite sein (`handgeschriebene-businessbriefe`, `handgeschriebene-postkarten`, `unterschriftenservice`, `automatisierte-follow-ups`) und mindestens zweimal verlinkt sein, insgesamt mindestens zwei Business-Links. Hoechstens ein Link auf die Privatkunden-Seiten, und nur im Block "Auch fuer Privatpersonen.". `papier-schriftmuster` und `kontakt` sind ergaenzend erlaubt. Links auf `review` (Dankeseite nach Bestellung) und Rechtstexte werden gemeldet. Die Regeln liegen in `build/stilpruefung.js` und lassen sich dort erweitern. Lokal testen: ``` node -e "const {stilpruefung}=require('./build/stilpruefung.js');console.log(stilpruefung(require('./artikel/artikel-SLUG.json')))" ``` Danach `python3 build/gen_wf2.py` und den Workflow in n8n aktualisieren. ### Praxiswissen Jede Zeile in `artikel-index.json` hat ein Feld `praxis`, eine Liste von drei bis fuenf Stichpunkten aus der eigenen Arbeit: Beispiele, typische Fehler, Zahlen aus der Produktion, klare Empfehlungen, auch Faelle, in denen Handschrift sich nicht lohnt. Workflow 1 legt sie in den Brief. Ist die Liste leer, wird der Artikel nicht geschrieben, sondern zuerst das Praxiswissen eingeholt. ### Nachbesserungsbericht `/evaluate-content` liefert ausschliesslich `{status, content_score}`, also keine Liste fehlender Begriffe. Der Workflow berechnet sie deshalb selbst, indem er den Artikeltext gegen `begriffe_pflicht` und `begriffe_erweitert` aus dem Brief zaehlt. ```json { "score": 44, "score_ziel": 72, "wortzahl": {"ist": 96, "ziel": 1178, "differenz": -1082}, "fehlende_pflichtbegriffe": [{"begriff": "empfänger", "soll_min": 1, "soll_max": 9, "ist": 0, "luecke": 1}], "fehlende_zusatzbegriffe": [], "unbeantwortete_fragen": [{"frage": "...", "gewicht": 10}] } ``` Die Zaehlung arbeitet als Teilstring auf kleingeschriebenem Text. Komposita zaehlen mit: `brief` wird auch in `Werbebriefe` gefunden. Umlaute werden **nicht** normalisiert. Steht im Artikel `Empfaenger` statt `Empfänger`, gilt der Begriff als fehlend. Das ist gewollt, weil NeuronWriter genauso zaehlt. --- ### Beitragsbild Hat ein importierter Beitrag noch kein Beitragsbild, erzeugt Workflow 2 eins: 1. `bild_farbe` und `bildmotiv` aus `artikel-index.json` lesen 2. Prompt nach `ratgeber/bildstil.md` bauen, die Farbe zusaetzlich mit Namen, weil das Modell reine Hexwerte etwas zu kraeftig umsetzt 3. `POST https://api.openai.com/v1/images/generations` mit `gpt-image-2`, Groesse `1200x1200` (quadratisch fuer die Artikelseite, Motiv mittig im Streifen, damit die Uebersicht auf 16:9 zuschneiden kann), `output_format: webp`, `quality: medium`. Antwort in `data[0].b64_json`, Dauer rund 25 Sekunden. 4. Als WebP in die Mediathek laden, Alt-Text "Illustration zum Artikel: " 5. Als `featured_media` setzen und gegenpruefen Ist schon ein Beitragsbild gesetzt, passiert nichts, auch nicht beim erneuten Import. **Neu erzeugen:** Beitragsbild im Beitrag entfernen, Workflow 2 erneut starten. Motiv anpassen geht ueber `bildmotiv` in `artikel-index.json`. Scheitert OpenAI (Guthaben, Verifizierung, abgelehnter Prompt), wird der Artikel trotzdem importiert und die Ursache gemeldet. Das Hochladen und Setzen in WordPress laeuft dagegen ohne Continue On Fail. Schalter und Werte im Node **Konfiguration**: `bild_aktiv`, `bild_modell`, `bild_groesse`, `bild_qualitaet`. Credential: `OpenAI Bilder` (HTTP Header Auth, `Authorization: Bearer `). ## 4. Fehlerverhalten Jeder HTTP-Node hat einen eigenen Fehlerausgang mit einer Meldung, die die wahrscheinliche Ursache benennt statt nur den Statuscode. Ein Artikel darf uebersprungen werden bei: Analyse-Start, Status-Abfrage, Poll-Timeout, Artikel-Datei laden, Score-Ermittlung, Nachbesserungs-Commit. Der Lauf geht dann mit dem naechsten Artikel weiter, der Fehler landet in der Benachrichtigung. Ein Artikel darf **nicht** uebersprungen werden beim Schreiben nach WordPress. Der Node `Beitrag anlegen oder aktualisieren` hat bewusst kein Continue On Fail. Schlaegt er fehl, stoppt der gesamte Lauf, damit kein halb importierter Zustand unbemerkt bleibt. Dasselbe gilt fuer das Zurueckschreiben des Status in Workflow 1: bleibt die Tabelle auf `offen`, wuerden beim naechsten Lauf Analysen doppelt erzeugt und Kontingent verbraucht. HTTP-Nodes wiederholen bei technischen Fehlern dreimal mit drei Sekunden Abstand. Zwischen zwei Artikeln liegt eine Pause von fuenf Sekunden. **Cache leeren gibt HTTP 403.** Der Redakteur-Benutzer darf `POST /wp-json/seopress/v1/commands/clear-cache` nicht aufrufen. Das ist protokolliert und bewusst kein Abbruchgrund: der importierte Beitrag ist ein Entwurf, ein Cache-Purge waere wirkungslos. Relevant wird es erst beim Aktualisieren eines bereits veroeffentlichten Beitrags. **Achtung bei haengenden Zielen.** Der `timeout` eines HTTP-Nodes greift erst nach dem Verbindungsaufbau. Ein Ziel, das SYN-Pakete verschluckt statt sie abzulehnen, laesst den Node unbegrenzt stehen, der Fehlerpfad feuert nie und die Execution blockiert. Dagegen hilft nur `EXECUTIONS_TIMEOUT` in der n8n-Umgebung. --- ## 5. Aufbau des Repos ``` artikel-index.json Redaktionsplan artikel/ fertige Artikel aus Cowork briefe/ NeuronWriter-Briefe aus Workflow 1 nachbessern/ Berichte zu Artikeln unter Zielwert status-importe.json was wann als Entwurf nach WordPress ging workflows/ die importfertigen n8n-Dateien, dazu netzcheck build/ Generatorskripte, die die Workflow-JSONs erzeugen wordpress/ Mu-Plugin fuer die Meta-Felder docs/ Feldnachweis und Rohbelege der API-Antworten .env Zugangsdaten, nicht versioniert .env.example Vorlage ohne Werte ``` Die Workflow-JSONs werden nicht von Hand bearbeitet, sondern aus `build/gen_wf1.py` und `build/gen_wf2.py` erzeugt: ``` python3 build/gen_wf1.py python3 build/gen_wf2.py ``` Danach in n8n importieren oder per API aktualisieren. Wer lieber direkt in der n8n-Oberflaeche arbeitet, exportiert den Workflow anschliessend wieder nach `workflows/` und laesst die Generatoren liegen. --- ## 6. Hilfsworkflow netzcheck In n8n liegt `netzcheck (Hilfswerkzeug)`. Er fragt aus n8n heraus eine beliebige URL ab und meldet Statuscode, Groesse und Fehlercode zurueck. Nuetzlich, wenn ein Ziel aus n8n nicht erreichbar scheint: ```bash curl -X POST https://n8n.lucas-orth.de/webhook/probe \ -H "Content-Type: application/json" \ -d '{"url":"http://gitea:3000/api/v1/version"}' ``` --- ## 7. Belege `docs/feldnachweis.md` listet jede Stelle, an der ein Feldname aus einer echten API-Antwort ermittelt wurde, mit Beispielantwort. Die Rohantworten liegen als `docs/beleg-*.json` daneben. `docs/testlauf.md` protokolliert den Testlauf vom 20.09.2026 gegen die echten Systeme, inklusive der beiden Fehler, die er gefunden hat, und des Netzwerkbefunds zu Gitea.