# 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": [{"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. - 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. **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) - Zeile in `status-importe.json` 6. Benachrichtigung mit Vorschaulinks ### Zielwert anpassen Im Node **Konfiguration**, Feld `score_ziel`. Standard ist 72. 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 72. Alle anderen Stellschrauben liegen im selben Node: Basis-URLs, Ordnernamen, Pause zwischen Artikeln, Empfaengeradresse. ### 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. --- ## 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.