Files
Skrift-Auto-Article/README.md

397 lines
15 KiB
Markdown

# 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-<slug>.json NeuronWriter-Brief als Schreibvorlage
|
v Cowork schreibt den Artikel
artikel/artikel-<slug>.json fertiger Artikel
|
v Workflow 2: pruefen-und-importieren
WordPress-Entwurf + nachbessern/nachbessern-<slug>.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 <gitea-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-<slug>.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-<slug>.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": "<h1>...</h1><p>...</p>",
"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 Node **Konfiguration** hat den Schalter `elementor_ausgabe`.
Steht er auf `true` (Standard), baut der Workflow aus dem Artikel eine
Elementor-Struktur: Der Text wird an den H2-Grenzen in einzelne
Text-Editor-Widgets zerlegt, die Kurzantwort bekommt einen eigenen Block, und
das FAQ wird zu einem **nested-accordion** mit aktiviertem `faq_schema`.
Das ist dasselbe Widget, das die Leistungsseiten von skrift.de verwenden, der
Beitrag laesst sich also im Builder wie jede andere Seite bearbeiten und
WordPress gibt automatisch FAQPage-Schema aus.
Steht er auf `false`, landet der Artikel als klassischer Beitragsinhalt. Das FAQ
gibt dann der Shortcode `[skrift_faq]` aus, den das Mu-Plugin mitbringt.
Die Meta-Felder `kurzantwort`, `faq`, `cta_ziel` und `stand_datum` werden in
beiden Faellen geschrieben, unabhaengig von der Darstellung.
### Schreibregeln fuer content_html
**Preise niemals ausschreiben.** Sie werden zentral gepflegt und per Shortcode
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-<slug>.json` committen
- naechster Artikel
5. Score ab Zielwert:
- `POST /import-content` speichert die Revision in NeuronWriter
- `GET /wp-json/wp/v2/posts?slug=<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.