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:
Jörg Lohrer 2025-10-01 08:10:09 +02:00
parent e3b19bb0df
commit 7a234be652
6 changed files with 880 additions and 180 deletions

View file

@ -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
View file

@ -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

View file

@ -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
![](nosTr-schrein.jpg)
**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
View 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
View 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

View file

@ -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}")