368 lines
13 KiB
Markdown
368 lines
13 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.
|
|
|
|
### 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` | Produkt- oder Positionsschluessel, z. B. `briefe`, `postkarten`, `porto_inland` |
|
|
| `ab=1` | guenstigster Staffelpreis; `ab=0` der Standardpreis |
|
|
| `brutto=1` | inkl. MwSt.; `brutto=0` netto |
|
|
|
|
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.
|