Feature: Automatische Metadaten-Extraktion aus Frontmatter
- Neuer markdown_parser.py mit YAML-Frontmatter Extraktion - Unterstützung für drei Modi: Einzelne URL, YAML-Batch, Forgejo-Repo - Metadaten (name, description, tags, image, author) aus Frontmatter - Schema.org-Support für commonMetadata - Vereinfachte posts.yaml (nur URLs statt vollständiger Metadaten) - Aktualisierte Dokumentation (README.md, QUICKSTART.md) - Beispiel-Beitrag mit vollständigem Frontmatter
This commit is contained in:
parent
e3b19bb0df
commit
7a234be652
6 changed files with 880 additions and 180 deletions
194
QUICKSTART.md
194
QUICKSTART.md
|
|
@ -1,5 +1,51 @@
|
|||
# Schnellstart-Anleitung
|
||||
|
||||
## Überblick
|
||||
|
||||
Das System extrahiert **automatisch alle Metadaten aus dem YAML-Frontmatter** Ihrer Markdown-Dateien:
|
||||
- **name** → WordPress-Titel
|
||||
- **description** oder **summary** → Excerpt
|
||||
- **image** → Beitragsbild
|
||||
- **tags** → WordPress-Tags
|
||||
- **categories** → WordPress-Kategorien
|
||||
- **author** → WordPress-Autor
|
||||
|
||||
Sie müssen nur noch die **URL zur Markdown-Datei** angeben!
|
||||
|
||||
## Drei Verwendungsmodi
|
||||
|
||||
### 1. Einzelne URL (Am einfachsten!)
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python workflow.py "https://example.com/artikel.md"
|
||||
```
|
||||
|
||||
### 2. Mehrere URLs aus YAML-Datei
|
||||
|
||||
Erstellen Sie `posts.yaml`:
|
||||
```yaml
|
||||
posts:
|
||||
- url: "https://example.com/artikel1.md"
|
||||
- url: "https://example.com/artikel2.md"
|
||||
- file: "content/lokaler-artikel.md"
|
||||
```
|
||||
|
||||
Dann ausführen:
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python workflow.py posts.yaml
|
||||
```
|
||||
|
||||
### 3. Ganzes Repository (Forgejo/Gitea)
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python workflow.py --repo "https://codeberg.org/user/repo" main
|
||||
```
|
||||
|
||||
## Schnellstart-Schritte
|
||||
|
||||
## 1. Virtuelle Umgebung aktivieren
|
||||
|
||||
Aktivieren Sie zuerst die Python-Umgebung:
|
||||
|
|
@ -28,64 +74,102 @@ WORDPRESS_USERNAME=IHR_USERNAME_HIER # ← Hier eintragen!
|
|||
WORDPRESS_APP_PASSWORD=UIVI 4Tdy oojL 9iZG g3X2 iAn5
|
||||
```
|
||||
|
||||
## 4. posts.yaml anpassen
|
||||
## 4. posts.yaml anpassen (Optional - nur für Batch-Verarbeitung)
|
||||
|
||||
Bearbeiten Sie `posts.yaml` und fügen Sie Ihre Beiträge hinzu:
|
||||
Für **einzelne URLs** brauchen Sie keine YAML-Datei!
|
||||
|
||||
Für **mehrere Beiträge** erstellen Sie `posts.yaml`:
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- title: "Ihr Beitragstitel"
|
||||
markdown_url: "https://ihre-url.de/artikel.md"
|
||||
# ODER für lokale Dateien:
|
||||
# markdown_file: "content/ihr-artikel.md"
|
||||
status: "draft"
|
||||
categories:
|
||||
- "Ihre Kategorie"
|
||||
tags:
|
||||
- "Ihr Tag"
|
||||
- url: "https://ihre-url.de/artikel.md"
|
||||
- file: "content/lokaler-artikel.md"
|
||||
|
||||
settings:
|
||||
default_status: "draft"
|
||||
```
|
||||
|
||||
**Das war's!** Alle Metadaten (Titel, Tags, Kategorien, Bild) kommen aus dem Frontmatter der Markdown-Dateien.
|
||||
|
||||
## 5. Workflow ausführen
|
||||
|
||||
Stellen Sie sicher, dass die virtuelle Umgebung aktiviert ist (siehe Schritt 1), dann:
|
||||
|
||||
### Einzelne URL (empfohlen für Tests):
|
||||
```bash
|
||||
python workflow.py
|
||||
python workflow.py "https://example.com/artikel.md"
|
||||
```
|
||||
|
||||
### Mehrere URLs aus YAML:
|
||||
```bash
|
||||
python workflow.py posts.yaml
|
||||
```
|
||||
|
||||
### Ganzes Forgejo-Repository:
|
||||
```bash
|
||||
python workflow.py --repo "https://codeberg.org/user/repo" main
|
||||
```
|
||||
|
||||
## Testen mit dem Beispiel-Beitrag
|
||||
|
||||
Ein Test-Beitrag ist bereits in `content/beispiel-beitrag.md` vorhanden.
|
||||
Ein Test-Beitrag mit vollständigem Frontmatter ist in `content/beispiel-beitrag.md`.
|
||||
|
||||
Um diesen zu verwenden, passen Sie `posts.yaml` an:
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- title: "Test: Beispiel-Beitrag"
|
||||
markdown_file: "content/beispiel-beitrag.md"
|
||||
status: "draft"
|
||||
categories:
|
||||
- "Test"
|
||||
**Direkter Test:**
|
||||
```bash
|
||||
python workflow.py "content/beispiel-beitrag.md"
|
||||
```
|
||||
|
||||
Dann führen Sie aus:
|
||||
**Oder über YAML:**
|
||||
```yaml
|
||||
posts:
|
||||
- file: "content/beispiel-beitrag.md"
|
||||
```
|
||||
|
||||
```bash
|
||||
python workflow.py
|
||||
python workflow.py posts.yaml
|
||||
```
|
||||
|
||||
## Was passiert beim Ausführen?
|
||||
|
||||
1. ✅ System liest die `posts.yaml`
|
||||
2. ✅ Für jeden Beitrag:
|
||||
- Lädt Markdown-Inhalt (von URL oder lokal)
|
||||
- Konvertiert Markdown zu HTML
|
||||
- Prüft ob Beitrag bereits existiert (nach Titel)
|
||||
- Erstellt fehlende Kategorien/Tags
|
||||
- Lädt Beitragsbilder hoch (falls vorhanden)
|
||||
- Erstellt den WordPress-Beitrag
|
||||
1. ✅ System lädt die Markdown-Datei (von URL oder lokal)
|
||||
2. ✅ **Extrahiert automatisch Metadaten aus dem YAML-Frontmatter:**
|
||||
- `name` → Titel
|
||||
- `description`/`summary` → Excerpt
|
||||
- `image` → Beitragsbild
|
||||
- `tags` → WordPress-Tags
|
||||
- `categories` → WordPress-Kategorien (falls vorhanden)
|
||||
- `author` → Autor
|
||||
3. ✅ Konvertiert Markdown zu HTML
|
||||
4. ✅ Prüft ob Beitrag bereits existiert (nach Titel)
|
||||
5. ✅ Erstellt fehlende Kategorien/Tags
|
||||
6. ✅ Lädt Beitragsbilder hoch (falls vorhanden)
|
||||
7. ✅ Erstellt den WordPress-Beitrag
|
||||
|
||||
3. ✅ Zeigt Zusammenfassung an
|
||||
## Frontmatter-Beispiel
|
||||
|
||||
Ihre Markdown-Dateien sollten so aussehen:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "Mein Artikel-Titel"
|
||||
description: "Eine kurze Zusammenfassung des Artikels"
|
||||
image: "https://example.com/bild.jpg"
|
||||
tags:
|
||||
- WordPress
|
||||
- Tutorial
|
||||
- Open Source
|
||||
categories:
|
||||
- Tutorials
|
||||
author:
|
||||
- Max Mustermann
|
||||
---
|
||||
|
||||
# Artikel-Inhalt
|
||||
|
||||
Hier beginnt der eigentliche Inhalt...
|
||||
```
|
||||
|
||||
Das System versteht auch Schema.org-Metadaten (wie im `beispiel-beitrag.md`)!
|
||||
|
||||
## Wichtige Hinweise
|
||||
|
||||
|
|
@ -97,34 +181,38 @@ python workflow.py
|
|||
## Beispiel-Ausgabe
|
||||
|
||||
```
|
||||
Lade Konfiguration aus: posts.yaml
|
||||
Direkt-Modus: Verarbeite URL: https://example.com/artikel.md
|
||||
|
||||
Verbinde mit WordPress: https://news.rpi-virtuell.de
|
||||
|
||||
Verarbeite 1 Beitrag/Beiträge...
|
||||
============================================================
|
||||
Verarbeite Markdown von URL: https://example.com/artikel.md
|
||||
============================================================
|
||||
Titel: Die Kraft der Gemeinschaft: Prozesse statt Strukturen
|
||||
Beitrag 'Die Kraft der Gemeinschaft' erstellt (ID: 123, Status: draft)
|
||||
|
||||
============================================================
|
||||
Verarbeite Beitrag: Test: Beispiel-Beitrag
|
||||
============================================================
|
||||
Lese lokale Markdown-Datei: content/beispiel-beitrag.md
|
||||
Beitrag 'Test: Beispiel-Beitrag' erstellt (ID: 123, Status: draft)
|
||||
|
||||
============================================================
|
||||
ZUSAMMENFASSUNG
|
||||
============================================================
|
||||
Erfolgreich: 1
|
||||
Fehler: 0
|
||||
Gesamt: 1
|
||||
============================================================
|
||||
✅ Erfolgreich: Beitrag erstellt (ID: 123)
|
||||
```
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
1. Testen Sie mit dem Beispiel-Beitrag
|
||||
2. Passen Sie `posts.yaml` für Ihre echten Beiträge an
|
||||
3. Führen Sie `python workflow.py` aus
|
||||
4. Überprüfen Sie die Beiträge in WordPress
|
||||
5. Bei Erfolg: Ändern Sie `status: "draft"` zu `status: "publish"`
|
||||
1. **Testen Sie mit dem Beispiel-Beitrag:**
|
||||
```bash
|
||||
python workflow.py "content/beispiel-beitrag.md"
|
||||
```
|
||||
|
||||
2. **Testen Sie mit Ihrer eigenen URL:**
|
||||
```bash
|
||||
python workflow.py "https://ihre-url.de/artikel.md"
|
||||
```
|
||||
|
||||
3. **Für Batch-Verarbeitung:** Erstellen Sie `posts.yaml` mit mehreren URLs
|
||||
|
||||
4. **Für Repository-Import:** Nutzen Sie `--repo` für Forgejo/Gitea
|
||||
|
||||
5. **Überprüfen Sie die Beiträge in WordPress**
|
||||
|
||||
6. **Bei Erfolg:** Ändern Sie `default_status: "publish"` in den Settings
|
||||
|
||||
## Hilfe
|
||||
|
||||
|
|
|
|||
225
README.md
225
README.md
|
|
@ -2,14 +2,18 @@
|
|||
|
||||
Automatisierter Workflow zum Erstellen von WordPress-Beiträgen aus Markdown-Dateien über die WordPress REST-API.
|
||||
|
||||
**Neu:** Metadaten werden automatisch aus dem YAML-Frontmatter der Markdown-Dateien extrahiert!
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **Automatische Metadaten-Extraktion**: name, description, tags, image, author aus YAML-Frontmatter
|
||||
- ✅ **Drei Verwendungsmodi**: Einzelne URL, YAML-Batch, Forgejo-Repository
|
||||
- ✅ **Duplikatsprüfung**: Verhindert das doppelte Erstellen von Beiträgen und Medien
|
||||
- ✅ **Markdown zu HTML**: Automatische Konvertierung von Markdown-Inhalten
|
||||
- ✅ **Medien-Upload**: Hochladen von Beitragsbildern mit Duplikatsprüfung
|
||||
- ✅ **Kategorien & Tags**: Automatische Erstellung fehlender Kategorien und Tags
|
||||
- ✅ **Flexible Quellen**: Unterstützt Markdown-URLs und lokale Dateien
|
||||
- ✅ **YAML-Konfiguration**: Einfache Verwaltung mehrerer Beiträge
|
||||
- ✅ **Flexible Quellen**: Unterstützt Markdown-URLs, lokale Dateien und Forgejo-Repositories
|
||||
- ✅ **Schema.org Support**: Versteht commonMetadata-Strukturen
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
|
|
@ -58,29 +62,31 @@ Automatisierter Workflow zum Erstellen von WordPress-Beiträgen aus Markdown-Dat
|
|||
|
||||
## Verwendung
|
||||
|
||||
### 1. YAML-Konfiguration erstellen
|
||||
### Modus 1: Einzelne URL (Am einfachsten!)
|
||||
|
||||
Erstellen Sie eine `posts.yaml`-Datei mit Ihren Beiträgen:
|
||||
Verarbeiten Sie eine einzelne Markdown-URL direkt:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python workflow.py "https://example.com/artikel.md"
|
||||
```
|
||||
|
||||
Alle Metadaten (Titel, Tags, Kategorien, Bild) werden aus dem YAML-Frontmatter der Markdown-Datei extrahiert.
|
||||
|
||||
### Modus 2: Mehrere URLs aus YAML-Datei
|
||||
|
||||
Erstellen Sie eine `posts.yaml`-Datei:
|
||||
|
||||
```yaml
|
||||
# Einfache URL-Liste - Metadaten kommen aus dem Frontmatter!
|
||||
posts:
|
||||
- title: "Mein erster Beitrag"
|
||||
markdown_url: "https://raw.githubusercontent.com/user/repo/main/post.md"
|
||||
status: "draft" # draft, publish, pending, private
|
||||
categories:
|
||||
- "News"
|
||||
- "Tutorials"
|
||||
tags:
|
||||
- "WordPress"
|
||||
- "API"
|
||||
featured_image: "images/header.jpg"
|
||||
excerpt: "Eine kurze Zusammenfassung"
|
||||
|
||||
- title: "Lokaler Beitrag"
|
||||
markdown_file: "content/local-post.md"
|
||||
status: "publish"
|
||||
categories:
|
||||
- "Updates"
|
||||
- url: "https://example.com/artikel1.md"
|
||||
- url: "https://example.com/artikel2.md"
|
||||
- file: "content/lokaler-artikel.md"
|
||||
|
||||
# Optional: Metadaten überschreiben
|
||||
- url: "https://example.com/artikel3.md"
|
||||
status: "publish" # Überschreibt Status aus Frontmatter
|
||||
|
||||
settings:
|
||||
default_status: "draft"
|
||||
|
|
@ -88,26 +94,84 @@ settings:
|
|||
skip_duplicate_media: true
|
||||
```
|
||||
|
||||
### 2. Workflow ausführen
|
||||
|
||||
Aktivieren Sie zuerst die virtuelle Umgebung:
|
||||
Dann ausführen:
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Dann führen Sie das Workflow-Script aus:
|
||||
|
||||
```bash
|
||||
python workflow.py posts.yaml
|
||||
```
|
||||
|
||||
Oder ohne Angabe der Datei (verwendet `posts.yaml` als Standard):
|
||||
### Modus 3: Ganzes Forgejo/Gitea-Repository
|
||||
|
||||
Verarbeiten Sie alle Markdown-Dateien aus einem Repository:
|
||||
|
||||
```bash
|
||||
python workflow.py
|
||||
source .venv/bin/activate
|
||||
python workflow.py --repo "https://codeberg.org/user/repo" main
|
||||
```
|
||||
|
||||
Dies lädt automatisch alle `.md`-Dateien aus dem Repository und erstellt WordPress-Beiträge.
|
||||
|
||||
## Markdown-Frontmatter Format
|
||||
|
||||
Ihre Markdown-Dateien sollten YAML-Frontmatter enthalten:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: "Artikel-Titel"
|
||||
description: "Kurze Zusammenfassung für WordPress-Excerpt"
|
||||
image: "https://example.com/bild.jpg"
|
||||
tags:
|
||||
- WordPress
|
||||
- Tutorial
|
||||
- Open Source
|
||||
categories:
|
||||
- Tutorials
|
||||
author:
|
||||
- Max Mustermann
|
||||
---
|
||||
|
||||
# Artikel-Inhalt
|
||||
|
||||
Hier beginnt der eigentliche Markdown-Inhalt...
|
||||
```
|
||||
|
||||
### Unterstützte Frontmatter-Felder
|
||||
|
||||
Das System extrahiert automatisch:
|
||||
|
||||
- **Titel**: `name` oder `title`
|
||||
- **Excerpt**: `description` oder `summary`
|
||||
- **Beitragsbild**: `image` oder `cover.image`
|
||||
- **Tags**: `tags` (Liste oder kommagetrennt)
|
||||
- **Kategorien**: `categories` (Liste oder kommagetrennt)
|
||||
- **Autor**: `author` (String oder Liste)
|
||||
- **Status**: `status` oder aus `creativeWorkStatus`
|
||||
- **Datum**: `date` oder `datePublished`
|
||||
|
||||
### Schema.org Support
|
||||
|
||||
Das System versteht auch Schema.org-Metadaten:
|
||||
|
||||
```yaml
|
||||
---
|
||||
'@context': https://schema.org/
|
||||
type: LearningResource
|
||||
name: "Artikel-Titel"
|
||||
description: "Beschreibung"
|
||||
image: "https://example.com/bild.jpg"
|
||||
creator:
|
||||
- givenName: Max
|
||||
familyName: Mustermann
|
||||
type: Person
|
||||
tags:
|
||||
- Tag1
|
||||
- Tag2
|
||||
---
|
||||
```
|
||||
|
||||
Siehe `content/beispiel-beitrag.md` für ein vollständiges Beispiel.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
|
|
@ -117,11 +181,13 @@ newsimport/
|
|||
├── .gitignore # Git-Ignorier-Liste
|
||||
├── requirements.txt # Python-Abhängigkeiten
|
||||
├── wordpress_api.py # WordPress REST-API Client
|
||||
├── markdown_parser.py # YAML-Frontmatter Parser
|
||||
├── workflow.py # Haupt-Workflow Script
|
||||
├── posts.yaml # Beitrags-Konfiguration
|
||||
├── posts.yaml # Beitrags-Konfiguration (optional)
|
||||
├── README.md # Diese Datei
|
||||
├── QUICKSTART.md # Schnellstart-Anleitung
|
||||
├── content/ # Lokale Markdown-Dateien (optional)
|
||||
│ └── *.md
|
||||
│ └── beispiel-beitrag.md
|
||||
└── images/ # Lokale Bilder (optional)
|
||||
└── *.jpg/png
|
||||
```
|
||||
|
|
@ -163,33 +229,36 @@ existing_post_id = wp.check_post_exists("Titel")
|
|||
existing_media_id = wp.check_media_exists("bild.jpg")
|
||||
```
|
||||
|
||||
## YAML-Konfiguration
|
||||
## YAML-Konfiguration (posts.yaml)
|
||||
|
||||
### Beitrags-Felder
|
||||
### Vereinfachte Struktur
|
||||
|
||||
- `title` (erforderlich): Titel des Beitrags
|
||||
- `markdown_url`: URL zur Markdown-Datei
|
||||
- `markdown_file`: Pfad zu lokaler Markdown-Datei
|
||||
- `content`: Direkter Markdown-Inhalt
|
||||
- `status`: `draft`, `publish`, `pending`, `private`
|
||||
- `categories`: Liste von Kategorie-Namen
|
||||
- `tags`: Liste von Tag-Namen
|
||||
- `featured_image`: Pfad oder URL zum Beitragsbild
|
||||
- `excerpt`: Kurze Zusammenfassung
|
||||
- `author`: Autor-Username
|
||||
Metadaten werden automatisch aus dem Frontmatter extrahiert:
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- url: "https://example.com/artikel.md" # URL zur Markdown-Datei
|
||||
- file: "content/artikel.md" # Oder lokale Datei
|
||||
|
||||
# Optional: Metadaten überschreiben
|
||||
- url: "https://example.com/artikel2.md"
|
||||
status: "publish" # Überschreibt Frontmatter
|
||||
categories: # Ergänzt Frontmatter-Kategorien
|
||||
- "Extra-Kategorie"
|
||||
```
|
||||
|
||||
### Globale Einstellungen
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
default_status: "draft" # Standard-Status für Beiträge
|
||||
default_author: "admin" # Standard-Autor
|
||||
default_status: "draft" # Fallback wenn nicht im Frontmatter
|
||||
default_author: "admin" # Fallback wenn nicht im Frontmatter
|
||||
skip_duplicates: true # Bestehende Beiträge überspringen
|
||||
skip_duplicate_media: true # Bestehende Medien überspringen
|
||||
markdown_extensions: # Markdown-Erweiterungen
|
||||
- tables
|
||||
- fenced_code
|
||||
- footnotes
|
||||
- extra
|
||||
- codehilite
|
||||
- toc
|
||||
```
|
||||
|
||||
## Duplikatsprüfung
|
||||
|
|
@ -202,41 +271,45 @@ Vor dem Upload wird geprüft, ob eine Datei mit dem gleichen Namen bereits exist
|
|||
|
||||
## Beispiele
|
||||
|
||||
### Beispiel 1: Einfacher Beitrag von URL
|
||||
### Beispiel 1: Einzelne URL direkt
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- title: "News Update"
|
||||
markdown_url: "https://example.com/news.md"
|
||||
status: "publish"
|
||||
```bash
|
||||
python workflow.py "https://example.com/artikel.md"
|
||||
```
|
||||
|
||||
### Beispiel 2: Beitrag mit Kategorien, Tags und Bild
|
||||
### Beispiel 2: Lokale Datei
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- title: "Tutorial: WordPress REST-API"
|
||||
markdown_url: "https://example.com/tutorial.md"
|
||||
status: "draft"
|
||||
categories:
|
||||
- "Tutorials"
|
||||
- "WordPress"
|
||||
tags:
|
||||
- "REST-API"
|
||||
- "Entwicklung"
|
||||
- "PHP"
|
||||
featured_image: "https://example.com/images/header.jpg"
|
||||
excerpt: "Lernen Sie die WordPress REST-API kennen"
|
||||
```bash
|
||||
python workflow.py "content/beispiel-beitrag.md"
|
||||
```
|
||||
|
||||
### Beispiel 3: Lokale Dateien
|
||||
### Beispiel 3: Mehrere URLs aus YAML
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- title: "Lokaler Inhalt"
|
||||
markdown_file: "content/article.md"
|
||||
status: "publish"
|
||||
featured_image: "images/local-image.jpg"
|
||||
- url: "https://example.com/artikel1.md"
|
||||
- url: "https://example.com/artikel2.md"
|
||||
- file: "content/lokaler-artikel.md"
|
||||
```
|
||||
|
||||
```bash
|
||||
python workflow.py posts.yaml
|
||||
```
|
||||
|
||||
### Beispiel 4: Forgejo-Repository
|
||||
|
||||
```bash
|
||||
python workflow.py --repo "https://codeberg.org/user/repo" main
|
||||
```
|
||||
|
||||
### Beispiel 5: Metadaten überschreiben
|
||||
|
||||
```yaml
|
||||
posts:
|
||||
- url: "https://example.com/artikel.md"
|
||||
status: "publish" # Überschreibt Status aus Frontmatter
|
||||
categories: # Ergänzt Kategorien aus Frontmatter
|
||||
- "Extra-Kategorie"
|
||||
```
|
||||
|
||||
## Fehlerbehebung
|
||||
|
|
|
|||
|
|
@ -1,25 +1,105 @@
|
|||
# Beispiel-Beitrag
|
||||
---
|
||||
|
||||
Dies ist ein Beispiel für einen lokalen Markdown-Beitrag.
|
||||
#commonMetadata:
|
||||
'@context': https://schema.org/
|
||||
creativeWorkStatus: Published
|
||||
type: LearningResource
|
||||
name: "Die Kraft der Gemeinschaft: Wahre Stärke liegt nicht in Strukturen, sondern in Prozessen"
|
||||
description: >-
|
||||
Im FOERBICO-Projekt zeigen wir: Nicht starre Strukturen machen Systeme dauerhaft
|
||||
robust, sondern die Kontinuität und Anpassungsfähigkeit ihrer Prozesse.
|
||||
Am Ise-Schrein und Open-Source-Prinzipien wird deutlich, wie Bildungsinfrastrukturen
|
||||
gemeinschaftsgetragen, erneuerbar und offen gestaltet werden können – jenseits
|
||||
geschlossener Plattformen hin zu atmenden Protokoll-Ökosystemen (z. B. Nostr).
|
||||
license: https://creativecommons.org/licenses/by/4.0/deed.de
|
||||
id: https://oer.community/die-kraft-der-gemeinschaft
|
||||
creator:
|
||||
- givenName: Jörg
|
||||
familyName: Lohrer
|
||||
id: https://orcid.org/0000-0002-9282-0406
|
||||
type: Person
|
||||
affiliation:
|
||||
name: Comenius-Institut
|
||||
id: https://ror.org/025e8aw85
|
||||
type: Organization
|
||||
inLanguage:
|
||||
- de
|
||||
about:
|
||||
- https://w3id.org/kim/hochschulfaechersystematik/n052
|
||||
- https://w3id.org/kim/hochschulfaechersystematik/n079
|
||||
- https://w3id.org/kim/hochschulfaechersystematik/n544
|
||||
image: https://oer.community/die-kraft-der-gemeinschaft/nosTr-schrein.jpg
|
||||
learningResourceType:
|
||||
- https://w3id.org/kim/hcrt/text
|
||||
- https://w3id.org/kim/hcrt/web_page
|
||||
educationalLevel:
|
||||
- https://w3id.org/kim/educationalLevel/level_A
|
||||
datePublished: '2025-09-02'
|
||||
|
||||
## Einführung
|
||||
#staticSiteGenerator:
|
||||
author:
|
||||
- Jörg Lohrer
|
||||
title: 'Die Kraft der Gemeinschaft: Prozesse statt Strukturen'
|
||||
cover:
|
||||
relative: true
|
||||
image: nosTr-schrein.jpg
|
||||
caption: "Symbolbild: Der Ise-Schrein als Metapher für erneuerbare, gemeinschaftsgetragene Bildungsinfrastruktur."
|
||||
alt: "Darstellung eines Schreins als Sinnbild zyklischer Erneuerung; übertragen auf offene Bildungsinfrastrukturen (z. B. Nostr)."
|
||||
hiddenInSingle: true
|
||||
summary: |
|
||||
Warum Prozesse wichtiger sind als Strukturen: Was der Ise-Schrein und Open Source für eine resiliente, gemeinschaftsgetragene Bildungsinfrastruktur lehren.
|
||||
url: die-kraft-der-gemeinschaft
|
||||
tags:
|
||||
- Open Educational Resources (OER)
|
||||
- Open Educational Practices (OEP)
|
||||
- Community
|
||||
- FOERBICO
|
||||
- Nostr
|
||||
- Bildungsinfrastruktur
|
||||
|
||||
Dieser Beitrag demonstriert die Verwendung von lokalem Markdown-Content für WordPress-Beiträge.
|
||||
---
|
||||
|
||||
## Features
|
||||
Zur Entwicklung unseres Community-Hubs untersuchen wir im FOERBICO-Projekt, wie langfristig erfolgreiche Kooperationsmodelle gelingen können. Eine wichtige Erkenntnis, die wir bisher gewinnen konnten: Die Robustheit eines Systems hängt weniger von seinen organisatorischen oder technischen Strukturen ab, sondern vor allem von der Kontinuität und Anpassungsfähigkeit seiner zugrunde liegenden Prozesse. Wenn wir mit einer Hub-Entwicklung die Bildungscommunities dabei unterstützen wollen, dass ihre Prozesse der OEP (Open Educational Practice) "[alles tragen, allem standhalten und niemals zu Fall kommen](https://offene-bibel.de/wiki/1_Korinther_13#l7)", brauchen wir eine Technik, die die zyklischen Erneuerungsprozesse dieser Communities nachhaltig unterstützt. Lasst uns einen Blick über den Tellerrand wagen und uns Inspiration aus jahrtausendealten Traditionen und Open-Source-Prinzipien schöpfen:
|
||||
|
||||
- **Markdown-Formatierung**: Vollständige Markdown-Unterstützung
|
||||
- **Code-Blöcke**: Syntax-Highlighting für verschiedene Sprachen
|
||||
- **Listen**: Geordnet und ungeordnet
|
||||
- **Links und Bilder**: Vollständige Unterstützung
|
||||
## Prozess statt Bauwerk – Die Kraft der Gemeinschaft am Beispiel des Ise-Schreins
|
||||
|
||||
## Code-Beispiel
|
||||
Stell dir vor, du würdest alle 20 Jahre dein Haus abreißen und identisch wieder aufbauen. Verrückt? In Japan passiert genau das seit 1.300 Jahren mit dem Großen Schrein von Ise, dem heiligsten Ort des Landes. Das Geheimnis seiner Beständigkeit liegt also nicht im Bauwerk selbst, sondern im gemeinsamen Ritual seiner Erneuerung.
|
||||
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, WordPress!")
|
||||
```
|
||||
Nicht das solide Gebäude selbst stiftet hier die Gemeinschaft, sondern die Zuverlässigkeit ihres kontinuierlichen Bauprozesses. Die Manifestation und Struktur des Schreins unterliegt also einem steten Wandel, während die Qualität und Verlässlichkeit im Prozess seiner Erneuerung durch die ritualisierten Abläufe über Generationen hinweg erhalten bleibt.
|
||||
|
||||
## Zusammenfassung
|
||||
## Open Source: Liebe als erneuerbares Baumaterial
|
||||
|
||||
Mit diesem System können Sie einfach Markdown-Inhalte in WordPress-Beiträge umwandeln.
|
||||
Clay Shirky beschreibt Open-Source-Projekte wie das Betriebssysten Linux als moderne Entsprechung zum Ise-Schrein: Ihre Beständigkeit beruhe nicht auf kommerzieller Unterstützung, sondern resultiere aus einem "Akt der Liebe" – sie sei getragen von Menschen, die sich umeinander kümmerten und gemeinsam etwas schaffen würden.
|
||||
|
||||
Die entscheidende Frage für die Langlebigkeit eines Systems sei daher nicht die nach dem das Geschäftsmodell, sondern vielmehr: "Kümmern sich die Menschen, die es lieben, umeinander?" Dieser Indikator könnte sich als ein überlegener Prädiktor für nachhaltige Kooperationserfolge und die Langlebigkeit eines Community-Hubs erweisen.
|
||||
|
||||
## Unsere digitalen Kathedralen der Bildung
|
||||
Schauen wir auf unsere Bildungslandschaft, sehen wir oft das Gegenteil: abgeschlossene Plattformen und getrennte Datensilos. Wir bauen digitale Festungen statt lebendige Gemeinschaften.
|
||||
|
||||
Anstatt Materialien gemeinsam zu ***v***erwenden, zu ***v***erarbeiten, zu ***v***ermischen, zu ***v***ervielfältigen und zu ***v***erbreiten, bleiben Bildungsmedien in Plattformen gefangen und ***v***erwahrt. Statt offener Kollaboration haben wir Insellösungen.
|
||||
|
||||
## Eine Infrastruktur, die atmet
|
||||
|
||||
Was wäre, wenn wir Bildungsinfrastruktur wie den Ise-Schrein denken würden?
|
||||
Protokolle wie [Nostr](https://nostr.how/de/what-is-nostr) zeigen, wie das technisch möglich wird: dezentral, offen und von der Gemeinschaft getragen.
|
||||
|
||||
Das Resultat wäre eine Infrastruktur, die nicht von einzelnen Plattformen, Institutionen oder "Internet-Gebäuden" abhängig ist, sondern von der kollektiven Fürsorge und dem Engagement der Community getragen werden kann – resilient, erneuerbar und offen für alle.
|
||||
|
||||
## Mach mit beim Bauen!
|
||||
|
||||
Die Geschichte des Ise-Schreins lehrt uns: Das beständigste Fundament sind die Menschen, die sich umeinander kümmern. Lasst uns gemeinsam ein lebendiges Ökosystem für die Bildung schaffen, das uns miteinander in Verbindung bringt!
|
||||
|
||||
Hier kannst du mitmachen:
|
||||
- Im Matrix [Space OERcommunity](https://matrix.to/#/#oercommunity:rpi-virtuell.de) Offene Räume für Austausch und Experimente
|
||||
- vor allem [im Raum "edufeed"](https://matrix.to/#/#edufeed:rpi-virtuell.de), wo wir OER & NOSTR zusammendenken
|
||||
- auf Nostr
|
||||
- [hier eine Starthilfe zur Profilerstellung](https://nstart.me/de?an=Primal&am=light&aa=203a8f&asb=yes&s=npub1k85m3haymj3ggjknfrxm5kwtf5umaze4nyghnp29a80lcpmg2k2q54v05a)
|
||||
- Hier ein paar Accounts z.B. von [Jörg](https://njump.me/npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9) oder [Steffen](https://njump.me/npub1r30l8j4vmppvq8w23umcyvd3vct4zmfpfkn4c7h2h057rmlfcrmq9xt9ma)
|
||||
- GitHub [Edufeed](https://github.com/edufeed-org): Wo wir gemeinsam an der Zukunft bauen
|
||||
|
||||

|
||||
|
||||
**Inspirationen:**
|
||||
- [Clay Shirky: Love, Internet Style](https://www.youtube.com/watch?v=Xe1TZaElTAs)
|
||||
- [Steffen Rörtgen: Just calling it Open is not enough](https://habla.news/u/laoc42@getalby.com/h-k72fOoZmf_SOC3cUpqc)
|
||||
- [Ise-Schrein – Japanliebe](https://japanliebe.de/alltaegliches/ise-jingu-schrein-neubau-alle-20-jahre/)
|
||||
226
markdown_parser.py
Normal file
226
markdown_parser.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Markdown Parser mit YAML-Frontmatter Unterstützung
|
||||
Extrahiert Metadaten aus Markdown-Dateien für WordPress-Import
|
||||
"""
|
||||
|
||||
import re
|
||||
import yaml
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
def extract_frontmatter(markdown_content: str) -> tuple[Optional[Dict[str, Any]], str]:
|
||||
"""
|
||||
Extrahiert YAML-Frontmatter und Markdown-Inhalt
|
||||
|
||||
Args:
|
||||
markdown_content: Vollständiger Markdown-Text
|
||||
|
||||
Returns:
|
||||
Tuple von (frontmatter_dict, markdown_body)
|
||||
"""
|
||||
# Regex für YAML-Frontmatter (zwischen --- Markern)
|
||||
frontmatter_pattern = r'^---\s*\n(.*?)\n---\s*\n(.*)$'
|
||||
match = re.match(frontmatter_pattern, markdown_content, re.DOTALL)
|
||||
|
||||
if not match:
|
||||
# Kein Frontmatter gefunden
|
||||
return None, markdown_content
|
||||
|
||||
frontmatter_text = match.group(1)
|
||||
markdown_body = match.group(2)
|
||||
|
||||
# YAML parsen
|
||||
try:
|
||||
frontmatter_data = yaml.safe_load(frontmatter_text)
|
||||
return frontmatter_data, markdown_body
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Warnung: Fehler beim Parsen des YAML-Frontmatters: {e}")
|
||||
return None, markdown_content
|
||||
|
||||
|
||||
def extract_wordpress_metadata(frontmatter: Dict[str, Any],
|
||||
default_author: str = "admin") -> Dict[str, Any]:
|
||||
"""
|
||||
Extrahiert WordPress-relevante Metadaten aus Frontmatter
|
||||
|
||||
Args:
|
||||
frontmatter: Geparste Frontmatter-Daten
|
||||
default_author: Fallback-Autor
|
||||
|
||||
Returns:
|
||||
Dictionary mit WordPress-Metadaten
|
||||
"""
|
||||
metadata = {}
|
||||
|
||||
# Titel extrahieren (verschiedene Felder möglich)
|
||||
# Priorität: title > name > (aus commonMetadata)
|
||||
if 'title' in frontmatter:
|
||||
metadata['title'] = frontmatter['title']
|
||||
elif 'name' in frontmatter:
|
||||
metadata['title'] = frontmatter['name']
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
metadata['title'] = common.get('name', '')
|
||||
|
||||
# Beschreibung/Excerpt extrahieren
|
||||
# Priorität: summary > description > (aus commonMetadata)
|
||||
if 'summary' in frontmatter:
|
||||
metadata['excerpt'] = frontmatter['summary']
|
||||
elif 'description' in frontmatter:
|
||||
metadata['excerpt'] = frontmatter['description']
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
metadata['excerpt'] = common.get('description', '')
|
||||
|
||||
# Bild extrahieren
|
||||
# Priorität: image > cover.image > (aus commonMetadata)
|
||||
if 'image' in frontmatter:
|
||||
metadata['featured_image'] = frontmatter['image']
|
||||
elif isinstance(frontmatter.get('cover'), dict):
|
||||
cover_image = frontmatter['cover'].get('image', '')
|
||||
if cover_image:
|
||||
metadata['featured_image'] = cover_image
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
if 'image' in common:
|
||||
metadata['featured_image'] = common['image']
|
||||
|
||||
# Tags extrahieren
|
||||
if 'tags' in frontmatter:
|
||||
tags = frontmatter['tags']
|
||||
if isinstance(tags, list):
|
||||
metadata['tags'] = tags
|
||||
elif isinstance(tags, str):
|
||||
metadata['tags'] = [t.strip() for t in tags.split(',')]
|
||||
|
||||
# Kategorien extrahieren (falls vorhanden)
|
||||
if 'categories' in frontmatter:
|
||||
categories = frontmatter['categories']
|
||||
if isinstance(categories, list):
|
||||
metadata['categories'] = categories
|
||||
elif isinstance(categories, str):
|
||||
metadata['categories'] = [c.strip() for c in categories.split(',')]
|
||||
|
||||
# Autor extrahieren
|
||||
if 'author' in frontmatter:
|
||||
author = frontmatter['author']
|
||||
if isinstance(author, list) and len(author) > 0:
|
||||
metadata['author'] = author[0]
|
||||
elif isinstance(author, str):
|
||||
metadata['author'] = author
|
||||
else:
|
||||
metadata['author'] = default_author
|
||||
elif isinstance(frontmatter.get('#staticSiteGenerator'), dict):
|
||||
static_gen = frontmatter['#staticSiteGenerator']
|
||||
if 'author' in static_gen:
|
||||
author = static_gen['author']
|
||||
if isinstance(author, list) and len(author) > 0:
|
||||
metadata['author'] = author[0]
|
||||
elif isinstance(author, str):
|
||||
metadata['author'] = author
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
if 'creator' in common:
|
||||
creator = common['creator']
|
||||
if isinstance(creator, list) and len(creator) > 0:
|
||||
first_creator = creator[0]
|
||||
if isinstance(first_creator, dict):
|
||||
given = first_creator.get('givenName', '')
|
||||
family = first_creator.get('familyName', '')
|
||||
metadata['author'] = f"{given} {family}".strip()
|
||||
|
||||
# Fallback für Autor
|
||||
if 'author' not in metadata:
|
||||
metadata['author'] = default_author
|
||||
|
||||
# Status extrahieren (falls vorhanden)
|
||||
if 'status' in frontmatter:
|
||||
metadata['status'] = frontmatter['status']
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
work_status = common.get('creativeWorkStatus', '').lower()
|
||||
if work_status == 'published':
|
||||
metadata['status'] = 'publish'
|
||||
elif work_status == 'draft':
|
||||
metadata['status'] = 'draft'
|
||||
|
||||
# Datum extrahieren (falls vorhanden)
|
||||
if 'date' in frontmatter:
|
||||
metadata['date'] = frontmatter['date']
|
||||
elif 'datePublished' in frontmatter:
|
||||
metadata['date'] = frontmatter['datePublished']
|
||||
elif isinstance(frontmatter.get('#commonMetadata'), dict):
|
||||
common = frontmatter['#commonMetadata']
|
||||
if 'datePublished' in common:
|
||||
metadata['date'] = common['datePublished']
|
||||
|
||||
return metadata
|
||||
|
||||
|
||||
def parse_markdown_with_metadata(markdown_content: str,
|
||||
default_author: str = "admin") -> Dict[str, Any]:
|
||||
"""
|
||||
Parst Markdown-Datei und extrahiert alle WordPress-relevanten Daten
|
||||
|
||||
Args:
|
||||
markdown_content: Vollständiger Markdown-Text
|
||||
default_author: Fallback-Autor
|
||||
|
||||
Returns:
|
||||
Dictionary mit 'metadata' und 'content' (Markdown-Body)
|
||||
"""
|
||||
frontmatter, markdown_body = extract_frontmatter(markdown_content)
|
||||
|
||||
result = {
|
||||
'content': markdown_body,
|
||||
'metadata': {}
|
||||
}
|
||||
|
||||
if frontmatter:
|
||||
result['metadata'] = extract_wordpress_metadata(frontmatter, default_author)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_base_url(url: str) -> str:
|
||||
"""
|
||||
Extrahiert die Basis-URL aus einer vollständigen URL
|
||||
Nützlich für relative Bild-Pfade
|
||||
|
||||
Args:
|
||||
url: Vollständige URL
|
||||
|
||||
Returns:
|
||||
Basis-URL (z.B. https://example.com/path/)
|
||||
"""
|
||||
parts = url.rsplit('/', 1)
|
||||
if len(parts) == 2:
|
||||
return parts[0] + '/'
|
||||
return url
|
||||
|
||||
|
||||
def resolve_relative_image_url(image_path: str, base_url: str) -> str:
|
||||
"""
|
||||
Löst relative Bild-URLs auf
|
||||
|
||||
Args:
|
||||
image_path: Bild-Pfad (relativ oder absolut)
|
||||
base_url: Basis-URL der Markdown-Datei
|
||||
|
||||
Returns:
|
||||
Absolute URL zum Bild
|
||||
"""
|
||||
# Wenn bereits absolute URL, zurückgeben
|
||||
if image_path.startswith('http://') or image_path.startswith('https://'):
|
||||
return image_path
|
||||
|
||||
# Relative URL auflösen
|
||||
if image_path.startswith('/'):
|
||||
# Absoluter Pfad auf dem Server
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(base_url)
|
||||
return f"{parsed.scheme}://{parsed.netloc}{image_path}"
|
||||
else:
|
||||
# Relativer Pfad
|
||||
return base_url + image_path
|
||||
28
posts.yaml.new
Normal file
28
posts.yaml.new
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# WordPress Import Konfiguration
|
||||
# Metadaten werden aus dem YAML-Frontmatter der Markdown-Dateien extrahiert
|
||||
|
||||
# Einfache URL-Liste (Metadaten aus Frontmatter)
|
||||
posts:
|
||||
- url: "https://example.com/artikel1.md"
|
||||
- url: "https://example.com/artikel2.md"
|
||||
- url: "https://raw.githubusercontent.com/user/repo/main/docs/post.md"
|
||||
|
||||
# Oder lokale Dateien
|
||||
- file: "content/beispiel-beitrag.md"
|
||||
|
||||
# Optional: Metadaten überschreiben
|
||||
- url: "https://example.com/artikel3.md"
|
||||
status: "publish" # Überschreibt Status aus Frontmatter
|
||||
categories: # Überschreibt Kategorien aus Frontmatter
|
||||
- "Zusätzliche Kategorie"
|
||||
|
||||
# Globale Einstellungen
|
||||
settings:
|
||||
default_status: "draft" # Fallback wenn nicht im Frontmatter
|
||||
default_author: "admin" # Fallback wenn nicht im Frontmatter
|
||||
skip_duplicates: true # Bestehende Beiträge überspringen
|
||||
skip_duplicate_media: true # Bestehende Medien überspringen
|
||||
markdown_extensions: # Markdown-Erweiterungen
|
||||
- extra
|
||||
- codehilite
|
||||
- toc
|
||||
275
workflow.py
275
workflow.py
|
|
@ -1,8 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
WordPress Import Workflow
|
||||
Liest Markdown-Dateien aus URLs oder lokalen Dateien und erstellt WordPress-Beiträge
|
||||
basierend auf einer YAML-Konfigurationsdatei.
|
||||
Liest Markdown-Dateien aus URLs oder lokalen Dateien und erstellt WordPress-Beiträge.
|
||||
Metadaten werden aus dem YAML-Frontmatter der Markdown-Dateien extrahiert.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
|
@ -14,6 +14,11 @@ from pathlib import Path
|
|||
from dotenv import load_dotenv
|
||||
from typing import Dict, Any, List, Optional
|
||||
from wordpress_api import WordPressAPI
|
||||
from markdown_parser import (
|
||||
parse_markdown_with_metadata,
|
||||
get_base_url,
|
||||
resolve_relative_image_url
|
||||
)
|
||||
|
||||
# Lade Umgebungsvariablen
|
||||
load_dotenv()
|
||||
|
|
@ -122,75 +127,110 @@ def process_featured_image(wp_api: WordPressAPI, image_path: str,
|
|||
def process_post(wp_api: WordPressAPI, post_config: Dict[str, Any],
|
||||
global_settings: Dict[str, Any]) -> Optional[int]:
|
||||
"""
|
||||
Verarbeitet einen einzelnen Beitrag aus der Konfiguration
|
||||
Verarbeitet einen einzelnen Beitrag aus der Konfiguration.
|
||||
Metadaten werden aus dem YAML-Frontmatter extrahiert.
|
||||
|
||||
Args:
|
||||
wp_api: WordPress API Client
|
||||
post_config: Beitrags-Konfiguration
|
||||
post_config: Beitrags-Konfiguration (kann nur URL enthalten)
|
||||
global_settings: Globale Einstellungen
|
||||
|
||||
Returns:
|
||||
Post-ID oder None
|
||||
"""
|
||||
title = post_config.get('title')
|
||||
if not title:
|
||||
print("Fehler: Titel fehlt in der Beitragskonfiguration")
|
||||
# URL oder Datei ermitteln
|
||||
source_url = post_config.get('url') or post_config.get('markdown_url')
|
||||
source_file = post_config.get('file') or post_config.get('markdown_file')
|
||||
|
||||
if not source_url and not source_file:
|
||||
print("Fehler: Keine URL oder Datei in der Beitragskonfiguration")
|
||||
return None
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Verarbeite Beitrag: {title}")
|
||||
if source_url:
|
||||
print(f"Verarbeite Markdown von URL: {source_url}")
|
||||
else:
|
||||
print(f"Verarbeite lokale Markdown-Datei: {source_file}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Markdown-Inhalt abrufen
|
||||
markdown_content = None
|
||||
if 'markdown_url' in post_config:
|
||||
print(f"Lade Markdown von URL: {post_config['markdown_url']}")
|
||||
markdown_content = download_markdown(post_config['markdown_url'])
|
||||
elif 'markdown_file' in post_config:
|
||||
print(f"Lese lokale Markdown-Datei: {post_config['markdown_file']}")
|
||||
markdown_content = read_local_markdown(post_config['markdown_file'])
|
||||
elif 'content' in post_config:
|
||||
markdown_content = post_config['content']
|
||||
base_url = None
|
||||
|
||||
if source_url:
|
||||
markdown_content = download_markdown(source_url)
|
||||
base_url = get_base_url(source_url)
|
||||
elif source_file:
|
||||
markdown_content = read_local_markdown(source_file)
|
||||
# Bei lokalen Dateien nehmen wir das Verzeichnis als Basis
|
||||
base_url = os.path.dirname(os.path.abspath(source_file)) + '/'
|
||||
|
||||
if not markdown_content:
|
||||
print(f"Fehler: Kein Inhalt für Beitrag '{title}'")
|
||||
print(f"Fehler: Konnte Markdown-Inhalt nicht laden")
|
||||
return None
|
||||
|
||||
# Markdown parsen und Metadaten extrahieren
|
||||
default_author = global_settings.get('default_author', 'admin')
|
||||
parsed = parse_markdown_with_metadata(markdown_content, default_author)
|
||||
|
||||
metadata = parsed['metadata']
|
||||
markdown_body = parsed['content']
|
||||
|
||||
# Titel prüfen
|
||||
title = metadata.get('title') or post_config.get('title')
|
||||
if not title:
|
||||
print("Fehler: Kein Titel gefunden (weder im Frontmatter noch in der Konfiguration)")
|
||||
return None
|
||||
|
||||
print(f"Titel: {title}")
|
||||
|
||||
# Markdown zu HTML konvertieren
|
||||
extensions = global_settings.get('markdown_extensions', ['extra', 'codehilite', 'toc'])
|
||||
html_content = markdown_to_html(markdown_content, extensions)
|
||||
html_content = markdown_to_html(markdown_body, extensions)
|
||||
|
||||
# Kategorien verarbeiten
|
||||
# Priorität: Frontmatter > post_config > global_settings
|
||||
category_ids = []
|
||||
if 'categories' in post_config:
|
||||
for cat_name in post_config['categories']:
|
||||
cat_id = wp_api.get_or_create_category(cat_name)
|
||||
if cat_id:
|
||||
category_ids.append(cat_id)
|
||||
categories_list = metadata.get('categories') or post_config.get('categories') or []
|
||||
|
||||
for cat_name in categories_list:
|
||||
cat_id = wp_api.get_or_create_category(cat_name)
|
||||
if cat_id:
|
||||
category_ids.append(cat_id)
|
||||
|
||||
# Tags verarbeiten
|
||||
tag_ids = []
|
||||
if 'tags' in post_config:
|
||||
for tag_name in post_config['tags']:
|
||||
tag_id = wp_api.get_or_create_tag(tag_name)
|
||||
if tag_id:
|
||||
tag_ids.append(tag_id)
|
||||
tags_list = metadata.get('tags') or post_config.get('tags') or []
|
||||
|
||||
for tag_name in tags_list:
|
||||
tag_id = wp_api.get_or_create_tag(tag_name)
|
||||
if tag_id:
|
||||
tag_ids.append(tag_id)
|
||||
|
||||
# Beitragsbild verarbeiten
|
||||
featured_media_id = None
|
||||
if 'featured_image' in post_config:
|
||||
featured_image = metadata.get('featured_image') or post_config.get('featured_image')
|
||||
|
||||
if featured_image:
|
||||
# Relative URLs auflösen
|
||||
if base_url and not featured_image.startswith('http'):
|
||||
featured_image = resolve_relative_image_url(featured_image, base_url)
|
||||
|
||||
skip_duplicate_media = global_settings.get('skip_duplicate_media', True)
|
||||
featured_media_id = process_featured_image(
|
||||
wp_api,
|
||||
post_config['featured_image'],
|
||||
featured_image,
|
||||
check_duplicate=skip_duplicate_media
|
||||
)
|
||||
|
||||
# Status
|
||||
status = post_config.get('status', global_settings.get('default_status', 'draft'))
|
||||
status = metadata.get('status') or post_config.get('status') or global_settings.get('default_status', 'draft')
|
||||
|
||||
# Excerpt
|
||||
excerpt = post_config.get('excerpt', '')
|
||||
excerpt = metadata.get('excerpt') or post_config.get('excerpt', '')
|
||||
|
||||
# Autor
|
||||
author_name = metadata.get('author') or post_config.get('author') or default_author
|
||||
|
||||
# Beitrag erstellen
|
||||
skip_duplicates = global_settings.get('skip_duplicates', True)
|
||||
|
|
@ -208,15 +248,180 @@ def process_post(wp_api: WordPressAPI, post_config: Dict[str, Any],
|
|||
return post_id
|
||||
|
||||
|
||||
def fetch_forgejo_repo_markdown_files(repo_url: str, branch: str = 'main') -> List[str]:
|
||||
"""
|
||||
Holt alle Markdown-URLs aus einem Forgejo-Repository
|
||||
|
||||
Args:
|
||||
repo_url: URL zum Repository (z.B. https://codeberg.org/user/repo)
|
||||
branch: Branch-Name (Standard: main)
|
||||
|
||||
Returns:
|
||||
Liste von URLs zu Markdown-Dateien
|
||||
"""
|
||||
# Forgejo/Gitea API endpoint
|
||||
# Format: https://codeberg.org/api/v1/repos/{owner}/{repo}/git/trees/{branch}?recursive=true
|
||||
|
||||
# URL parsen
|
||||
parts = repo_url.rstrip('/').split('/')
|
||||
if len(parts) < 2:
|
||||
print(f"Fehler: Ungültige Repository-URL: {repo_url}")
|
||||
return []
|
||||
|
||||
owner = parts[-2]
|
||||
repo = parts[-1]
|
||||
|
||||
# API-URL ermitteln
|
||||
if 'codeberg.org' in repo_url:
|
||||
api_base = 'https://codeberg.org/api/v1'
|
||||
elif 'gitea' in repo_url or 'forgejo' in repo_url:
|
||||
# Generischer Ansatz für selbst-gehostete Instanzen
|
||||
base_parts = repo_url.split('/')[:3]
|
||||
api_base = '/'.join(base_parts) + '/api/v1'
|
||||
else:
|
||||
print(f"Warnung: Unbekannte Forgejo-Instanz, versuche generischen API-Pfad")
|
||||
base_parts = repo_url.split('/')[:3]
|
||||
api_base = '/'.join(base_parts) + '/api/v1'
|
||||
|
||||
api_url = f"{api_base}/repos/{owner}/{repo}/git/trees/{branch}?recursive=true"
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
markdown_files = []
|
||||
for item in data.get('tree', []):
|
||||
if item['type'] == 'blob' and item['path'].endswith('.md'):
|
||||
# Raw-URL konstruieren
|
||||
raw_url = f"https://codeberg.org/{owner}/{repo}/raw/branch/{branch}/{item['path']}"
|
||||
markdown_files.append(raw_url)
|
||||
|
||||
return markdown_files
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Fehler beim Abrufen der Repository-Dateien: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def main():
|
||||
"""Hauptfunktion des Workflows"""
|
||||
|
||||
# Konfigurationsdatei laden
|
||||
config_file = sys.argv[1] if len(sys.argv) > 1 else 'posts.yaml'
|
||||
# Kommandozeilen-Argumente verarbeiten
|
||||
if len(sys.argv) > 1:
|
||||
arg = sys.argv[1]
|
||||
|
||||
# Prüfe ob es eine direkte URL ist
|
||||
if arg.startswith('http://') or arg.startswith('https://'):
|
||||
# Direkter URL-Modus
|
||||
print(f"Direkt-Modus: Verarbeite URL: {arg}")
|
||||
|
||||
# WordPress-Credentials
|
||||
wp_url = os.getenv('WORDPRESS_URL')
|
||||
wp_username = os.getenv('WORDPRESS_USERNAME')
|
||||
wp_password = os.getenv('WORDPRESS_APP_PASSWORD')
|
||||
|
||||
if not all([wp_url, wp_username, wp_password]):
|
||||
print("Fehler: WordPress-Credentials fehlen in .env-Datei")
|
||||
sys.exit(1)
|
||||
|
||||
wp_api = WordPressAPI(wp_url, wp_username, wp_password)
|
||||
|
||||
# Erstelle minimale Konfiguration
|
||||
post_config = {'url': arg}
|
||||
global_settings = {
|
||||
'default_status': 'draft',
|
||||
'default_author': 'admin',
|
||||
'skip_duplicates': True,
|
||||
'skip_duplicate_media': True
|
||||
}
|
||||
|
||||
try:
|
||||
post_id = process_post(wp_api, post_config, global_settings)
|
||||
if post_id:
|
||||
print(f"\n✅ Erfolgreich: Beitrag erstellt (ID: {post_id})")
|
||||
else:
|
||||
print(f"\n❌ Fehler beim Erstellen des Beitrags")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Fehler: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
return
|
||||
|
||||
# Prüfe ob es eine Forgejo-Repo-URL ist
|
||||
elif '--forgejo-repo' in sys.argv or '--repo' in sys.argv:
|
||||
repo_index = sys.argv.index('--forgejo-repo') if '--forgejo-repo' in sys.argv else sys.argv.index('--repo')
|
||||
if len(sys.argv) > repo_index + 1:
|
||||
repo_url = sys.argv[repo_index + 1]
|
||||
branch = sys.argv[repo_index + 2] if len(sys.argv) > repo_index + 2 else 'main'
|
||||
|
||||
print(f"Forgejo-Modus: Verarbeite Repository: {repo_url}")
|
||||
print(f"Branch: {branch}")
|
||||
|
||||
markdown_urls = fetch_forgejo_repo_markdown_files(repo_url, branch)
|
||||
|
||||
if not markdown_urls:
|
||||
print("Keine Markdown-Dateien im Repository gefunden")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\nGefundene Markdown-Dateien: {len(markdown_urls)}")
|
||||
|
||||
# WordPress-Credentials
|
||||
wp_url = os.getenv('WORDPRESS_URL')
|
||||
wp_username = os.getenv('WORDPRESS_USERNAME')
|
||||
wp_password = os.getenv('WORDPRESS_APP_PASSWORD')
|
||||
|
||||
if not all([wp_url, wp_username, wp_password]):
|
||||
print("Fehler: WordPress-Credentials fehlen in .env-Datei")
|
||||
sys.exit(1)
|
||||
|
||||
wp_api = WordPressAPI(wp_url, wp_username, wp_password)
|
||||
|
||||
global_settings = {
|
||||
'default_status': 'draft',
|
||||
'default_author': 'admin',
|
||||
'skip_duplicates': True,
|
||||
'skip_duplicate_media': True
|
||||
}
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for url in markdown_urls:
|
||||
try:
|
||||
post_config = {'url': url}
|
||||
post_id = process_post(wp_api, post_config, global_settings)
|
||||
if post_id:
|
||||
success_count += 1
|
||||
else:
|
||||
error_count += 1
|
||||
except Exception as e:
|
||||
print(f"Fehler bei der Verarbeitung von {url}: {e}")
|
||||
error_count += 1
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"ZUSAMMENFASSUNG")
|
||||
print(f"{'='*60}")
|
||||
print(f"Erfolgreich: {success_count}")
|
||||
print(f"Fehler: {error_count}")
|
||||
print(f"Gesamt: {len(markdown_urls)}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
return
|
||||
|
||||
# Sonst als Konfigurationsdatei behandeln
|
||||
config_file = arg
|
||||
else:
|
||||
config_file = 'posts.yaml'
|
||||
|
||||
# Konfigurationsdatei-Modus
|
||||
if not os.path.exists(config_file):
|
||||
print(f"Fehler: Konfigurationsdatei '{config_file}' nicht gefunden")
|
||||
print("Verwendung: python workflow.py [config.yaml]")
|
||||
print("\nVerwendung:")
|
||||
print(" python workflow.py [config.yaml] # YAML-Konfiguration")
|
||||
print(" python workflow.py <URL> # Einzelne Markdown-URL")
|
||||
print(" python workflow.py --repo <REPO_URL> [branch] # Forgejo-Repository")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Lade Konfiguration aus: {config_file}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue