Dieses Tutorial zeigt, wie du sowohl Markdown-Dateien mit YAML-Frontmatter als auch JSON-Dateien zu Nostr Kind-30023 Events konvertierst und diese in das Nostr-Netzwerk postest. Das erweiterte Mapping unterstützt sowohl grundlegende als auch komplexe Bildungsmetadaten.
Das Projekt umfasst folgende Hauptskripte:
| Skript | Beschreibung |
|---|---|
parse.js |
Einfache Umwandlung von Markdown/YAML zu JSON |
adv-mapping.js |
Erweitertes Mapping von Markdown/YAML oder JSON zu Nostr-Events |
post.js |
Veröffentlichung von Nostr-Events an konfigurierte Relays |
fetch-and-post.js |
Abruf und Veröffentlichung von Remote-Markdown-Dateien |
bulk-fetch-post.js |
Bulk-Verarbeitung mehrerer Markdown-Dateien aus Git-Repositories |
-
Klone dieses Repository:
git clone https://github.com/edufeed-org/md2nostr-tool.git cd md2nostr-tool -
Installiere alle Abhängigkeiten:
npm install
-
Erstelle eine
.env-Datei basierend auf.env.example:cp .env.example .env
-
Bearbeite die
.env-Datei und füge deinen Nostr-Schlüssel und gewünschte Relays hinzu.
-
Erstelle eine Testdatei
test.mdmit YAML-Frontmatter:--- title: Test-Artikel summary: Eine kurze Zusammenfassung tags: - test - beispiel --- # Test-Inhalt Dies ist ein Testinhalt.
-
Konvertiere die Markdown-Datei zu einem Nostr-Event:
node adv-mapping.js test.md > test-mapped.json -
Veröffentliche das Event an die Relays:
node post.js test-mapped.json
Paket: npm gray-matter als Bibliothek für YAML -> JSON
install ( für .md nötig)
npm i gray-matter
const fs = require("fs");
const matter = require("gray-matter");
const file = fs.readFileSync("test.md", "utf8");
const parsed = matter(file);
console.log(JSON.stringify(event));
const fs = require("fs");- Lädt das Node-Core-Modul fs (Dateisystem). Damit kannst du Dateien lesen/schreiben.
const matter = require("gray-matter");- Lädt gray-matter (vorher
npm i gray-matter).matter()parst Frontmatter aus Strings.
const { data, content } = matter(fs.readFileSync("beispiel.md", "utf8"));fs.readFileSync("beispiel.md", "utf8"): Liest synchron die Datei als UTF-8-String (kein Buffer).matter(<string>): Trennt Frontmatter vom Body und gibt ein Objekt zurück.- Destructuring: Nimmt daraus direkt
data(YAML als Objekt) undcontent(Markdown-Body als String).
console.log(JSON.stringify({ ...data, content: content.trim() }, null, 2));- Baut ein neues Objekt:
...data(alle YAML-Felder) +content(Body, mittrim()führende/abschließende Leerzeilen weg). JSON.stringify(obj, null, 2): Pretty-Print mit 2 Leerzeichen Einrückung.console.log(...): Gibt das eine JSON-Objekt auf STDOUT aus.
node parse.js artikel.md > artikel-konvertiert.json| YAML-Feld | Nostr-Tag | Beschreibung |
|---|---|---|
url / id |
d |
Eindeutige Identifier für das Event |
name / title |
title |
Titel des Artikels |
summary / description |
summary |
Kurze Zusammenfassung (optional mit Sprache) |
image / cover.image / thumbnailUrl |
image |
URL zum Titelbild |
datePublished / published / date |
published_at |
Veröffentlichungsdatum (Unix-Timestamp) |
tags |
t |
Array von Tags (jeder als eigener t-Tag) |
content / body |
content |
Der Markdown-Inhalt |
| YAML-Feld | Nostr-Tag | Beschreibung |
|---|---|---|
about |
about |
Themen/Konzepte (URI, Label, Sprache) |
learningResourceType |
learningResourceType |
Art der Lernressource |
keywords |
keywords |
Schlüsselwörter (kompakt in einem Tag) |
inLanguage / language |
inLanguage |
Sprachen der Ressource |
license |
license |
Lizenzinformationen |
educationalLevel / educationLevel |
educationalLevel |
Bildungsniveau |
creator |
creator, creatorId, creatorAffiliation |
Autor/in-Informationen |
Das erweiterte Mapping-Skript adv-mapping.js bietet:
- Multi-Format-Support: Verarbeitet sowohl
.jsonals auch.mdDateien - Flexible Feldmapping: Unterstützt alternative Feldnamen (z.B.
nameodertitle) - Sprachunterstützung: Mehrsprachige Inhalte mit
inLanguage-Tags - Bildungsmetadaten: Spezialisierte Tags für Lernressourcen
- Robuste Verarbeitung: Fehlerbehandlung und Validierung
// adv-mapping.js
// Usage:
// node adv-mapping.js input.json
// node adv-mapping.js input.md
//
// Für .md: npm i gray-matter
const fs = require("fs");
let matter = null;
try { matter = require("gray-matter"); } catch {}
function parseInput(filePath) {
const raw = fs.readFileSync(filePath, "utf8");
const isMD = /\.md$/i.test(filePath);
if (!isMD) return JSON.parse(raw);
if (!matter) throw new Error("gray-matter fehlt. Bitte `npm i gray-matter` installieren.");
const fm = matter(raw);
const data = fm.data || {};
const content = (fm.content || "").trim();
return { ...data, content };
}
function mapTo30023(src = {}) {
if (!src || typeof src !== "object") return { error: "invalid input" };
const content = (src.content || src.body || "").toString().trim();
if (!content) return { error: "missing content" };
const now = Math.floor(Date.now() / 1000);
const tags = [];
// Flexible Feldmapping
const url = src.url || src.id;
const name = src.name || src.title;
const summary = src.summary || src.description;
// Grundlegende Tags
if (url) tags.push(["d", String(url)]);
if (name) tags.push(["title", String(name)]);
if (summary) tags.push(["summary", String(summary)]);
// Bild-URL aus verschiedenen Quellen
const image = src.image || src.cover?.image || src.thumbnailUrl;
if (image) tags.push(["image", String(image)]);
// Datum-Verarbeitung
const published = src.datePublished || src.published || src.date;
if (published) {
const ts = Math.floor(new Date(published).getTime() / 1000);
if (!Number.isNaN(ts)) tags.push(["published_at", String(ts)]);
}
// Standard-Tags
if (Array.isArray(src.tags)) {
for (const t of src.tags.filter(Boolean)) {
tags.push(["t", String(t)]);
}
}
// Erweiterte Bildungsmetadaten
// ... (weitere Mappings für about, learningResourceType, etc.)
return {
kind: 30023,
created_at: now,
content,
tags
};
}
// Hauptfunktion
const inputFile = process.argv[2] || "oeroep.json";
try {
const src = parseInput(inputFile);
const event = mapTo30023(src);
if (event.error) {
console.error(`Fehler: ${event.error}`);
process.exit(1);
}
console.log(JSON.stringify(event, null, 2));
} catch (err) {
console.error("Fehler beim Verarbeiten:", err.message);
process.exit(1);
}Das erweiterte Skript kann sowohl JSON- als auch Markdown-Dateien verarbeiten:
# JSON-Datei verarbeiten
node adv-mapping.js artikel-konvertiert.json > artikel-mapped.json
# Markdown-Datei direkt verarbeiten (benötigt gray-matter)
node adv-mapping.js artikel.md > artikel-mapped.json
# Mit verschiedenen Eingabedateien
node adv-mapping.js oeroep.json > oeroep-mapped.json
node adv-mapping.js beispiel.md > beispiel-mapped.jsonWorkflow-Optionen:
-
Direkter MD-zu-Nostr Workflow:
node adv-mapping.js artikel.md > artikel-mapped.json -
Traditioneller 2-Schritt Workflow:
node parse.js artikel.md > artikel-konvertiert.json node adv-mapping.js artikel-konvertiert.json > artikel-mapped.json
PrelayInit fehlt in deiner nostr-tools-Version. Nimm den einfachsten, versionssicheren Weg mit SimplePool (fire-and-forget) – keine ACKs, aber zuverlässig genug fürs „simpel posten“.
npm i nostr-tools ws dotenvDie dotenv-Bibliothek ermöglicht das Laden von Umgebungsvariablen aus einer .env-Datei, was sicherer als Umgebungsvariablen in der Befehlszeile ist.
Erstelle eine .env-Datei im Projektverzeichnis mit folgendem Inhalt:
# Nostr-Schlüssel (entweder nsec oder hex)
NSEC=nsec1...
# NOSTR_SK=dein_hex_secret_key # Alternative zu NSEC
# Komma-getrennte Liste der Relays
RELAYS=wss://relay-rpi.edufeed.org/,wss://relay.damus.io,wss://relay.primal.net/,wss://nostr.win
Die .env-Datei sollte niemals in die Versionskontrolle (Git) eingecheckt werden! Füge .env zu deiner .gitignore-Datei hinzu.
const fs = require("fs");
// dotenv-Konfiguration laden
require("dotenv").config();
const { finalizeEvent, getPublicKey, nip19, SimplePool } = require("nostr-tools");
// WebSocket-Impl für Node
global.WebSocket = require("ws");
// kleine Helper
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
(async () => {
try {
const inPath = process.argv[2] || "nostr-schrein.json";
// .env Datei nutzen oder Fallback-Werte
const relayList = (process.env.RELAYS || "wss://nos.lol,wss://relay.damus.io")
.split(",").map(s => s.trim()).filter(Boolean);
const secret = process.env.NSEC || process.env.NOSTR_SK; // nsec... oder hex
if (!secret) throw new Error("Bitte NSEC oder NOSTR_SK in .env-Datei oder als Umgebungsvariable setzen.");
const sk = secret.startsWith("nsec") ? nip19.decode(secret).data : secret;
const pk = getPublicKey(sk);
const src = JSON.parse(fs.readFileSync(inPath, "utf8"));
if (src.kind !== 30023) throw new Error("JSON muss kind: 30023 enthalten.");
if (!src.content) throw new Error("JSON braucht content.");
const unsigned = { ...src, pubkey: pk, created_at: src.created_at || Math.floor(Date.now()/1000) };
const ev = finalizeEvent(unsigned, sk); // id + sig
const pool = new SimplePool();
pool.publish(relayList, ev); // feuern …
// kurz warten, damit Verbindungen aufgehen und senden können
await sleep(2500);
pool.close(relayList);
console.log(JSON.stringify({ id: ev.id, relays: relayList, published: true }, null, 2));
} catch (e) {
console.error(JSON.stringify({ error: e.message }, null, 2));
process.exit(1);
}
})();Mit der .env-Datei musst du die Umgebungsvariablen nicht mehr in der Befehlszeile setzen:
# Einfache Nutzung mit .env-Datei
node post.js artikel-mapped.json
# Oder mit dem erweiterten Mapping direkt:
node adv-mapping.js artikel.md | node post.js /dev/stdinAlternativ kannst du die Umgebungsvariablen auch weiterhin direkt setzen, falls nötig:
export NSEC="nsec1..." # oder: export NOSTR_SK="<hex>"
export RELAYS="wss://relay-rpi.edufeed.org/,wss://relay.damus.io,wss://relay.primal.net/,wss://nostr.win"
node post.js artikel-mapped.jsonVorteile des dotenv-Ansatzes:
- Sicherere Speicherung von Schlüsseln (keine Befehlszeilenhistorie)
- Konsistente Relay-Konfiguration zwischen Ausführungen
- Einfacherer Workflow ohne Export-Befehle
- Keine
relayInit/Versionstricks nötig SimplePool.publish()existiert in allen verbreiteten Builds- Wir warten 2.5 s, damit die Sockets senden; dann schließen wir sauber
Mit dem fetch-and-post.js-Skript kannst du Markdown-Dateien direkt von einer URL abrufen, parsen und als Nostr-Event posten:
npm i axiosnode fetch-and-post.js https://raw.githubusercontent.com/user/repo/main/artikel.mdDieses Skript kombiniert alle vorherigen Funktionen:
- Abrufen der Markdown-Datei von einer URL
- Parsen des YAML-Frontmatters und Markdown-Inhalts
- Mapping zu einem Nostr-Kind-30023-Event
- Posten des Events an die konfigurierten Relays
Alle Konfigurationsparameter (nsec, Relays) werden aus der .env-Datei geladen.
Mit dem bulk-fetch-post.js-Skript können mehrere Markdown-Dateien aus einem Git-Repository automatisch abgerufen, geparst und als Nostr-Events veröffentlicht werden.
npm i axios dotenv gray-matter nostr-tools ws commander- Passe die Repository-Informationen im Skript an:
// Konfiguration für die Forgejo/Gitea API
const repoOwner = 'Dein-Repository-Besitzer';
const repoName = 'Dein-Repository-Name';
const branch = 'main';
const path = 'Pfad/zu/markdown/dateien';- Stelle sicher, dass deine
.env-Datei konfiguriert ist:
NSEC=nsec1...
RELAYS=wss://relay1,wss://relay2
# Für private Repositories
API_TOKEN=dein_api_token
# Standardausführung
node bulk-fetch-post.js
# Mit Optionen
node bulk-fetch-post.js --dry-run --delay 10000 --verbose--dry-run: Führt das Skript aus, ohne Events zu posten (zum Testen)--delay <ms>: Verzögerung zwischen Posts (Standard: 5000ms)--verbose: Ausführlichere Ausgabe während der Ausführung
Das Skript ist für Forgejo/Gitea-APIs optimiert, kann aber für andere Plattformen angepasst werden. Siehe die Kommentare im Skript für GitLab- und GitHub-Beispiele.