Compare commits

...

No commits in common. "hugo-archive" and "main" have entirely different histories.

542 changed files with 17858 additions and 18038 deletions

View File

@ -6,7 +6,8 @@
"Bash(git commit -m ':*)",
"Bash(git rm:*)",
"Bash(git commit:*)",
"Bash(git checkout:*)"
"Bash(git checkout:*)",
"Bash(git submodule:*)"
]
}
}

View File

@ -0,0 +1,180 @@
---
name: joerglohrerde-workflow
description: Repo-spezifischer Skill für joerglohrerde. Nutze ihn bei jedem Session-Start, um den aktuellen Zustand zu erfassen, Konventionen zu verstehen und wiederkehrende Workflows (Deploy, Publish, Tests) effizient auszuführen.
---
# joerglohrerde — Session-Skill
Dieses Repo ist die persönliche Webseite von Jörg Lohrer — in Transition
von Hugo zu einer dezentralen Nostr-basierten SvelteKit-SPA.
## Beim Session-Start IMMER zuerst
1. **Lies `docs/STATUS.md`** — aktueller Projektstand, live-URLs, Branches.
2. **Lies `docs/HANDOFF.md`** — was wartet, nächste Schritte, Stolperfallen.
3. Bei konkreten Aufgaben: entsprechende Spec unter `docs/superpowers/specs/`
oder Plan unter `docs/superpowers/plans/`.
4. Branch-Zustand prüfen: `git log --oneline -10 spa main hugo-archive`.
Dann erst Rückfragen oder Vorschläge formulieren.
## Drei Live-Webseiten
| URL | Inhalt | Wann anfassen |
|---|---|---|
| `joerg-lohrer.de` | Hugo-Seite (alt) | nur im finalen Cutover |
| `spa.joerg-lohrer.de` | Vanilla-Mini-Spike | als Referenz, aber nicht weiterentwickeln |
| `svelte.joerg-lohrer.de` | SvelteKit-SPA | **Haupt-Arbeitsziel** |
## Git-Branches
- `main` — kanonisch (Content, Specs, Pläne, Deploy-Scripts)
- `spa` — aktueller Arbeitszweig mit allen SvelteKit-Commits
- `hugo-archive` — Orphan, eingefrorener Hugo-Zustand
Specs und Pläne gehören auf `main`; SvelteKit-Code auf `spa`. Typischer
Workflow: committe Spec-Updates auf `main`, merge `main``spa` um
sie überall zu haben.
## Sprache und Ton
- Antworten und Commit-Messages auf **Deutsch** (Kundensprache).
- Code-Kommentare auch auf Deutsch (wenn überhaupt).
- Identifier, Variablen, Funktionen auf **Englisch**.
- Kurz und konkret — Jörg ist technisch versiert, erwartet keine
Grundlagen-Erklärungen.
## Kernkonventionen
### Kanonisches URL-Schema
- Post-URL ist **kurz**: `/<dtag>/` (z. B. `/dezentrale-oep-oer/`).
- Legacy-Hugo-URLs `/YYYY/MM/DD/<dtag>.html/` werden per SvelteKit-Load
auf die kurze Form 301-redirected (Backlink-Kompatibilität).
- Tag-Route: `/tag/<name>/`.
### Slug-Regel
Alle Slugs sind lowercase (Frontmatter `slug:`). Commit `d17410f` hat das
normalisiert. Keine Runtime-Transformation, beim Publishen 1:1 übernehmen.
### Nostr-Konstanten
- Pubkey (hex): `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41`
- npub: `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9`
- Bootstrap-Relay: `wss://relay.damus.io`
- Vollständige Relay-Liste: aus `kind:10002` des Autors (on-the-fly).
- Blossom-Server: aus `kind:10063` des Autors.
Zentralisiert in `app/src/lib/nostr/config.ts`.
### Signing
- **Im Browser (Kommentare):** NIP-07 via Extension (Alby, nos2x).
- **Aus der Kommandozeile (Publish):** NIP-46 via Amber-Bunker. Bunker-URL
in `.env.local` als `BUNKER_URL`.
- Privater Schlüssel **nie** im Repo, nie in CI-Secrets, nie in einer
Pipeline-Umgebung direkt.
## Wiederkehrende Kommandos
### SPA-Entwicklung
```sh
cd app
npm run dev # Dev-Server localhost:5173
npm run check # Type-Check (sollte 0 errors sein)
npm run test:unit # Vitest — aktuell 29 Tests
npm run test:e2e # Playwright — aktuell 3 Tests
npm run build # Prod-Build nach app/build/
```
### Deploy nach `svelte.joerg-lohrer.de`
```sh
cd app && npm run build && cd ..
./scripts/deploy-svelte.sh
```
Das Script:
- liest `SVELTE_FTP_*` aus `.env.local`
- uploaded `app/build/*` per FTPS (TLS 1.2-Cap wegen All-Inkl-Bug)
- checkt `HTTP/2 200` am Ende
### Manuelles Publishen eines Posts (bis Publish-Pipeline fertig ist)
Siehe `docs/HANDOFF.md` Abschnitt „Manuelles Publishen". Kurz:
- Body aus Markdown-Frontmatter extrahieren (awk-Pattern dort)
- Bilder zu Blossom: `nak blossom upload --server https://blossom.edufeed.org --sec "$BUNKER_URL" <bild>`
- Event bauen mit `nak event -k 30023 -d <slug> -t title=... ...`
- Push zu allen Relays
### Nostr-Status checken
```sh
# Alle publizierten kind:30023-Events des Autors
nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io 2>/dev/null | jq -c '{d: (.tags[] | select(.[0]=="d") | .[1]), title: (.tags[] | select(.[0]=="title") | .[1])}'
# kind:10002 (Relay-Liste)
nak req -k 10002 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io
# kind:10063 (Blossom-Liste)
nak req -k 10063 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.damus.io
```
## Tech-Stack-Eigenheiten, die man kennen muss
1. **Svelte 5 Runes:** `$props()`-Werte müssen via `$derived()` in lokale
Variablen abgeleitet werden — sonst `state_referenced_locally`-Warning.
2. **applesauce-relay v5.x API:** RxJS-basiert. `pool.request(relays, filter)`
liefert `Observable<NostrEvent>`. Die Loader in `app/src/lib/nostr/loaders.ts`
nutzen `toArray() + lastValueFrom + timeout + catchError`-Pattern.
**Nicht** das Tupel-Pattern `msg[0] === 'EVENT'` — das gehört in
alte nostr-tools-Beispiele, nicht hierher.
3. **DOMPurify braucht DOM:** im `renderMarkdown`-Helper gibt es einen
Early-Fail-Guard für Node-Aufrufe (SSR ist ohnehin aus).
4. **All-Inkl-FTPS-Bug:** Data-Connection bricht bei TLS 1.3 ab.
`--tls-max 1.2` im curl-Call. Sobald SSH auf All-Inkl verfügbar ist
(Premium-Tarif angefragt), wird das Deploy-Script auf rsync umgestellt.
5. **Amber-Bunker-Session:** bei neuer Bunker-URL müssen globale
Permissions in Amber zurückgesetzt werden. Sonst hängt `nak event`
auf die Signatur-Response.
## Was nicht in Scope ist (laut Plan/Specs)
- Impressum-Inhalt (rechtliche Texte)
- Meta-Stubs pro Post (kommt via Publish-Pipeline Phase 3)
- Menü-Navigation (einfach nachrüstbar, aber nicht priorisiert)
- Eigener Relay (ideologischer Evolutionspfad, nicht Phase 1)
- Eigener Blossom-Server (dito)
## Wie mit Jörg arbeiten
- **Kurze Antworten**, konkrete Optionen, nicht lang umherreden.
- Bei mehreren Wegen: 23 Varianten mit Empfehlung nennen, nicht alles
aufzählen.
- Commit-Nachrichten: imperativ, auf Deutsch, mit Kontext im Body.
Co-Author: `Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>`.
- Vor dem Dispatchen von Subagents: kritische API-Details der Libraries
manuell verifizieren (Plan-Annahmen können alte Versionsstände
widerspiegeln). Beispiel: applesauce-relay API war nicht so wie im Plan
beschrieben — Subagent mit aktueller API briefen statt blind vertrauen.
- Nach jedem Feature-Commit: Build + Deploy, damit Jörg live sehen kann.
Das ist in diesem Workflow wichtig, weil UI-Feedback oft Layout-Fragen
aufwirft, die kein Test entdeckt.
## Credentials / Secrets
Alle in `.env.local` (gitignored). Variablen:
- `BUNKER_URL` — Amber-NIP-46-Pairing für Signaturen
- `SPA_FTP_HOST/USER/PASS/REMOTE_PATH` — FTPS nach spa.joerg-lohrer.de
- `SVELTE_FTP_HOST/USER/PASS/REMOTE_PATH` — FTPS nach svelte.joerg-lohrer.de
Falls neue Bunker-URL nötig (Amber-Session kaputt):
- In Amber neue Bunker-URL generieren
- In `.env.local` ersetzen
- In Amber globale Permissions für die App löschen, sonst hängt der Request

57
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: Publish Nostr Events
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
description: 'Publish all posts (--force-all)'
type: boolean
default: false
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Pre-Flight Check
working-directory: ./publish
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
BOOTSTRAP_RELAY: ${{ secrets.BOOTSTRAP_RELAY }}
CLIENT_SECRET_HEX: ${{ secrets.CLIENT_SECRET_HEX }}
run: |
deno run --allow-env --allow-read --allow-net src/cli.ts check
- name: Publish
working-directory: ./publish
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
BOOTSTRAP_RELAY: ${{ secrets.BOOTSTRAP_RELAY }}
CLIENT_SECRET_HEX: ${{ secrets.CLIENT_SECRET_HEX }}
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
run: |
if [ "${{ github.event.inputs.force_all }}" = "true" ]; then
deno run --allow-env --allow-read --allow-write=./logs --allow-net --allow-run=git src/cli.ts publish --force-all
else
deno run --allow-env --allow-read --allow-write=./logs --allow-net --allow-run=git src/cli.ts publish
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: publish-log
path: ./publish/logs/publish-*.json
retention-days: 30

3
.gitignore vendored
View File

@ -1 +1,4 @@
**/.DS_Store
.env
.env.local
logs/

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "themes/tailwind"]
path = themes/tailwind
url = https://github.com/tomowang/hugo-theme-tailwind.git

View File

View File

@ -1,3 +1,97 @@
# joerglohrerde
# joerg-lohrer.de
update
Persönliche Webseite. Nach einer Transition von einer Hugo-basierten,
statischen Seite läuft `joerg-lohrer.de` jetzt als SvelteKit-SPA, die
Blog-Posts live aus signierten Nostr-Events (NIP-23, `kind:30023`) rendert.
## Aktueller Stand
- **`https://joerg-lohrer.de/`** — SvelteKit-SPA, Cutover am 2026-04-18 erfolgt.
- **`https://staging.joerg-lohrer.de/`** — Staging (gleicher Build, ein Schritt vor Prod).
- **`https://svelte.joerg-lohrer.de/`** — Entwicklungs-Deploy-Target der Pipeline.
- **`https://spa.joerg-lohrer.de/`** — Vanilla-HTML-Mini-Spike (Proof of Concept, historisch).
Detailliert in [`docs/STATUS.md`](docs/STATUS.md).
## Wie die Seite funktioniert
1. **Inhalte** liegen als Markdown in `content/posts/<slug>/index.md` mit
strukturierten Bild-Metadaten im Frontmatter (Alt-Text, Lizenz, Autor:innen).
2. **Publish-Pipeline** (`publish/`, Deno) lädt Bilder auf Blossom-Server
(content-addressed) und publiziert signierte `kind:30023`-Events via
NIP-46-Bunker (Amber) auf 5 Relays.
3. **SvelteKit-SPA** (`app/`) lädt diese Events zur Laufzeit und rendert
Post-Liste + Detailseiten. Keine Server-Komponente, Static-Hosting reicht.
4. **CI**: GitHub Actions triggert die Publish-Pipeline bei Push auf `main`
(via Forgejo→GitHub Push-Mirror).
Identität und Assets:
- **Pubkey:** `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9`
- **NIP-05:** `joerglohrer@joerg-lohrer.de` (statisches `.well-known/nostr.json`)
- **Blossom-Server:** `blossom.edufeed.org`, `blossom.primal.net`
- **Relays:** `relay.damus.io`, `nos.lol`, `relay.primal.net`, `relay.tchncs.de`, `relay.edufeed.org`
## Navigation
- 📍 **Stand und Live-URLs:** [`docs/STATUS.md`](docs/STATUS.md)
- 🔜 **Wie es weitergeht:** [`docs/HANDOFF.md`](docs/HANDOFF.md)
- 📐 **SPA-Spec:** [`docs/superpowers/specs/2026-04-15-nostr-page-design.md`](docs/superpowers/specs/2026-04-15-nostr-page-design.md)
- 📐 **Publish-Pipeline-Spec:** [`docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`](docs/superpowers/specs/2026-04-15-publish-pipeline-design.md)
- 📐 **Bild-Metadaten-Konvention:** [`docs/superpowers/specs/2026-04-16-image-metadata-convention.md`](docs/superpowers/specs/2026-04-16-image-metadata-convention.md)
- 🛠 **SvelteKit-SPA-Plan:** [`docs/superpowers/plans/2026-04-15-spa-sveltekit.md`](docs/superpowers/plans/2026-04-15-spa-sveltekit.md) (35 Tasks, abgeschlossen)
- 🛠 **Publish-Pipeline-Plan:** [`docs/superpowers/plans/2026-04-16-publish-pipeline.md`](docs/superpowers/plans/2026-04-16-publish-pipeline.md) (24 Tasks, abgeschlossen)
- 🤖 **Claude-Workflow-Skill:** [`.claude/skills/joerglohrerde-workflow.md`](.claude/skills/joerglohrerde-workflow.md)
## Branches
- **`main`** — kanonisch. Seit Cutover (2026-04-18) Produktions-Quelle.
- **`spa`** — historischer SvelteKit-Arbeitszweig, inzwischen gemerged.
- **`hugo-archive`** — eingefrorener Hugo-Zustand als Orphan-Branch.
Rollback-Option über `git checkout hugo-archive && hugo build`.
## Repo-Struktur
```
content/posts/ Markdown-Posts (Quelle für Nostr-Events, 18 Stück)
content/impressum.md Statisches Impressum (wird von SPA geladen)
app/ SvelteKit-SPA (Laufzeit-Renderer)
publish/ Deno-Publish-Pipeline (Blossom + Nostr)
preview/spa-mini/ Vanilla-HTML-Mini-Spike (historische Referenz)
scripts/deploy-svelte.sh FTPS-Deploy, Targets: svelte/staging/prod
static/ Site-Assets (Favicons, Profilbild, .well-known/)
docs/ Specs, Pläne, Status, Handoff, Wiki-Entwürfe
.github/workflows/ GitHub-Actions CI (Publish-Pipeline-Trigger)
.claude/ Claude-Code-Sessions (Transparenz) + Skills
```
## Entwicklung
```sh
# SPA lokal
cd app && npm run dev
# SPA testen
cd app && npm run test:unit
cd app && npm run test:e2e
cd app && npm run check
# Publish-Pipeline
cd publish && deno task check # pre-flight
cd publish && deno task publish --dry-run # Simulation
cd publish && deno task publish # diff-modus echt
cd publish && deno task publish --post <slug> # ein Post
cd publish && deno task test # Tests
# Deploy
DEPLOY_TARGET=staging ./scripts/deploy-svelte.sh
DEPLOY_TARGET=prod ./scripts/deploy-svelte.sh
```
## Lizenz
Inhalte: [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/deed.de)
(Namensnennung erwünscht, aber rechtlich nicht erforderlich), sofern nicht
anders vermerkt. Drittinhalte sind beim jeweiligen Bild mit Autor:innen und
Lizenz gekennzeichnet.
Code: siehe [LICENSE](LICENSE).

9
app/.env.example Normal file
View File

@ -0,0 +1,9 @@
# Öffentliche Site-URL für Canonical-Link und og:url-Meta-Tags.
# Zur Build-Zeit fest; gilt domain-übergreifend (svelte./staging./haupt-
# domain). Für jeden Deploy-Zweck kann eine andere URL gesetzt werden.
#
# Beispiele:
# PUBLIC_SITE_URL=https://svelte.joerg-lohrer.de
# PUBLIC_SITE_URL=https://staging.joerg-lohrer.de
# PUBLIC_SITE_URL=https://joerg-lohrer.de
PUBLIC_SITE_URL=https://joerg-lohrer.de

28
app/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# npm
package-lock.json
*.log
test-results/

1
app/.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

3
app/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode"]
}

42
app/README.md Normal file
View File

@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv@0.15.1 create --template minimal --types ts --install npm .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

42
app/package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "app",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest run",
"test:e2e": "playwright test",
"deploy:svelte": "../scripts/deploy-svelte.sh"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.57.0",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@testing-library/svelte": "^5.3.1",
"@types/dompurify": "^3.0.5",
"jsdom": "^29.0.2",
"svelte": "^5.55.2",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.7",
"vitest": "^4.1.4"
},
"dependencies": {
"applesauce-core": "^5.2.0",
"applesauce-loaders": "^5.1.0",
"applesauce-relay": "^5.2.0",
"applesauce-signers": "^5.2.0",
"dompurify": "^3.4.0",
"highlight.js": "^11.11.1",
"marked": "^18.0.0",
"nostr-tools": "^2.23.3",
"rxjs": "^7.8.2"
}
}

13
app/playwright.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: 'tests/e2e',
use: { baseURL: 'http://localhost:5173' },
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: true,
timeout: 120_000
},
timeout: 60_000
});

13
app/src/app.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

66
app/src/app.html Normal file
View File

@ -0,0 +1,66 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Jörg Lohrer Blog (Nostr-basiert)" />
<meta property="og:title" content="Jörg Lohrer Blog" />
<meta property="og:type" content="website" />
<meta property="og:url" content="__SITE_URL__/" />
<meta property="og:description" content="Jörg Lohrer Blog (Nostr-basiert)" />
<link rel="canonical" href="__SITE_URL__/" />
<meta name="robots" content="index, follow" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="192x192" href="/android-chrome-192x192.png" />
<link rel="icon" type="image/png" sizes="512x512" href="/android-chrome-512x512.png" />
<title>Jörg Lohrer</title>
<style>
:root {
--fg: #1f2937;
--muted: #6b7280;
--bg: #fafaf9;
--accent: #2563eb;
--code-bg: #f3f4f6;
--border: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--fg: #e5e7eb;
--muted: #9ca3af;
--bg: #18181b;
--accent: #60a5fa;
--code-bg: #27272a;
--border: #3f3f46;
}
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
padding: 0;
}
body {
font:
17px/1.55 -apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif;
color: var(--fg);
background: var(--bg);
}
a {
color: var(--accent);
}
</style>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,47 @@
<script lang="ts">
// CC-Zero-Badge: kombination aus CC-Heart + Zero-Logo, monochrom via
// currentColor. Icons aus dem offiziellen CC-Press-Kit
// (creativecommons.org/mission/branding/). Inline hier, weil statische
// svg-imports mit ?raw in vite problematisch sind.
</script>
<span class="cc-badge" aria-hidden="true">
<!-- CC-Heart (vereinfachtes herz aus dem offiziellen logo) -->
<svg viewBox="0 0 46296 40689" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M23204.91 7530.98c2944.63,-3188.84 6384.04,-4639.01 10366.38,-4077.21 4110.34,579.88 7609.97,3518.41 8854.17,7479.01 957.39,3047.58 559.96,6460.83 -722.09,9573.35 -1993.98,4840.97 -7886.31,11722.09 -18555.24,16532.85 -10668.92,-4810.76 -16561.25,-11691.88 -18555.24,-16532.85 -1282.05,-3112.52 -1679.47,-6525.77 -722.09,-9573.35 1244.19,-3960.6 4743.83,-6899.13 8854.17,-7479.01 3982.46,-561.82 7421.94,888.46 10366.64,4077.48 5.4,5.84 56.52,61.37 56.53,61.36 0.04,0.04 51.9,-56.36 56.79,-61.63zm-56.79 -4522.44c-6431.69,-5048.01 -16512.25,-3730.83 -21147.65,3855.94 -1539.08,2519.03 -2117.14,5447.75 -1981.3,8355.45 235.64,5043.59 2412.75,9452.27 5610.61,13256.78 4306.02,5122.9 10531.26,9148.59 17382.21,12152.72 9.53,4.18 88.63,38.56 136.13,59.69 41.66,-17.53 114.6,-50.41 137.01,-60.3 6815.65,-3004.07 13075.56,-7030.12 17381.33,-12152.12 3198.08,-3804.33 5374.97,-8213.2 5610.61,-13256.78 135.85,-2907.7 -442.2,-5836.43 -1981.3,-8355.45 -4635.4,-7586.77 -14715.95,-8903.95 -21147.65,-3855.94z"
/>
<path
fill="currentColor"
d="M22983.64 21630.19l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.85,-2300.18 -2239.67,-3843.76 -87.17,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.75,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.13,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.43,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12zm13802.46 0l-2928.01 -1451.38c-1017.73,1483.99 -1758.21,2488.33 -3897.08,1902.25 -1678.91,-460.05 -2175.86,-2300.18 -2239.67,-3843.76 -87.18,-2108.39 649.94,-4543.46 3168.15,-4413.24 1609.13,83.19 2294.74,1032.23 2661.15,1885.36l3196.99 -1638.9c-1574.75,-3004.31 -5265.14,-4026.05 -8393.32,-3188.81 -3328.66,890.9 -5014.61,3952.95 -4955.5,7255.23 60.42,3375.58 1680.8,6291.51 5161.55,7052.54 1697.16,371.06 3545.13,284.81 5116.74,-503.18 1216.27,-609.83 2567.56,-1786.86 3109,-3056.12z"
/>
</svg>
<!-- CC-Zero (kreis + 0 aus dem cc-0-logo) -->
<svg viewBox="-0.5 0.5 64 64" xmlns="http://www.w3.org/2000/svg">
<path
fill="currentColor"
d="M31.5,14.08c-10.565,0-13.222,9.969-13.222,18.42c0,8.452,2.656,18.42,13.222,18.42c10.564,0,13.221-9.968,13.221-18.42C44.721,24.049,42.064,14.08,31.5,14.08z M31.5,21.026c0.429,0,0.82,0.066,1.188,0.157c0.761,0.656,1.133,1.561,0.403,2.823l-7.036,12.93c-0.216-1.636-0.247-3.24-0.247-4.437C25.808,28.777,26.066,21.026,31.5,21.026z M36.766,26.987c0.373,1.984,0.426,4.056,0.426,5.513c0,3.723-0.258,11.475-5.69,11.475c-0.428,0-0.822-0.045-1.188-0.136c-0.07-0.021-0.134-0.043-0.202-0.067c-0.112-0.032-0.23-0.068-0.336-0.11c-1.21-0.515-1.972-1.446-0.874-3.093L36.766,26.987z"
/>
<path
fill="currentColor"
d="M31.433,0.5c-8.877,0-16.359,3.09-22.454,9.3c-3.087,3.087-5.443,6.607-7.082,10.532C0.297,24.219-0.5,28.271-0.5,32.5c0,4.268,0.797,8.32,2.397,12.168c1.6,3.85,3.921,7.312,6.969,10.396c3.085,3.049,6.549,5.399,10.398,7.037c3.886,1.602,7.939,2.398,12.169,2.398c4.229,0,8.34-0.826,12.303-2.465c3.962-1.639,7.496-3.994,10.621-7.081c3.011-2.933,5.289-6.297,6.812-10.106C62.73,41,63.5,36.883,63.5,32.5c0-4.343-0.77-8.454-2.33-12.303c-1.562-3.885-3.848-7.32-6.857-10.33C48.025,3.619,40.385,0.5,31.433,0.5z M31.567,6.259c7.238,0,13.412,2.566,18.554,7.709c2.477,2.477,4.375,5.31,5.67,8.471c1.296,3.162,1.949,6.518,1.949,10.061c0,7.354-2.516,13.454-7.506,18.33c-2.592,2.516-5.502,4.447-8.74,5.781c-3.2,1.334-6.498,1.994-9.927,1.994c-3.468,0-6.788-0.653-9.949-1.948c-3.163-1.334-6.001-3.238-8.516-5.716c-2.515-2.514-4.455-5.353-5.826-8.516c-1.333-3.199-2.017-6.498-2.017-9.927c0-3.467,0.684-6.787,2.017-9.949c1.371-3.2,3.312-6.074,5.826-8.628C18.092,8.818,24.252,6.259,31.567,6.259z"
/>
</svg>
</span>
<style>
.cc-badge {
display: inline-flex;
align-items: center;
gap: 0.15em;
color: var(--accent);
vertical-align: -0.2em;
}
.cc-badge svg {
width: 1.1em;
height: 1.1em;
display: block;
}
</style>

View File

@ -0,0 +1,56 @@
<script lang="ts">
import { externalClientLinks } from '$lib/nostr/naddr';
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
interface Props {
dtag: string;
}
let { dtag }: Props = $props();
const links = $derived(
externalClientLinks({
pubkey: AUTHOR_PUBKEY_HEX,
kind: 30023,
identifier: dtag
})
);
</script>
<section class="external">
<span class="label">In Nostr-Client öffnen (für Threads, Reactions, Teilen):</span>
<ul>
{#each links as l}
<li><a href={l.url} target="_blank" rel="noopener">{l.label}</a></li>
{/each}
</ul>
</section>
<style>
.external {
margin: 2rem 0 1rem;
padding: 0.8rem 1rem;
background: var(--code-bg);
border-radius: 4px;
font-size: 0.9rem;
}
.label {
display: block;
color: var(--muted);
margin-bottom: 0.4rem;
}
ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-wrap: wrap;
gap: 0.8rem;
}
li a {
color: var(--accent);
text-decoration: none;
}
li a:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
interface Props {
loading: boolean;
error: string | null;
hablaLink?: string;
}
let { loading, error, hablaLink }: Props = $props();
</script>
{#if loading && !error}
<p class="status">Lade von Nostr-Relays …</p>
{:else if error}
<p class="status status-error">
{error}
{#if hablaLink}
<br />
<a href={hablaLink} target="_blank" rel="noopener"> In Habla.news öffnen </a>
{/if}
</p>
{/if}
<style>
.status {
padding: 1rem;
border-radius: 4px;
background: var(--code-bg);
color: var(--muted);
text-align: center;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
@media (prefers-color-scheme: dark) {
.status-error {
background: #450a0a;
color: #fca5a5;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script lang="ts">
import type { NostrEvent } from '$lib/nostr/loaders';
import { canonicalPostPath } from '$lib/url/legacy';
interface Props {
event: NostrEvent;
}
let { event }: Props = $props();
function tagValue(e: NostrEvent, name: string): string {
return e.tags.find((t) => t[0] === name)?.[1] ?? '';
}
const dtag = $derived(tagValue(event, 'd'));
const title = $derived(tagValue(event, 'title') || '(ohne Titel)');
const summary = $derived(tagValue(event, 'summary'));
const image = $derived(tagValue(event, 'image'));
const publishedAt = $derived(
parseInt(tagValue(event, 'published_at') || `${event.created_at}`, 10)
);
const date = $derived(
new Date(publishedAt * 1000).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
);
const href = $derived(canonicalPostPath(dtag));
</script>
<a class="card" {href}>
<div
class="thumb"
style:background-image={image ? `url('${image}')` : undefined}
aria-hidden="true"
></div>
<div class="text">
<div class="meta">{date}</div>
<h2>{title}</h2>
{#if summary}<p class="excerpt">{summary}</p>{/if}
</div>
</a>
<style>
.card {
display: flex;
gap: 1rem;
padding: 1rem 0;
border-bottom: 1px solid var(--border);
color: inherit;
text-decoration: none;
align-items: flex-start;
}
.card:hover {
background: var(--code-bg);
}
.thumb {
flex: 0 0 120px;
aspect-ratio: 1 / 1;
border-radius: 4px;
background: var(--code-bg) center/cover no-repeat;
}
.text {
flex: 1;
min-width: 0;
}
h2 {
margin: 0 0 0.3rem;
font-size: 1.2rem;
color: var(--fg);
word-wrap: break-word;
}
.excerpt {
color: var(--muted);
font-size: 0.95rem;
margin: 0;
}
.meta {
font-size: 0.85rem;
color: var(--muted);
margin-bottom: 0.2rem;
}
@media (max-width: 479px) {
.card {
flex-direction: column;
gap: 0.5rem;
}
.thumb {
flex: 0 0 auto;
width: 100%;
aspect-ratio: 2 / 1;
}
}
</style>

View File

@ -0,0 +1,168 @@
<script lang="ts">
import type { NostrEvent } from '$lib/nostr/loaders';
import type { SignedEvent } from '$lib/nostr/signer';
import { renderMarkdown } from '$lib/render/markdown';
import Reactions from './Reactions.svelte';
import ReplyList from './ReplyList.svelte';
import ReplyComposer from './ReplyComposer.svelte';
import ExternalClientLinks from './ExternalClientLinks.svelte';
interface Props {
event: NostrEvent;
}
let { event }: Props = $props();
function tagValue(e: NostrEvent, name: string): string {
return e.tags.find((t) => t[0] === name)?.[1] ?? '';
}
function tagsAll(e: NostrEvent, name: string): string[] {
return e.tags.filter((t) => t[0] === name).map((t) => t[1]);
}
const dtag = $derived(tagValue(event, 'd'));
const title = $derived(tagValue(event, 'title') || '(ohne Titel)');
const summary = $derived(tagValue(event, 'summary'));
const image = $derived(tagValue(event, 'image'));
const publishedAt = $derived(
parseInt(tagValue(event, 'published_at') || `${event.created_at}`, 10)
);
const date = $derived(
new Date(publishedAt * 1000).toLocaleDateString('de-DE', {
year: 'numeric',
month: 'long',
day: 'numeric'
})
);
const tags = $derived(tagsAll(event, 't'));
const bodyHtml = $derived(renderMarkdown(event.content));
// Optimistisch gesendete Replies: der Composer pusht sie rein,
// ReplyList merged sie mit den vom Relay geladenen Replies (dedup per id).
let optimisticReplies: NostrEvent[] = $state([]);
function handlePublished(signed: SignedEvent) {
optimisticReplies = [...optimisticReplies, signed as unknown as NostrEvent];
}
$effect(() => {
document.title = `${title} Jörg Lohrer`;
});
</script>
<h1 class="post-title">{title}</h1>
<div class="meta">
Veröffentlicht am {date}
{#if tags.length > 0}
<div class="tags">
{#each tags as t}
<a class="tag" href="/tag/{encodeURIComponent(t)}/">{t}</a>
{/each}
</div>
{/if}
</div>
{#if image}
<p class="cover"><img src={image} alt="Cover-Bild" /></p>
{/if}
{#if summary}
<p class="summary">{summary}</p>
{/if}
<article>{@html bodyHtml}</article>
{#if dtag}
<Reactions {dtag} />
<ExternalClientLinks {dtag} />
<ReplyComposer {dtag} eventId={event.id} onPublished={handlePublished} />
<ReplyList {dtag} optimistic={optimisticReplies} />
{/if}
<style>
.post-title {
font-size: 1.5rem;
line-height: 1.25;
margin: 0 0 0.4rem;
word-wrap: break-word;
}
@media (min-width: 640px) {
.post-title {
font-size: 2rem;
line-height: 1.2;
}
}
.meta {
color: var(--muted);
font-size: 0.92rem;
margin-bottom: 2rem;
}
.tags {
margin-top: 0.4rem;
}
.tag {
display: inline-block;
background: var(--code-bg);
border-radius: 3px;
padding: 1px 7px;
margin: 0 4px 4px 0;
font-size: 0.85em;
color: var(--fg);
text-decoration: none;
}
.tag:hover {
background: var(--border);
}
.cover {
max-width: 480px;
margin: 1rem auto 1.5rem;
}
.cover img {
display: block;
width: 100%;
height: auto;
border-radius: 4px;
}
.summary {
font-style: italic;
color: var(--muted);
}
article :global(img) {
max-width: 100%;
height: auto;
border-radius: 4px;
}
article :global(a) {
color: var(--accent);
word-break: break-word;
}
article :global(pre) {
background: var(--code-bg);
padding: 0.8rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.88em;
max-width: 100%;
}
article :global(code) {
background: var(--code-bg);
padding: 1px 4px;
border-radius: 3px;
font-size: 0.92em;
word-break: break-word;
}
article :global(pre code) {
padding: 0;
background: none;
word-break: normal;
}
article :global(hr) {
border: none;
border-top: 1px solid var(--border);
margin: 2rem 0;
}
article :global(blockquote) {
border-left: 3px solid var(--border);
padding: 0 0 0 1rem;
margin: 1rem 0;
color: var(--muted);
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="ts">
import type { Profile } from '$lib/nostr/loaders';
interface Props {
profile: Profile | null;
}
let { profile }: Props = $props();
</script>
{#if profile}
<div class="profile">
{#if profile.picture}
<img class="avatar" src={profile.picture} alt={profile.display_name ?? profile.name ?? ''} />
{:else}
<div class="avatar"></div>
{/if}
<div class="info">
<div class="name">{profile.display_name ?? profile.name ?? ''}</div>
{#if profile.about}
<div class="about">{profile.about}</div>
{/if}
{#if profile.nip05 || profile.website}
<div class="meta-line">
{#if profile.nip05}<span>{profile.nip05}</span>{/if}
{#if profile.nip05 && profile.website}<span class="sep">·</span>{/if}
{#if profile.website}
<a href={profile.website} target="_blank" rel="noopener">
{profile.website.replace(/^https?:\/\//, '')}
</a>
{/if}
</div>
{/if}
</div>
</div>
{/if}
<style>
.profile {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.avatar {
flex: 0 0 80px;
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
background: var(--code-bg);
}
.info {
flex: 1;
min-width: 0;
}
.name {
font-size: 1.3rem;
font-weight: 600;
margin: 0 0 0.2rem;
}
.about {
color: var(--muted);
font-size: 0.95rem;
margin: 0 0 0.3rem;
}
.meta-line {
font-size: 0.85rem;
color: var(--muted);
}
.meta-line a {
color: var(--accent);
text-decoration: none;
}
.meta-line a:hover {
text-decoration: underline;
}
.sep {
margin: 0 0.4rem;
opacity: 0.5;
}
</style>

View File

@ -0,0 +1,58 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { ReactionSummary } from '$lib/nostr/loaders';
import { loadReactions } from '$lib/nostr/loaders';
interface Props {
dtag: string;
}
let { dtag }: Props = $props();
let reactions: ReactionSummary[] = $state([]);
onMount(async () => {
try {
reactions = await loadReactions(dtag);
} catch {
reactions = [];
}
});
function displayChar(c: string): string {
if (c === '+' || c === '') return '👍';
if (c === '-') return '👎';
return c;
}
</script>
{#if reactions.length > 0}
<div class="reactions">
{#each reactions as r}
<span class="reaction">
<span class="emoji">{displayChar(r.content)}</span>
<span class="count">{r.count}</span>
</span>
{/each}
</div>
{/if}
<style>
.reactions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1.5rem 0;
}
.reaction {
display: inline-flex;
align-items: center;
gap: 0.3rem;
padding: 0.2rem 0.6rem;
background: var(--code-bg);
border-radius: 999px;
font-size: 0.9rem;
}
.count {
color: var(--muted);
}
</style>

View File

@ -0,0 +1,148 @@
<script lang="ts">
import { get } from 'svelte/store';
import {
hasNip07,
getPublicKey,
signEvent,
type SignedEvent,
type UnsignedEvent
} from '$lib/nostr/signer';
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
import { pool } from '$lib/nostr/pool';
import { readRelays } from '$lib/stores/readRelays';
interface Props {
/** d-Tag des Posts, auf den geantwortet wird */
dtag: string;
/** Event-ID des ursprünglichen Posts (für e-Tag) */
eventId: string;
/** Callback, wenn ein Reply erfolgreich publiziert wurde */
onPublished?: (ev: SignedEvent) => void;
}
let { dtag, eventId, onPublished }: Props = $props();
let text = $state('');
let publishing = $state(false);
let error: string | null = $state(null);
let info: string | null = $state(null);
const nip07 = hasNip07();
async function submit() {
error = null;
info = null;
if (!text.trim()) {
error = 'Leeres Kommentar — nichts zu senden.';
return;
}
publishing = true;
try {
const pubkey = await getPublicKey();
if (!pubkey) {
error = 'Nostr-Extension (z. B. Alby) hat den Pubkey nicht geliefert.';
return;
}
const unsigned: UnsignedEvent = {
kind: 1,
pubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [
['a', `30023:${AUTHOR_PUBKEY_HEX}:${dtag}`],
['e', eventId, '', 'root'],
['p', AUTHOR_PUBKEY_HEX]
],
content: text.trim()
};
const signed = await signEvent(unsigned);
if (!signed) {
error = 'Signatur wurde abgelehnt oder ist fehlgeschlagen.';
return;
}
const relays = get(readRelays);
const results = await pool.publish(relays, signed);
const okCount = results.filter((r) => r.ok).length;
if (okCount === 0) {
error = 'Kein Relay hat den Kommentar akzeptiert.';
return;
}
info = `Kommentar gesendet (${okCount}/${results.length} Relays).`;
text = '';
onPublished?.(signed);
} catch (e) {
error = e instanceof Error ? e.message : 'Unbekannter Fehler';
} finally {
publishing = false;
}
}
</script>
<div class="composer">
{#if !nip07}
<p class="hint">
Um zu kommentieren, benötigst du eine Nostr-Extension
(<a href="https://getalby.com" target="_blank" rel="noopener">Alby</a>,
<a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noopener">nos2x</a>), oder
kommentiere direkt in einem Nostr-Client.
</p>
{:else}
<textarea
bind:value={text}
placeholder="Dein Kommentar …"
rows="4"
disabled={publishing}
></textarea>
<div class="actions">
<button type="button" onclick={submit} disabled={publishing || !text.trim()}>
{publishing ? 'Sende …' : 'Kommentar senden'}
</button>
</div>
{#if error}<p class="error">{error}</p>{/if}
{#if info}<p class="info">{info}</p>{/if}
{/if}
</div>
<style>
.composer {
margin: 1.5rem 0;
}
textarea {
width: 100%;
padding: 0.6rem;
font: inherit;
color: inherit;
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 4px;
resize: vertical;
}
.actions {
margin-top: 0.5rem;
display: flex;
justify-content: flex-end;
}
button {
padding: 0.4rem 1rem;
background: var(--accent);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font: inherit;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.hint {
font-size: 0.9rem;
color: var(--muted);
}
.error {
color: #991b1b;
font-size: 0.9rem;
}
.info {
color: #065f46;
font-size: 0.9rem;
}
</style>

View File

@ -0,0 +1,100 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
import { getProfile } from '$lib/nostr/profileCache';
import { buildNjumpProfileUrl } from '$lib/nostr/naddr';
interface Props {
event: NostrEvent;
}
let { event }: Props = $props();
const date = $derived(new Date(event.created_at * 1000).toLocaleString('de-DE'));
const npubPrefix = $derived(event.pubkey.slice(0, 12) + '…');
const profileUrl = $derived(buildNjumpProfileUrl(event.pubkey));
let profile = $state<Profile | null>(null);
onMount(async () => {
try {
profile = await getProfile(event.pubkey);
} catch {
profile = null;
}
});
const displayName = $derived(profile?.display_name || profile?.name || npubPrefix);
</script>
<li class="reply">
<a class="header" href={profileUrl} target="_blank" rel="noopener">
{#if profile?.picture}
<img class="avatar" src={profile.picture} alt={displayName} />
{:else}
<div class="avatar avatar-placeholder" aria-hidden="true"></div>
{/if}
<div class="meta">
<span class="name">{displayName}</span>
<span class="date">{date}</span>
</div>
</a>
<div class="content">{event.content}</div>
</li>
<style>
.reply {
list-style: none;
padding: 0.8rem 0;
border-bottom: 1px solid var(--border);
}
.header {
display: flex;
gap: 0.6rem;
align-items: center;
margin-bottom: 0.4rem;
color: inherit;
text-decoration: none;
border-radius: 4px;
padding: 2px;
margin-left: -2px;
}
.header:hover {
background: var(--code-bg);
}
.header:hover .name {
color: var(--accent);
}
.avatar {
flex: 0 0 32px;
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
background: var(--code-bg);
}
.avatar-placeholder {
display: block;
}
.meta {
font-size: 0.85rem;
color: var(--muted);
display: flex;
flex-direction: column;
line-height: 1.3;
}
.name {
color: var(--fg);
font-weight: 500;
word-break: break-word;
}
.content {
white-space: pre-wrap;
word-wrap: break-word;
margin-left: calc(32px + 0.6rem);
}
@media (max-width: 479px) {
.content {
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,68 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent } from '$lib/nostr/loaders';
import { loadReplies } from '$lib/nostr/loaders';
import ReplyItem from './ReplyItem.svelte';
interface Props {
dtag: string;
/**
* Optimistisch hinzugefügte Events (z. B. frisch gesendete Kommentare).
* Werden vor dem Rendern zur geladenen Liste gemerged, dedupliziert per id.
*/
optimistic?: NostrEvent[];
}
let { dtag, optimistic = [] }: Props = $props();
let fetched: NostrEvent[] = $state([]);
let loading = $state(true);
const merged = $derived.by(() => {
const byId = new Map<string, NostrEvent>();
for (const ev of fetched) byId.set(ev.id, ev);
for (const ev of optimistic) byId.set(ev.id, ev);
return [...byId.values()].sort((a, b) => a.created_at - b.created_at);
});
onMount(async () => {
try {
fetched = await loadReplies(dtag);
} finally {
loading = false;
}
});
</script>
<section class="replies">
<h3>Kommentare ({merged.length})</h3>
{#if loading}
<p class="hint">Lade Kommentare …</p>
{:else if merged.length === 0}
<p class="hint">Noch keine Kommentare.</p>
{:else}
<ul>
{#each merged as reply (reply.id)}
<ReplyItem event={reply} />
{/each}
</ul>
{/if}
</section>
<style>
.replies {
margin: 2rem 0;
}
h3 {
font-size: 1.1rem;
margin: 0 0 0.8rem;
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
.hint {
color: var(--muted);
font-size: 0.9rem;
}
</style>

View File

@ -0,0 +1,118 @@
<script lang="ts">
// Inline-SVGs als kleine social-icon-leiste. bewusst keine zusätzlichen
// image-requests, kein icon-font. hover-color via currentColor + fill=currentColor.
const AUTHOR_PUBKEY_HEX =
'4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41';
type Entry = { label: string; href: string; icon: 'nostr' | 'mastodon' | 'linkedin' | 'bluesky' | 'mail' | 'orcid' };
const entries: Entry[] = [
{ label: 'Nostr', href: `https://edufeed.org/p/${AUTHOR_PUBKEY_HEX}`, icon: 'nostr' },
{ label: 'Mastodon', href: 'https://reliverse.social/@joerglohrer', icon: 'mastodon' },
{ label: 'Bluesky', href: 'https://bsky.app/profile/joerglohrer.bsky.social', icon: 'bluesky' },
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/joerglohrer', icon: 'linkedin' },
{ label: 'ORCID', href: 'https://orcid.org/0000-0002-9282-0406', icon: 'orcid' },
{ label: 'E-Mail', href: 'mailto:webmaster@joerg-lohrer.de', icon: 'mail' }
];
</script>
<nav class="social" aria-label="Soziale Profile und Kontakt">
{#each entries as e (e.href)}
<a
href={e.href}
target={e.icon === 'mail' ? undefined : '_blank'}
rel={e.icon === 'mail' ? undefined : 'me noopener'}
aria-label={e.label}
title={e.label}
>
{#if e.icon === 'nostr'}
<!-- Nostr-Logo (Outline) von satscoffee/nostr_icons, CC0 -->
<svg
viewBox="0 0 875 875"
aria-hidden="true"
width="20"
height="20"
fill="none"
stroke="currentColor"
stroke-width="40"
stroke-miterlimit="10"
>
<path
d="m684.72,485.57c.22,12.59-11.93,51.47-38.67,81.3-26.74,29.83-56.02,20.85-58.42,20.16s-3.09-4.46-7.89-3.77-9.6,6.17-18.86,7.2-17.49,1.71-26.06-1.37c-4.46.69-5.14.71-7.2,2.24s-17.83,10.79-21.6,11.47c0,7.2-1.37,44.57,0,55.89s3.77,25.71,7.54,36c3.77,10.29,2.74,10.63,7.54,9.94s13.37.34,15.77,4.11c2.4,3.77,1.37,6.51,5.49,8.23s60.69,17.14,99.43,19.2c26.74.69,42.86,2.74,52.12,19.54,1.37,7.89,7.54,13.03,11.31,14.06s8.23,2.06,12,5.83,1.03,8.23,5.49,11.66c4.46,3.43,14.74,8.57,25.37,13.71,10.63,5.14,15.09,13.37,15.77,16.11s1.71,10.97,1.71,10.97c0,0-8.91,0-10.97-2.06s-2.74-5.83-2.74-5.83c0,0-6.17,1.03-7.54,3.43s.69,2.74-7.89.69-11.66-3.77-18.17-8.57c-6.51-4.8-16.46-17.14-25.03-16.8,4.11,8.23,5.83,8.23,10.63,10.97s8.23,5.83,8.23,5.83l-7.2,4.46s-4.46,2.06-14.74-.69-11.66-4.46-12.69-10.63,0-9.26-2.74-14.4-4.11-15.77-22.29-21.26c-18.17-5.49-66.52-21.26-100.12-24.69s-22.63-2.74-28.11-1.37-15.77,4.46-26.4-1.37c-10.63-5.83-16.8-13.71-17.49-20.23s-1.71-10.97,0-19.2,3.43-19.89,1.71-26.74-14.06-55.89-19.89-64.12c-13.03,1.03-50.74-.69-50.74-.69,0,0-2.4-.69-17.49,5.83s-36.48,13.76-46.77,19.93-14.4,9.7-16.12,13.13c.12,3-1.23,7.72-2.79,9.06s-12.48,2.42-12.48,2.42c0,0-5.85,5.86-8.25,9.97-6.86,9.6-55.2,125.14-66.52,149.83-13.54,32.57-9.77,27.43-37.71,27.43s-8.06.3-8.06.3c0,0-12.34,5.88-16.8,5.88s-18.86-2.4-26.4,0-16.46,9.26-23.31,10.29-4.95-1.34-8.38-3.74c-4-.21-14.27-.12-14.27-.12,0,0,1.74-6.51,7.91-10.88,8.23-5.83,25.37-16.11,34.63-21.26s17.49-7.89,23.31-9.26,18.51-6.17,30.51-9.94,19.54-8.23,29.83-31.54c10.29-23.31,50.4-111.43,51.43-116.23.63-2.96,3.73-6.48,4.8-15.09.66-5.35-2.49-13.04,1.71-22.63,10.97-25.03,21.6-20.23,26.4-20.23s17.14.34,26.4-1.37,15.43-2.74,24.69-7.89,11.31-8.91,11.31-8.91l-19.89-3.43s-18.51.69-25.03-4.46-15.43-15.77-15.43-15.77l-7.54-7.2,1.03,8.57s-5.14-8.91-6.51-10.29-8.57-6.51-11.31-11.31-7.54-25.03-7.54-25.03l-6.17,13.03-1.71-18.86-5.14,7.2-2.74-16.11-4.8,8.23-3.43-14.4-5.83,4.46-2.4-10.29-5.83-3.43s-14.06-9.26-16.46-9.6-4.46,3.43-4.46,3.43l1.37,12-12.2-6.27-7-11.9s2.36,4.01-9.62,7.53c-20.55,0-21.89-2.28-24.93-3.94-1.31-6.56-5.57-10.11-5.57-10.11h-20.57l-.34-6.86-7.89,3.09.69-10.29h-14.06l1.03-11.31h-8.91s3.09-9.26,25.71-22.97,25.03-16.46,46.29-17.14c21.26-.69,32.91,2.74,46.29,8.23s38.74,13.71,43.89,17.49c11.31-9.94,28.46-19.89,34.29-19.89,1.03-2.4,6.19-12.33,17.96-17.6,35.31-15.81,108.13-34,131.53-35.54,31.2-2.06,7.89-1.37,39.09,2.06,31.2,3.43,54.17,7.54,69.6,12.69,12.58,4.19,25.03,9.6,34.29,2.06,4.33-1.81,11.81-1.34,17.83-5.14,30.69-25.09,34.72-32.35,43.63-41.95s20.14-24.91,22.54-45.14,4.46-58.29-10.63-88.12-28.8-45.26-34.63-69.26c-5.83-24-8.23-61.03-6.17-73.03,2.06-12,5.14-22.29,6.86-30.51s9.94-14.74,19.89-16.46c9.94-1.71,17.83,1.37,22.29,4.8,4.46,3.43,11.65,6.28,13.37,10.29.34,1.71-1.37,6.51,8.23,8.23,9.6,1.71,16.05,4.16,16.05,4.16,0,0,15.64,4.29,3.11,7.73-12.69,2.06-20.52-.71-24.29,1.69s-7.21,10.08-9.61,11.1-7.2.34-12,4.11-9.6,6.86-12.69,14.4-5.49,15.77-3.43,26.74,8.57,31.54,14.4,43.2c5.83,11.66,20.23,40.8,24.34,47.66s15.77,29.49,16.8,53.83,1.03,44.23,0,54.86-10.84,51.65-35.53,85.94c-8.16,14.14-23.21,31.9-24.67,35.03-1.45,3.13-3.02,4.88-1.61,7.65,4.62,9.05,12.87,22.13,14.71,29.22,2.29,6.64,6.99,16.13,7.22,28.72Z"
/>
</svg>
{:else if e.icon === 'mastodon'}
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path
fill="currentColor"
d="M21.58 13.9c-.3 1.53-2.67 3.22-5.4 3.54-1.42.17-2.82.33-4.32.26-2.45-.11-4.39-.58-4.39-.58 0 .24.01.47.05.68.32 2.4 2.39 2.55 4.34 2.61 1.97.07 3.74-.49 3.74-.49l.08 1.78s-1.39.75-3.87.88c-1.37.08-3.07-.03-5.05-.56C2.46 20.86 1.72 16.2 1.61 11.47 1.57 10.07 1.59 8.76 1.59 7.66c0-4.85 3.18-6.27 3.18-6.27 1.6-.73 4.35-1.04 7.21-1.07h.07c2.86.02 5.61.33 7.22 1.07 0 0 3.18 1.42 3.18 6.27 0 0 .04 3.59-.45 6.08-.03.16-.18.16-.42.16zm-3.01-5.53c0-1.18-.3-2.11-.9-2.81-.62-.7-1.43-1.05-2.44-1.05-1.17 0-2.06.45-2.65 1.35l-.58.97-.58-.97c-.59-.9-1.48-1.35-2.65-1.35-1.01 0-1.82.36-2.44 1.05-.6.7-.9 1.63-.9 2.81v5.78h2.3V8.54c0-1.18.5-1.78 1.5-1.78 1.1 0 1.65.71 1.65 2.13v3.09h2.28V8.88c0-1.41.55-2.13 1.65-2.13 1 0 1.5.6 1.5 1.78v5.61h2.3V8.37z"
/>
</svg>
{:else if e.icon === 'bluesky'}
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path
fill="currentColor"
d="M5.2 3.5c2.4 1.8 5 5.4 5.9 7.4.9-2 3.5-5.6 5.9-7.4 1.8-1.3 4.7-2.3 4.7 1 0 .7-.4 5.6-.6 6.4-.8 2.8-3.6 3.5-6.1 3.1 4.4.7 5.5 3.2 3.1 5.7-4.6 4.8-6.6-1.2-7.1-2.7-.1-.3-.1-.4-.1-.3 0-.1-.1 0-.1.3-.5 1.5-2.5 7.5-7.1 2.7-2.5-2.5-1.3-5 3.1-5.7-2.5.4-5.3-.3-6.1-3.1C-.1 10.1-.5 5.2-.5 4.5c0-3.3 2.9-2.3 4.7-1l1 0z"
/>
</svg>
{:else if e.icon === 'linkedin'}
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path
fill="currentColor"
d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14zM8.34 18.34v-8.4H5.67v8.4h2.67zM7 8.67a1.55 1.55 0 1 0 0-3.1 1.55 1.55 0 0 0 0 3.1zm11.34 9.67v-4.6c0-2.46-1.31-3.6-3.06-3.6-1.41 0-2.04.78-2.39 1.32v-1.13h-2.67c.04.75 0 8.01 0 8.01h2.67v-4.47c0-.24.02-.48.09-.65.19-.48.63-.97 1.37-.97.97 0 1.36.74 1.36 1.82v4.27h2.63z"
/>
</svg>
{:else if e.icon === 'orcid'}
<!-- offizielles ORCID-symbol stark vereinfacht, monochrom via currentColor -->
<svg viewBox="0 0 256 256" aria-hidden="true" width="20" height="20">
<circle cx="128" cy="128" r="128" fill="currentColor" opacity="0.15" />
<circle cx="128" cy="128" r="118" fill="none" stroke="currentColor" stroke-width="10" />
<path
fill="currentColor"
d="M86 76a8 8 0 1 1-16 0 8 8 0 0 1 16 0zM72 96h14v96H72V96zm34 0h38c28 0 50 21 50 48s-22 48-50 48h-38V96zm14 14v68h22c22 0 36-14 36-34s-14-34-36-34h-22z"
/>
</svg>
{:else if e.icon === 'mail'}
<svg viewBox="0 0 24 24" aria-hidden="true" width="20" height="20">
<path
fill="currentColor"
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2zm0 2v.01L12 13l8-6.99V6H4zm0 2.7V18h16V8.7l-8 6.99L4 8.7z"
/>
</svg>
{/if}
</a>
{/each}
</nav>
<style>
.social {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
margin-top: 0.6rem;
}
.social a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--code-bg);
color: var(--muted);
transition:
color 140ms,
background 140ms,
transform 140ms;
}
.social a:hover,
.social a:focus-visible {
color: var(--accent);
background: color-mix(in srgb, var(--accent) 15%, var(--code-bg));
transform: translateY(-1px);
}
.social svg {
display: block;
}
</style>

1
app/src/lib/index.ts Normal file
View File

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -0,0 +1,37 @@
/**
* Nostr-Konfiguration der SPA.
*
* Wichtig: Der AUTHOR_PUBKEY_HEX muss synchron zum tatsächlichen
* Autorenkonto sein (siehe docs/superpowers/specs/2026-04-15-nostr-page-design.md).
*/
/** npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9 in hex */
export const AUTHOR_PUBKEY_HEX =
'4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41';
/** Bootstrap-Relay für das initiale Lesen von kind:10002 */
export const BOOTSTRAP_RELAY = 'wss://relay.damus.io';
/**
* Fallback, falls kind:10002 nicht geladen werden kann.
* Bootstrap-Relay ist bewusst als erster Eintrag Teil der Liste ein Ort der Wahrheit.
*/
export const FALLBACK_READ_RELAYS = [
BOOTSTRAP_RELAY,
'wss://nos.lol',
'wss://relay.primal.net',
'wss://relay.tchncs.de',
'wss://relay.edufeed.org',
] as const;
/**
* Habla.news-Route für Addressable Events URL endet auf `/a/`, der
* vollständige Deep-Link wird durch Anhängen des `naddr1…`-Bech32 gebildet.
*/
export const HABLA_BASE = 'https://habla.news/a/';
/** Soft-Timeout: einzelne Relay-Abfrage darf nicht länger als diese Dauer blockieren. */
export const RELAY_TIMEOUT_MS = 8000;
/** Hard-Timeout: Page-Budget, nach dem eine Route-Abfrage endgültig abbricht. */
export const RELAY_HARD_TIMEOUT_MS = 15000;

View File

@ -0,0 +1,191 @@
import { get } from 'svelte/store';
import { lastValueFrom, timeout, toArray, EMPTY, tap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import type { NostrEvent } from 'applesauce-core/helpers/event';
import type { Filter as ApplesauceFilter } from 'applesauce-core/helpers/filter';
import { pool } from './pool';
import { readRelays } from '$lib/stores/readRelays';
import { AUTHOR_PUBKEY_HEX, RELAY_HARD_TIMEOUT_MS } from './config';
/** Re-export als sprechenden Alias */
export type { NostrEvent };
/** Profile-Content (kind:0) */
export interface Profile {
name?: string;
display_name?: string;
picture?: string;
banner?: string;
about?: string;
website?: string;
nip05?: string;
lud16?: string;
}
type Filter = ApplesauceFilter;
interface CollectOpts {
onEvent?: (ev: NostrEvent) => void;
hardTimeoutMs?: number;
}
/**
* Startet eine Request-Subscription und sammelt alle gelieferten Events
* bis EOSE (pool.request completes nach EOSE) oder Hard-Timeout.
*/
async function collectEvents(
relays: string[],
filter: Filter,
opts: CollectOpts = {}
): Promise<NostrEvent[]> {
const events = await lastValueFrom(
pool.request(relays, filter).pipe(
tap((ev: NostrEvent) => opts.onEvent?.(ev)),
timeout(opts.hardTimeoutMs ?? RELAY_HARD_TIMEOUT_MS),
toArray(),
catchError(() => EMPTY)
),
{ defaultValue: [] as NostrEvent[] }
);
return events;
}
/** Dedup per d-Tag: neueste (created_at) wins */
function dedupByDtag(events: NostrEvent[]): NostrEvent[] {
const byDtag = new Map<string, NostrEvent>();
for (const ev of events) {
const d = ev.tags.find((t) => t[0] === 'd')?.[1];
if (!d) continue;
const existing = byDtag.get(d);
if (!existing || ev.created_at > existing.created_at) {
byDtag.set(d, ev);
}
}
return [...byDtag.values()];
}
/** Alle kind:30023-Posts des Autors, neueste zuerst */
export async function loadPostList(
onEvent?: (ev: NostrEvent) => void
): Promise<NostrEvent[]> {
const relays = get(readRelays);
const events = await collectEvents(
relays,
{ kinds: [30023], authors: [AUTHOR_PUBKEY_HEX], limit: 200 },
{ onEvent }
);
const deduped = dedupByDtag(events);
return deduped.sort((a, b) => {
const ap = parseInt(
a.tags.find((t) => t[0] === 'published_at')?.[1] ?? `${a.created_at}`,
10
);
const bp = parseInt(
b.tags.find((t) => t[0] === 'published_at')?.[1] ?? `${b.created_at}`,
10
);
return bp - ap;
});
}
/** Einzelpost per d-Tag */
export async function loadPost(dtag: string): Promise<NostrEvent | null> {
const relays = get(readRelays);
const events = await collectEvents(relays, {
kinds: [30023],
authors: [AUTHOR_PUBKEY_HEX],
'#d': [dtag],
limit: 1
});
if (events.length === 0) return null;
return events.reduce((best, cur) =>
cur.created_at > best.created_at ? cur : best
);
}
/**
* Profil-Event kind:0 (neueste Version).
* Default: Autoren-Pubkey der SPA. Optional: beliebiger Pubkey für
* die Anzeige fremder Kommentar-Autoren.
*/
export async function loadProfile(pubkey: string = AUTHOR_PUBKEY_HEX): Promise<Profile | null> {
const relays = get(readRelays);
const events = await collectEvents(relays, {
kinds: [0],
authors: [pubkey],
limit: 1
});
if (events.length === 0) return null;
const latest = events.reduce((best, cur) =>
cur.created_at > best.created_at ? cur : best
);
try {
return JSON.parse(latest.content) as Profile;
} catch {
return null;
}
}
/** Post-Adresse im `a`-Tag-Format: "30023:<pubkey>:<dtag>" */
function eventAddress(pubkey: string, dtag: string): string {
return `30023:${pubkey}:${dtag}`;
}
/**
* Alle kind:1-Replies auf einen Post, chronologisch aufsteigend (älteste zuerst).
* Streamt via onEvent, wenn angegeben.
*/
export async function loadReplies(
dtag: string,
onEvent?: (ev: NostrEvent) => void
): Promise<NostrEvent[]> {
const relays = get(readRelays);
const address = eventAddress(AUTHOR_PUBKEY_HEX, dtag);
const events = await collectEvents(
relays,
{ kinds: [1], '#a': [address], limit: 500 },
{ onEvent }
);
return events.sort((a, b) => a.created_at - b.created_at);
}
/**
* Filtert Post-Liste clientseitig nach Tag-Name.
* (Relay-seitige #t-Filter werden nicht von allen Relays unterstützt safer
* ist es, die ganze Liste zu laden und lokal zu filtern.)
*/
export async function loadPostsByTag(tagName: string): Promise<NostrEvent[]> {
const all = await loadPostList();
const norm = tagName.toLowerCase();
return all.filter((ev) =>
ev.tags.some((t) => t[0] === 't' && t[1]?.toLowerCase() === norm)
);
}
export interface ReactionSummary {
/** Emoji oder "+"/"-" */
content: string;
count: number;
}
/**
* Aggregiert kind:7-Reactions auf einen Post.
* Gruppiert nach content, zählt Anzahl.
*/
export async function loadReactions(dtag: string): Promise<ReactionSummary[]> {
const relays = get(readRelays);
const address = eventAddress(AUTHOR_PUBKEY_HEX, dtag);
const events = await collectEvents(relays, {
kinds: [7],
'#a': [address],
limit: 500
});
const counts = new Map<string, number>();
for (const ev of events) {
const key = ev.content || '+';
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return [...counts.entries()]
.map(([content, count]) => ({ content, count }))
.sort((a, b) => b.count - a.count);
}

View File

@ -0,0 +1,66 @@
import { nip19 } from 'nostr-tools';
import { HABLA_BASE } from './config';
/**
* Argumente für NIP-19 addressable-event-Pointer.
* Validierung (hex-Länge etc.) wird an `nip19.naddrEncode` delegiert.
*/
export interface NaddrArgs {
pubkey: string;
kind: number;
identifier: string;
relays?: string[];
}
/**
* Baut einen `naddr1…`-Bech32-String (NIP-19) für ein addressable Event.
* Wird u. a. für Habla.news-Deep-Links genutzt.
*/
export function buildNaddr(args: NaddrArgs): string {
return nip19.naddrEncode({
pubkey: args.pubkey,
kind: args.kind,
identifier: args.identifier,
relays: args.relays ?? []
});
}
/**
* Habla.news-Deep-Link auf ein addressable Event.
* Fallback für Post nicht gefunden" / JS-lose Clients.
*/
export function buildHablaLink(args: NaddrArgs): string {
return `${HABLA_BASE}${buildNaddr(args)}`;
}
/**
* `npub1…`-Bech32-String für einen Pubkey für Profil-Links außerhalb
* der SPA (z. B. njump.me).
*/
export function buildNpub(pubkeyHex: string): string {
return nip19.npubEncode(pubkeyHex);
}
/**
* njump.me-Profil-URL. Öffnet das Nostr-native Profil-Browser mit
* vollständiger Event-Historie.
*/
export function buildNjumpProfileUrl(pubkeyHex: string): string {
return `https://njump.me/${buildNpub(pubkeyHex)}`;
}
/**
* Liste externer Nostr-Clients für Post öffnen in "-Links.
* Nutzt naddr, damit jeder Client das addressable Event adressieren kann.
* EduFeed zuerst OER/OEP-Bildungscommunity, wichtig für Jörgs Zielgruppe.
*/
export function externalClientLinks(
args: NaddrArgs
): { label: string; url: string }[] {
const naddr = buildNaddr(args);
return [
{ label: 'EduFeed', url: `https://edufeed.org/${naddr}` },
{ label: 'Habla', url: `https://habla.news/a/${naddr}` },
{ label: 'Yakihonne', url: `https://yakihonne.com/article/${naddr}` }
];
}

View File

@ -0,0 +1,7 @@
import { RelayPool } from 'applesauce-relay';
/**
* Singleton-Pool für alle Nostr-Requests der SPA.
* applesauce-relay verwaltet Reconnects, Subscriptions, deduping intern.
*/
export const pool = new RelayPool();

View File

@ -0,0 +1,17 @@
import type { Profile } from './loaders';
import { loadProfile } from './loaders';
/**
* Sessionsweiter Cache für kind:0-Profile.
* Jeder Pubkey wird maximal einmal angefragt; mehrfache parallele
* Aufrufe teilen sich dieselbe Promise.
*/
const cache = new Map<string, Promise<Profile | null>>();
export function getProfile(pubkey: string): Promise<Profile | null> {
const existing = cache.get(pubkey);
if (existing) return existing;
const pending = loadProfile(pubkey);
cache.set(pubkey, pending);
return pending;
}

View File

@ -0,0 +1,95 @@
import { lastValueFrom, timeout, toArray, EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import type { NostrEvent } from 'applesauce-core/helpers/event';
import { pool } from './pool';
import {
AUTHOR_PUBKEY_HEX,
BOOTSTRAP_RELAY,
FALLBACK_READ_RELAYS,
RELAY_TIMEOUT_MS
} from './config';
export interface OutboxRelay {
url: string;
/** true = zum Lesen zu nutzen (kein dritter Tag-Wert oder "read") */
read: boolean;
/** true = zum Schreiben zu nutzen (kein dritter Tag-Wert oder "write") */
write: boolean;
}
/**
* Lädt die NIP-65-Relay-Liste (kind:10002) des Autors vom Bootstrap-Relay.
* Fallback auf FALLBACK_READ_RELAYS, wenn das Event nicht innerhalb von
* RELAY_TIMEOUT_MS gefunden wird.
*
* Interpretation des dritten Tag-Werts:
* - nicht gesetzt read + write
* - "read" nur read
* - "write" nur write
*/
export async function loadOutboxRelays(): Promise<OutboxRelay[]> {
const event = await firstEvent();
if (!event) {
return FALLBACK_READ_RELAYS.map((url) => ({ url, read: true, write: true }));
}
const relays: OutboxRelay[] = [];
for (const tag of event.tags) {
if (tag[0] !== 'r' || !tag[1]) continue;
const mode = tag[2];
relays.push({
url: tag[1],
read: mode !== 'write',
write: mode !== 'read'
});
}
if (relays.length === 0) {
return FALLBACK_READ_RELAYS.map((url) => ({ url, read: true, write: true }));
}
return relays;
}
/** Nur die Read-URLs aus OutboxRelay[] */
export function readUrls(relays: OutboxRelay[]): string[] {
return relays.filter((r) => r.read).map((r) => r.url);
}
/** Nur die Write-URLs aus OutboxRelay[] */
export function writeUrls(relays: OutboxRelay[]): string[] {
return relays.filter((r) => r.write).map((r) => r.url);
}
// ---------- Internes --------------------------------------------------------
/**
* Fragt das neueste kind:10002-Event vom Bootstrap-Relay ab.
* Sammelt alle Events bis EOSE (`pool.request(...)` emittiert nur Events
* und completes bei EOSE), nimmt das neueste, oder null falls keines.
*/
async function firstEvent(): Promise<NostrEvent | null> {
try {
const events = await lastValueFrom(
pool
.request([BOOTSTRAP_RELAY], {
kinds: [10002],
authors: [AUTHOR_PUBKEY_HEX],
limit: 1
})
.pipe(
timeout(RELAY_TIMEOUT_MS),
toArray(),
catchError(() => EMPTY)
),
{ defaultValue: [] as NostrEvent[] }
);
if (events.length === 0) return null;
return events.reduce((best, cur) =>
cur.created_at > best.created_at ? cur : best
);
} catch {
return null;
}
}

View File

@ -0,0 +1,50 @@
/**
* NIP-07-Wrapper für Browser-Extension-Signer (Alby, nos2x, Flamingo).
*
* `window.nostr` ist optional wenn die Extension fehlt, liefern die Helper
* null zurück und der Aufrufer zeigt einen Hinweis an.
*/
declare global {
interface Window {
nostr?: {
getPublicKey(): Promise<string>;
signEvent(event: UnsignedEvent): Promise<SignedEvent>;
};
}
}
export interface UnsignedEvent {
kind: number;
tags: string[][];
content: string;
created_at: number;
pubkey: string;
}
export interface SignedEvent extends UnsignedEvent {
id: string;
sig: string;
}
export function hasNip07(): boolean {
return typeof window !== 'undefined' && !!window.nostr;
}
export async function getPublicKey(): Promise<string | null> {
if (!hasNip07()) return null;
try {
return await window.nostr!.getPublicKey();
} catch {
return null;
}
}
export async function signEvent(event: UnsignedEvent): Promise<SignedEvent | null> {
if (!hasNip07()) return null;
try {
return await window.nostr!.signEvent(event);
} catch {
return null;
}
}

View File

@ -0,0 +1,53 @@
import { Marked } from 'marked';
import DOMPurify from 'dompurify';
import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import bash from 'highlight.js/lib/languages/bash';
import typescript from 'highlight.js/lib/languages/typescript';
import json from 'highlight.js/lib/languages/json';
hljs.registerLanguage('javascript', javascript);
hljs.registerLanguage('js', javascript);
hljs.registerLanguage('typescript', typescript);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('bash', bash);
hljs.registerLanguage('sh', bash);
hljs.registerLanguage('json', json);
/**
* Lokaler Marked-Instance, damit die globale `marked`-Singleton nicht
* mutiert wird andere Module können `marked` unbeeinflusst weiterverwenden.
* (Spec §3: lokale Ersetzbarkeit der Engine.)
*/
const markedInstance = new Marked({
breaks: true,
gfm: true,
renderer: {
code({ text, lang }) {
const language = lang && hljs.getLanguage(lang) ? lang : undefined;
const highlighted = language
? hljs.highlight(text, { language }).value
: hljs.highlightAuto(text).value;
const cls = language ? ` language-${language}` : '';
return `<pre><code class="hljs${cls}">${highlighted}</code></pre>`;
}
}
});
/**
* Rendert einen Markdown-String zu sanitized HTML.
* Einziger Export des Moduls so bleibt Austausch der Engine lokal.
*
* Nur im Browser/jsdom aufrufen: DOMPurify braucht ein DOM. Die SPA
* hat SSR global ausgeschaltet (`+layout.ts: ssr = false`), Vitest läuft
* in jsdom beide Szenarien sind abgedeckt. Ein Aufruf in reiner
* Node-Umgebung würde hier laut fehlschlagen statt stumm unsicher
* durchzulaufen.
*/
export function renderMarkdown(md: string): string {
if (typeof window === 'undefined') {
throw new Error('renderMarkdown: DOM-Kontext erforderlich (Browser oder jsdom).');
}
const raw = markedInstance.parse(md, { async: false }) as string;
return DOMPurify.sanitize(raw);
}

View File

@ -0,0 +1,29 @@
import { writable, type Readable } from 'svelte/store';
import { loadOutboxRelays, readUrls } from '$lib/nostr/relays';
import { FALLBACK_READ_RELAYS } from '$lib/nostr/config';
/**
* Store mit der aktuellen Read-Relay-Liste.
* Initial = FALLBACK_READ_RELAYS, damit die SPA sofort abfragen kann;
* sobald loadOutboxRelays() fertig ist, wird der Store aktualisiert.
*
* Singleton-Initialisierung: bootstrapReadRelays() wird genau einmal beim ersten
* Import aufgerufen.
*/
const store = writable<string[]>([...FALLBACK_READ_RELAYS]);
let bootstrapped = false;
export function bootstrapReadRelays(): void {
if (bootstrapped) return;
bootstrapped = true;
loadOutboxRelays()
.then((relays) => {
const urls = readUrls(relays);
if (urls.length > 0) store.set(urls);
})
.catch(() => {
// Store behält seinen initialen FALLBACK-Zustand
});
}
export const readRelays: Readable<string[]> = store;

24
app/src/lib/url/legacy.ts Normal file
View File

@ -0,0 +1,24 @@
/**
* Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/<dtag>.html oder .../<dtag>.html/
* und gibt den dtag-Teil zurück. Für alle anderen Pfade: null.
*
* Erwartet nur den Pfad ohne Query/Fragment wenn vorhanden vom Aufrufer
* trennen. `decodeURIComponent` wird defensiv gekapselt, damit malformed
* Percent-Encoding die SPA beim Boot nicht crasht.
*/
export function parseLegacyUrl(path: string): string | null {
const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/);
if (!match) return null;
try {
return decodeURIComponent(match[1]);
} catch {
return null;
}
}
/**
* Erzeugt die kanonische kurze Post-URL /<dtag>/.
*/
export function canonicalPostPath(dtag: string): string {
return `/${encodeURIComponent(dtag)}/`;
}

View File

@ -0,0 +1,163 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/state';
import { bootstrapReadRelays } from '$lib/stores/readRelays';
import CcZeroBadge from '$lib/components/CcZeroBadge.svelte';
let { children } = $props();
// Normalisierter pfad ohne trailing slash für aktiv-erkennung ("/" bleibt "/")
const currentPath = $derived((page.url?.pathname ?? '/').replace(/\/$/, '') || '/');
function isActive(path: string): boolean {
const normalized = path.replace(/\/$/, '') || '/';
return currentPath === normalized;
}
onMount(() => {
bootstrapReadRelays();
});
</script>
<!-- favicon-Tags liegen in src/app.html — hier nichts nötig. -->
<header class="site-header">
<div class="header-inner">
<a href="/" class="brand" aria-label="Zur Startseite">Jörg Lohrer</a>
<nav aria-label="Hauptnavigation">
<a href="/" class:active={isActive('/')}>Home</a>
<a href="/archiv/" class:active={isActive('/archiv/')}>Archiv</a>
<a href="/impressum/" class:active={isActive('/impressum/')}>Impressum</a>
</nav>
</div>
</header>
<main>
{@render children()}
</main>
<footer class="site-footer">
<div class="footer-inner">
<span class="footer-license">
<a
href="https://creativecommons.org/publicdomain/zero/1.0/deed.de"
target="_blank"
rel="license noopener"
aria-label="CC0 1.0 Universal Public Domain Dedication"
title="CC0 1.0 Universal"
>
<CcZeroBadge />
<span class="cc-label">CC0</span>
</a>
Jörg Lohrer
</span>
<span class="footer-sep">·</span>
<a href="/impressum/">Impressum</a>
<span class="footer-sep">·</span>
<a
href="https://github.com/joerglohrer/joerglohrerde"
target="_blank"
rel="noopener"
title="Quellcode, Making-of und Nostr-Publish-Pipeline"
>Nostr-basiert Making-of im Repo</a>
</div>
</footer>
<style>
.site-header {
border-bottom: 1px solid var(--border);
background: var(--bg);
position: sticky;
top: 0;
z-index: 10;
backdrop-filter: blur(8px);
}
.header-inner {
max-width: 720px;
margin: 0 auto;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.brand {
font-weight: 700;
font-size: 1.05rem;
color: var(--fg);
text-decoration: none;
white-space: nowrap;
}
nav {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
nav a {
color: var(--muted);
text-decoration: none;
font-size: 0.95rem;
padding: 0.25rem 0;
border-bottom: 2px solid transparent;
transition: color 120ms, border-color 120ms;
}
nav a:hover {
color: var(--fg);
}
nav a.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
main {
max-width: 720px;
margin: 0 auto;
padding: 1.5rem 1rem;
min-height: calc(100vh - 200px);
}
@media (min-width: 640px) {
main {
padding: 1.5rem;
}
}
.site-footer {
border-top: 1px solid var(--border);
margin-top: 3rem;
}
.footer-inner {
max-width: 720px;
margin: 0 auto;
padding: 1rem;
color: var(--muted);
font-size: 0.85rem;
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
align-items: center;
}
.footer-inner a {
color: var(--muted);
text-decoration: none;
}
.footer-inner a:hover {
color: var(--accent);
text-decoration: underline;
}
.footer-sep {
opacity: 0.5;
}
.footer-license a {
color: var(--accent);
display: inline-flex;
align-items: center;
gap: 0.25em;
text-decoration: none;
}
.footer-license a:hover .cc-label {
text-decoration: underline;
}
.cc-label {
font-weight: 600;
}
</style>

View File

@ -0,0 +1,3 @@
export const prerender = false;
export const ssr = false;
export const trailingSlash = 'always';

174
app/src/routes/+page.svelte Normal file
View File

@ -0,0 +1,174 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent, Profile } from '$lib/nostr/loaders';
import { loadPostList } from '$lib/nostr/loaders';
import { getProfile } from '$lib/nostr/profileCache';
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
import PostCard from '$lib/components/PostCard.svelte';
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
import SocialIcons from '$lib/components/SocialIcons.svelte';
// Lokales Profilbild aus static/ — schneller als der Nostr-kind:0-Roundtrip
// fürs kind:0 -> picture-Feld (URL wäre identisch, aber Netzwerk-Latenz).
const HERO_AVATAR = '/joerg-profil-2024.webp';
const LATEST_COUNT = 5;
let profile: Profile | null = $state(null);
let posts: NostrEvent[] = $state([]);
let loading = $state(true);
let error: string | null = $state(null);
onMount(async () => {
try {
const [p, list] = await Promise.all([getProfile(AUTHOR_PUBKEY_HEX), loadPostList()]);
profile = p;
posts = list;
loading = false;
if (list.length === 0) {
error = 'Keine Posts gefunden auf den abgefragten Relays.';
}
} catch (e) {
loading = false;
error = e instanceof Error ? e.message : 'Unbekannter Fehler';
}
});
$effect(() => {
const p = profile;
const name = (p && (p.display_name ?? p.name)) ?? 'Jörg Lohrer';
document.title = `${name} Blog`;
});
const displayName = $derived.by(() => {
const p = profile;
return (p && (p.display_name ?? p.name)) ?? 'Jörg Lohrer';
});
const avatarSrc = HERO_AVATAR;
const about = $derived.by(() => profile?.about ?? '');
const website = $derived.by(() => profile?.website ?? '');
const latest = $derived(posts.slice(0, LATEST_COUNT));
const hasMore = $derived(posts.length > LATEST_COUNT);
</script>
<section class="hero">
<div class="hero-left">
<img class="avatar" src={avatarSrc} alt={displayName} />
<SocialIcons />
</div>
<div class="hero-text">
<h1 class="hero-name">{displayName}</h1>
<p class="hero-greeting">
Hi <span aria-hidden="true">🖖</span> Willkommen auf meinem Blog
<span aria-hidden="true">🤗</span>
</p>
{#if about}
<p class="hero-about">{about}</p>
{/if}
{#if website}
<div class="meta-line">
<a href={website} target="_blank" rel="noopener">
{website.replace(/^https?:\/\//, '').replace(/\/$/, '')}
</a>
</div>
{/if}
</div>
</section>
<section class="latest">
<h2 class="section-title">Neueste Beiträge</h2>
<LoadingOrError {loading} {error} />
{#each latest as post (post.id)}
<PostCard event={post} />
{/each}
{#if hasMore}
<div class="more">
<a href="/archiv/" class="more-link">Alle Beiträge im Archiv →</a>
</div>
{/if}
</section>
<style>
.hero {
display: flex;
gap: 1.25rem;
align-items: flex-start;
padding: 1rem 0 2rem;
margin-bottom: 1.5rem;
border-bottom: 1px solid var(--border);
}
.hero-left {
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.avatar {
width: 96px;
height: 96px;
border-radius: 50%;
object-fit: cover;
background: var(--code-bg);
border: 2px solid var(--accent);
}
.hero-text {
flex: 1;
min-width: 0;
}
.hero-name {
margin: 0 0 0.3rem;
font-size: 1.6rem;
font-weight: 700;
}
.hero-greeting {
margin: 0 0 0.5rem;
font-size: 1.05rem;
color: var(--fg);
}
.hero-about {
margin: 0 0 0.5rem;
color: var(--muted);
font-size: 1rem;
line-height: 1.45;
}
.meta-line {
font-size: 0.9rem;
color: var(--muted);
}
.meta-line a {
color: var(--accent);
text-decoration: none;
}
.meta-line a:hover {
text-decoration: underline;
}
.section-title {
margin: 0 0 1rem;
font-size: 1.25rem;
font-weight: 600;
}
.more {
margin-top: 1.5rem;
text-align: center;
}
.more-link {
color: var(--accent);
text-decoration: none;
font-weight: 500;
}
.more-link:hover {
text-decoration: underline;
}
@media (max-width: 520px) {
.hero {
flex-direction: column;
align-items: center;
text-align: center;
gap: 0.8rem;
}
.hero-left {
align-items: center;
}
}
</style>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent } from '$lib/nostr/loaders';
import { loadPost } from '$lib/nostr/loaders';
import { AUTHOR_PUBKEY_HEX } from '$lib/nostr/config';
import { buildHablaLink } from '$lib/nostr/naddr';
import PostView from '$lib/components/PostView.svelte';
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
let { data } = $props();
const dtag = $derived(data.dtag);
let post: NostrEvent | null = $state(null);
let loading = $state(true);
let error: string | null = $state(null);
const hablaLink = $derived(
buildHablaLink({
pubkey: AUTHOR_PUBKEY_HEX,
kind: 30023,
identifier: dtag
})
);
onMount(async () => {
try {
const p = await loadPost(dtag);
loading = false;
if (!p) {
error = `Post "${dtag}" nicht gefunden.`;
} else {
post = p;
}
} catch (e) {
loading = false;
error = e instanceof Error ? e.message : 'Unbekannter Fehler';
}
});
</script>
<nav class="breadcrumb"><a href="/">← Zurück zur Übersicht</a></nav>
<LoadingOrError {loading} {error} {hablaLink} />
{#if post}
<PostView event={post} />
{/if}
<style>
.breadcrumb {
font-size: 0.9rem;
margin-bottom: 1rem;
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,21 @@
import { error, redirect } from '@sveltejs/kit';
import { parseLegacyUrl, canonicalPostPath } from '$lib/url/legacy';
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ url }) => {
const pathname = url.pathname;
// Legacy-Form /YYYY/MM/DD/<dtag>.html/ → Redirect auf /<dtag>/
const legacyDtag = parseLegacyUrl(pathname);
if (legacyDtag) {
throw redirect(301, canonicalPostPath(legacyDtag));
}
// Kanonisch: /<dtag>/ — erster Segment des Pfades.
const segments = pathname.replace(/^\/+|\/+$/g, '').split('/');
if (segments.length !== 1 || !segments[0]) {
throw error(404, 'Seite nicht gefunden');
}
return { dtag: decodeURIComponent(segments[0]) };
};

View File

@ -0,0 +1,80 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent } from '$lib/nostr/loaders';
import { loadPostList } from '$lib/nostr/loaders';
import PostCard from '$lib/components/PostCard.svelte';
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
let posts: NostrEvent[] = $state([]);
let loading = $state(true);
let error: string | null = $state(null);
onMount(async () => {
try {
posts = await loadPostList();
loading = false;
if (posts.length === 0) {
error = 'Keine Posts gefunden auf den abgefragten Relays.';
}
} catch (e) {
loading = false;
error = e instanceof Error ? e.message : 'Unbekannter Fehler';
}
});
// Posts nach Jahr gruppieren (neueste zuerst)
type YearGroup = { year: number; posts: NostrEvent[] };
const groupsByYear = $derived.by<YearGroup[]>(() => {
const byYear = new Map<number, NostrEvent[]>();
for (const p of posts) {
const ts = Number(p.tags.find((t) => t[0] === 'published_at')?.[1] ?? p.created_at);
const year = new Date(ts * 1000).getUTCFullYear();
if (!byYear.has(year)) byYear.set(year, []);
byYear.get(year)!.push(p);
}
return [...byYear.entries()]
.map(([year, p]) => ({ year, posts: p }))
.sort((a, b) => b.year - a.year);
});
</script>
<svelte:head>
<title>Archiv Jörg Lohrer</title>
</svelte:head>
<h1 class="title">Archiv</h1>
<p class="meta">Alle Beiträge, nach Jahr gruppiert.</p>
<LoadingOrError {loading} {error} />
{#each groupsByYear as group (group.year)}
<section class="year-group">
<h2 class="year">{group.year}</h2>
{#each group.posts as post (post.id)}
<PostCard event={post} />
{/each}
</section>
{/each}
<style>
.title {
margin: 0 0 0.3rem;
font-size: 1.8rem;
}
.meta {
color: var(--muted);
margin: 0 0 2rem;
font-size: 0.95rem;
}
.year-group {
margin-bottom: 2.5rem;
}
.year {
font-size: 1.2rem;
font-weight: 600;
margin: 0 0 1rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--border);
color: var(--muted);
}
</style>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import { renderMarkdown } from '$lib/render/markdown';
import impressumRaw from '../../../../content/impressum.md?raw';
// Frontmatter abtrennen, nur Body rendern.
// Toleriert trailing spaces auf den ---/--- trenner-zeilen.
const match = impressumRaw.match(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*\r?\n([\s\S]*)$/);
const body = match ? match[1] : impressumRaw;
const html = renderMarkdown(body);
</script>
<svelte:head>
<title>Impressum Jörg Lohrer</title>
<meta name="robots" content="index, follow" />
</svelte:head>
<article class="impressum">
{@html html}
</article>
<style>
.impressum :global(h1) {
font-size: 1.8rem;
margin: 0 0 1rem;
}
.impressum :global(h2) {
font-size: 1.3rem;
margin: 2rem 0 0.6rem;
}
.impressum :global(h3) {
font-size: 1.05rem;
margin: 1.4rem 0 0.4rem;
}
.impressum :global(p) {
margin: 0 0 1rem;
}
.impressum :global(a) {
color: var(--accent);
}
</style>

View File

@ -0,0 +1,59 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { NostrEvent } from '$lib/nostr/loaders';
import { loadPostsByTag } from '$lib/nostr/loaders';
import PostCard from '$lib/components/PostCard.svelte';
import LoadingOrError from '$lib/components/LoadingOrError.svelte';
let { data } = $props();
const tagName = $derived(data.tagName);
let posts: NostrEvent[] = $state([]);
let loading = $state(true);
let error: string | null = $state(null);
onMount(async () => {
try {
posts = await loadPostsByTag(tagName);
loading = false;
if (posts.length === 0) {
error = `Keine Posts mit Tag "${tagName}" gefunden.`;
}
} catch (e) {
loading = false;
error = e instanceof Error ? e.message : 'Unbekannter Fehler';
}
});
$effect(() => {
document.title = `#${tagName} Jörg Lohrer`;
});
</script>
<nav class="breadcrumb"><a href="/">← Zurück zur Übersicht</a></nav>
<h1 class="tag-title">#{tagName}</h1>
<LoadingOrError {loading} {error} />
{#each posts as post (post.id)}
<PostCard event={post} />
{/each}
<style>
.breadcrumb {
font-size: 0.9rem;
margin-bottom: 1rem;
}
.breadcrumb a {
color: var(--accent);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.tag-title {
margin: 0 0 1.5rem;
font-size: 1.6rem;
}
</style>

View File

@ -0,0 +1,5 @@
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params }) => {
return { tagName: decodeURIComponent(params.name) };
};

20
app/static/.htaccess Normal file
View File

@ -0,0 +1,20 @@
RewriteEngine On
# HTTPS forcieren
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# NIP-05-Verifikation: CORS-Header für .well-known/nostr.json, sonst
# lehnen nostr-clients die verifizierung ab.
<FilesMatch "nostr\.json$">
Header set Access-Control-Allow-Origin "*"
Header set Content-Type "application/json"
</FilesMatch>
# Existierende Datei oder Verzeichnis? Direkt ausliefern.
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Alles andere → SPA-Fallback (SvelteKit mit adapter-static)
RewriteRule ^ /index.html [L]

View File

@ -0,0 +1,14 @@
{
"names": {
"joerglohrer": "4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41"
},
"relays": {
"4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41": [
"wss://relay.damus.io",
"wss://nos.lol",
"wss://relay.primal.net",
"wss://relay.tchncs.de",
"wss://relay.edufeed.org"
]
}
}

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

View File

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 844 B

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

2
app/static/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Allow: /

25
app/svelte.config.js Normal file
View File

@ -0,0 +1,25 @@
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
compilerOptions: {
// Force runes mode for the project, except for libraries. Can be removed in svelte 6.
runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true)
},
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: 'index.html',
precompress: false,
strict: false
}),
alias: {
$lib: 'src/lib'
}
}
};
export default config;

View File

@ -0,0 +1,22 @@
import { expect, test } from '@playwright/test';
test('Home zeigt Hero (Name + Avatar) und neueste Beiträge', async ({ page }) => {
await page.goto('/');
// Hero: Name als h1
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
// Hero: Avatar (lokaler fallback oder nostr-profil)
await expect(page.locator('.hero .avatar')).toBeVisible({ timeout: 15_000 });
// Neueste-Beiträge-Sektion
await expect(page.getByRole('heading', { level: 2, name: /Neueste Beiträge/i })).toBeVisible();
// Mindestens ein Post lädt
await expect(page.locator('a.card').first()).toBeVisible({ timeout: 15_000 });
});
test('Navigation erreicht Archiv und Impressum', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'Archiv', exact: true }).click();
await expect(page.getByRole('heading', { level: 1, name: /Archiv/i })).toBeVisible();
await page.getByRole('link', { name: 'Impressum', exact: true }).first().click();
await expect(page.getByRole('heading', { level: 1, name: /Impressum/i })).toBeVisible();
});

View File

@ -0,0 +1,16 @@
import { expect, test } from '@playwright/test';
test('Einzelpost rendert Titel und Markdown-Body', async ({ page }) => {
await page.goto('/dezentrale-oep-oer/');
// Titel steht einmal als .post-title (H1 außerhalb des Artikels),
// und nochmal im Markdown-Body des Events — wir prüfen den ersten.
await expect(page.locator('h1.post-title')).toBeVisible({ timeout: 15_000 });
await expect(page.locator('h1.post-title')).toContainText('Gemeinsam die Bildungszukunft');
await expect(page.locator('article')).toContainText('Open Educational');
});
test('Legacy-URL wird auf kurze Form umgeleitet', async ({ page }) => {
await page.goto('/2025/03/04/dezentrale-oep-oer.html/');
await expect(page).toHaveURL(/\/dezentrale-oep-oer\/$/);
await expect(page.locator('h1.post-title')).toBeVisible({ timeout: 15_000 });
});

View File

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import { parseLegacyUrl, canonicalPostPath } from '$lib/url/legacy';
describe('parseLegacyUrl', () => {
it('extrahiert dtag aus der Hugo-URL-Form mit Trailing-Slash', () => {
expect(parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/')).toBe(
'dezentrale-oep-oer',
);
});
it('extrahiert dtag aus der Hugo-URL-Form ohne Trailing-Slash', () => {
expect(parseLegacyUrl('/2024/01/26/offenheit-das-wesentliche.html')).toBe(
'offenheit-das-wesentliche',
);
});
it('returned null für die kanonische kurze Form', () => {
expect(parseLegacyUrl('/dezentrale-oep-oer/')).toBeNull();
});
it('returned null für leeren Pfad', () => {
expect(parseLegacyUrl('/')).toBeNull();
});
it('returned null für andere Strukturen', () => {
expect(parseLegacyUrl('/tag/OER/')).toBeNull();
expect(parseLegacyUrl('/some/random/path/')).toBeNull();
});
it('decodiert percent-encoded dtags', () => {
expect(parseLegacyUrl('/2024/05/12/mit%20leerzeichen.html/')).toBe(
'mit leerzeichen',
);
});
it('gibt null zurück bei malformed percent-encoding (crash-sicher)', () => {
expect(parseLegacyUrl('/2024/01/26/%E0.html/')).toBeNull();
});
it('gibt null zurück für leeren dtag', () => {
expect(parseLegacyUrl('/2024/01/26/.html/')).toBeNull();
});
});
describe('canonicalPostPath', () => {
it('erzeugt /<dtag>/ mit encodeURIComponent', () => {
expect(canonicalPostPath('dezentrale-oep-oer')).toBe('/dezentrale-oep-oer/');
});
it('kodiert Sonderzeichen', () => {
expect(canonicalPostPath('mit leerzeichen')).toBe('/mit%20leerzeichen/');
});
});
describe('round-trip parseLegacyUrl → canonicalPostPath', () => {
it('Legacy-URL wird zur kanonischen kurzen Form', () => {
const dtag = parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/');
expect(dtag).not.toBeNull();
expect(canonicalPostPath(dtag!)).toBe('/dezentrale-oep-oer/');
});
});

View File

@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest';
import { renderMarkdown } from '$lib/render/markdown';
describe('renderMarkdown', () => {
it('rendert einfachen Markdown-Text zu HTML', () => {
const html = renderMarkdown('**bold** and *italic*');
expect(html).toContain('<strong>bold</strong>');
expect(html).toContain('<em>italic</em>');
});
it('entfernt <script>-Tags (DOMPurify)', () => {
const html = renderMarkdown('hello <script>alert("x")</script> world');
expect(html).not.toContain('<script>');
});
it('entfernt javascript:-URLs', () => {
const html = renderMarkdown('[click](javascript:alert(1))');
expect(html).not.toMatch(/javascript:/i);
});
it('rendert Links mit http:// und erhält das href', () => {
const html = renderMarkdown('[nostr](https://nostr.com)');
expect(html).toContain('href="https://nostr.com"');
});
it('rendert horizontale Linie aus ---', () => {
const html = renderMarkdown('oben\n\n---\n\nunten');
expect(html).toContain('<hr>');
});
it('rendert fenced code blocks mit hljs-klasse', () => {
const html = renderMarkdown('```js\nconst x = 1;\n```');
expect(html).toContain('<pre>');
expect(html).toContain('<code');
expect(html).toContain('class="hljs');
});
it('rendert GFM tables', () => {
const md = '| a | b |\n|---|---|\n| 1 | 2 |';
const html = renderMarkdown(md);
expect(html).toContain('<table');
expect(html).toContain('<td>1</td>');
});
it('rendert Bilder', () => {
const html = renderMarkdown('![alt](https://example.com/img.png)');
expect(html).toContain('<img');
expect(html).toContain('src="https://example.com/img.png"');
});
// Erweiterte XSS-Matrix — relevant ab Reply-Komponenten (3rd-party Content).
it('entfernt onerror-Attribute auf inline-HTML-img', () => {
const html = renderMarkdown('<img src="x" onerror="alert(1)">');
expect(html.toLowerCase()).not.toContain('onerror');
});
it('entfernt onclick-Attribute auf inline-HTML', () => {
const html = renderMarkdown('<a href="#" onclick="alert(1)">x</a>');
expect(html.toLowerCase()).not.toContain('onclick');
});
it('entfernt iframe-Tags', () => {
const html = renderMarkdown('<iframe src="https://evil.com"></iframe>');
expect(html.toLowerCase()).not.toContain('<iframe');
});
it('entfernt data:text/html-URLs in Links', () => {
const html = renderMarkdown('[x](data:text/html,<script>alert(1)</script>)');
expect(html.toLowerCase()).not.toMatch(/href="data:text\/html/);
});
it('entfernt vbscript:-URLs', () => {
const html = renderMarkdown('<a href="vbscript:msgbox(1)">x</a>');
expect(html.toLowerCase()).not.toContain('vbscript:');
});
it('entfernt script-Tag innerhalb svg', () => {
const html = renderMarkdown('<svg><script>alert(1)</script></svg>');
expect(html.toLowerCase()).not.toContain('<script');
});
});

View File

@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { buildHablaLink } from '$lib/nostr/naddr';
describe('buildHablaLink', () => {
it('erzeugt einen habla.news/a/-Link mit naddr1-Bech32', () => {
const link = buildHablaLink({
pubkey: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41',
kind: 30023,
identifier: 'dezentrale-oep-oer',
relays: ['wss://relay.damus.io'],
});
expect(link).toMatch(/^https:\/\/habla\.news\/a\/naddr1[a-z0-9]+$/);
});
it('ist deterministisch für gleiche Inputs', () => {
const args = {
pubkey: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41',
kind: 30023,
identifier: 'foo',
relays: ['wss://relay.damus.io'],
};
expect(buildHablaLink(args)).toBe(buildHablaLink(args));
});
it('funktioniert ohne relays (optional)', () => {
const link = buildHablaLink({
pubkey: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41',
kind: 30023,
identifier: 'foo',
});
expect(link).toMatch(/^https:\/\/habla\.news\/a\/naddr1[a-z0-9]+$/);
});
it('erzeugt unterschiedliche Links für unterschiedliche Inputs', () => {
const base = {
pubkey: '4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41',
kind: 30023,
relays: [],
};
const a = buildHablaLink({ ...base, identifier: 'foo' });
const b = buildHablaLink({ ...base, identifier: 'bar' });
expect(a).not.toBe(b);
});
});

20
app/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

11
app/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['tests/unit/**/*.{test,spec}.{js,ts}'],
environment: 'jsdom',
globals: true
}
});

View File

@ -1,5 +0,0 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++

BIN
assets/.DS_Store vendored

Binary file not shown.

BIN
assets/icons/.DS_Store vendored

Binary file not shown.

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-bluesky"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M6.335 5.144c-1.654 -1.199 -4.335 -2.127 -4.335 .826c0 .59 .35 4.953 .556 5.661c.713 2.463 3.13 2.75 5.444 2.369c-4.045 .665 -4.889 3.208 -2.667 5.41c1.03 1.018 1.913 1.59 2.667 1.59c2 0 3.134 -2.769 3.5 -3.5c.333 -.667 .5 -1.167 .5 -1.5c0 .333 .167 .833 .5 1.5c.366 .731 1.5 3.5 3.5 3.5c.754 0 1.637 -.571 2.667 -1.59c2.222 -2.203 1.378 -4.746 -2.667 -5.41c2.314 .38 4.73 .094 5.444 -2.369c.206 -.708 .556 -5.072 .556 -5.661c0 -2.953 -2.68 -2.025 -4.335 -.826c-2.293 1.662 -4.76 5.048 -5.665 6.856c-.905 -1.808 -3.372 -5.194 -5.665 -6.856z" /></svg>

Before

Width:  |  Height:  |  Size: 847 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-mastodon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18.648 15.254c-1.816 1.763 -6.648 1.626 -6.648 1.626a18.262 18.262 0 0 1 -3.288 -.256c1.127 1.985 4.12 2.81 8.982 2.475c-1.945 2.013 -13.598 5.257 -13.668 -7.636l-.026 -1.154c0 -3.036 .023 -4.115 1.352 -5.633c1.671 -1.91 6.648 -1.666 6.648 -1.666s4.977 -.243 6.648 1.667c1.329 1.518 1.352 2.597 1.352 5.633s-.456 4.074 -1.352 4.944z" /><path d="M12 11.204v-2.926c0 -1.258 -.895 -2.278 -2 -2.278s-2 1.02 -2 2.278v4.722m4 -4.722c0 -1.258 .895 -2.278 2 -2.278s2 1.02 2 2.278v4.722" /></svg>

Before

Width:  |  Height:  |  Size: 787 B

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
class="nostro"
d="m 21.219688,10.632199 v 9.782376 c 0,0.36788 -0.298537,0.666422 -0.666421,0.666422 h -7.997031 c -0.367884,0 -0.666422,-0.298542 -0.666422,-0.666422 v -1.82178 c 0.03645,-2.233152 0.272682,-4.372276 0.768675,-5.34546 0.297363,-0.58532 0.78748,-0.903839 1.350469,-1.074263 1.063686,-0.319694 2.93013,-0.10108 3.722312,-0.138691 0,0 2.392997,0.09521 2.392997,-1.259967 0,-1.0907187 -1.069562,-1.0049187 -1.069562,-1.0049187 -1.178867,0.03055 -2.076831,-0.04937 -2.658626,-0.278557 C 15.421721,9.1077763 15.388811,8.4049219 15.385285,8.1698534 15.337094,5.4548117 11.333878,5.1292411 7.8066742,5.8027121 3.9503743,6.536127 7.8489842,12.063765 7.8489842,19.44257 v 0.984936 c -0.00706,0.362004 -0.299712,0.654662 -0.6640682,0.654662 h -3.95973 c -0.3678829,0 -0.6664195,-0.298534 -0.6664195,-0.666415 V 3.4966903 c 0,-0.3678822 0.2985366,-0.6664194 0.6664195,-0.6664194 h 3.7223109 c 0.3678822,0 0.6664189,0.2985372 0.6664189,0.6664194 0,0.546534 0.6147044,0.8509478 1.0589839,0.5324305 1.3387143,-0.9590801 3.0570663,-1.4703543 4.9799263,-1.4703543 4.307633,0 7.564508,2.5105327 7.564508,8.0734325 z M 14.068901,8.6470424 c 0,-0.7874805 -0.638211,-1.4256909 -1.425691,-1.4256909 -0.787479,0 -1.425692,0.6382104 -1.425692,1.4256909 0,0.7874809 0.638213,1.4256916 1.425692,1.4256916 0.78748,0 1.425691,-0.6382107 1.425691,-1.4256916 z"
id="nostr"
style="fill:currentColor;fill-opacity:1;stroke-width:0.5" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -17,7 +17,7 @@ Die Inhalte unserer Seiten wurden mit größter Sorgfalt erstellt. Für die Rich
Mein Angebot enthält Links zu externen Webseiten Dritter, auf deren Inhalte ich keinen Einfluss habe. Deshalb kann ich für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Links umgehend entfernen.
### Urheberrecht
Die durch den Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen jedoch nicht der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind sowohl für den privaten, als auch für den kommerziellen Gebrauch unter Namensnennung und der Creative Commons Lizenz [CC BY-SA](https://creativecommons.org/licenses/by-sa/4.0/deed.de) gestattet. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Solltest Du trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten ich um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Inhalte umgehend entfernen.
Die durch den Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Soweit nicht anders angegeben, stelle ich eigene Inhalte und Werke unter der Creative-Commons-Lizenz [CC0 1.0 Universal (Public Domain Dedication)](https://creativecommons.org/publicdomain/zero/1.0/deed.de) zur Verfügung — sie dürfen ohne Rückfrage für jeden Zweck, auch kommerziell, kopiert, bearbeitet, verbreitet und weiterverwendet werden. Eine Namensnennung ist rechtlich nicht erforderlich, aber ich freue mich natürlich, wenn Du mich als Quelle nennst. Wo eine abweichende Lizenz gilt, ist sie beim jeweiligen Inhalt vermerkt. Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Solltest Du trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitte ich um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werde ich derartige Inhalte umgehend entfernen.
### Datenschutz
Die Nutzung meiner Webseite ist in der Regel ohne Angabe personenbezogener Daten möglich. Soweit auf meiner Seiten personenbezogene Daten (beispielsweise Name, Anschrift oder eMail-Adressen) erhoben werden, erfolgt dies, soweit möglich, stets auf freiwilliger Basis. Diese Daten werden ohne Deine ausdrückliche Zustimmung nicht an Dritte weitergegeben. Ich weise darauf hin, dass die Datenübertragung im Internet (z.B. bei der Kommunikation per E-Mail) Sicherheitslücken aufweisen kann. Ein lückenloser Schutz der Daten vor dem Zugriff durch Dritte ist nicht möglich. Der Nutzung von im Rahmen der Impressumspflicht veröffentlichten Kontaktdaten durch Dritte zur Übersendung von nicht ausdrücklich angeforderter Werbung und Informationsmaterialien wird hiermit ausdrücklich widersprochen. Die Betreiber der Seiten behalten sich ausdrücklich rechtliche Schritte im Falle der unverlangten Zusendung von Werbeinformationen, etwa durch Spam-Mails, vor.

View File

@ -11,6 +11,14 @@ author: Jörg Lohrer
slug: "premium-freemium-mium-mium-mium"
lang: de
dir: ltr
images:
- file: my-very-hungry-caterpillar.jpg
role: cover
alt: "Kleine Raupe aus Papier gefaltet, Anspielung auf das Kinderbuch 'Die kleine Raupe Nimmersatt'"
license: "https://creativecommons.org/licenses/by-nc-sa/3.0/"
authors:
- name: "Relly Annett-Baker"
source_url: "https://www.flickr.com/photos/fizzkitten/4454153264/"
---

View File

@ -12,9 +12,7 @@ dir: ltr
# Erlebnispädagogik im Handbuch Jugend Evangelische Perspektiven
Das
[![](http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&ASIN=3847400746&Format=_SL160_&ID=AsinImage&MarketPlace=DE&ServiceVersion=20070822&WS=1&tag=httpwwwjoergl-21)](http://ci-muenster.de/bookshop/artikel/buecher/Bildung-im-Kindes-und-Jugendalter/A40097_Handbuch_Jugend_2013.php)
**Handbuch Jugend Evangelische Perspektiven**, welches 2013 erschienen ist, hat auf Seite 343-345 folgenden Artikel von mir zur Erlebnispädagogik abgedruckt.
Das [**Handbuch Jugend Evangelische Perspektiven**](http://ci-muenster.de/bookshop/artikel/buecher/Bildung-im-Kindes-und-Jugendalter/A40097_Handbuch_Jugend_2013.php), welches 2013 erschienen ist, hat auf Seite 343-345 folgenden Artikel von mir zur Erlebnispädagogik abgedruckt.
[CC BY](http://creativecommons.org/licenses/by/2.0/de/) Jörg Lohrer
## Erlebnispädagogik
@ -44,14 +42,10 @@ Die Wirkungsforschung erlebnispädagogischer Lernarrangements untersucht deren S
Im Kontext evangelischer Jugendbildung stellt sich bei der Verwendung erlebnispädagogischer Methoden immer auch die Frage nach einer Verknüpfung von christlichen Inhalten mit der handlungsorientierten Pädagogik. Die Assoziation von Natur und Schöpfung ist beispielsweise ebenso naheliegend wie die Reflexion von religiösen Erfahrungen in erlebnispädagogischen Lernsettings. Inwiefern sich dabei Unverfügbares inszenieren lässt, bleibt dabei ebenso eine Herausforderung, wie bei der oben beschriebenen Wirkungsforschung. In den vergangenen Jahren haben sich einige Konzepte zur Erprobung erlebnispädagogischer Methoden im christlichen Kontext entwickelt, die zunehmend auch theologisch und religionsdidaktisch reflektiert werden. Eine überregionale Organisation dieser Einzelinitiativen auf Bundesebene wäre der nächste Schritt hin zu einem evangelischen Profil erlebnispädagogischer Jugendbildungsarbeit.
### Literatur
![](http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&ASIN=3866870493&Format=_SL160_&ID=AsinImage&MarketPlace=DE&ServiceVersion=20070822&WS=1&tag=httpwwwjoergl-21)
Großer, Achim/Oberländer, Rainer/Lohrer, Jörg/Wiedmayer, Jörg (2005/2011): Sinn gesucht Gott erfahren. Erlebnispädagogik im christlichen Kontext (Band 1 (2005)/Band 2 (2011)). Stuttgart: Buch und Musik.
![](http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&ASIN=3497022934&Format=_SL160_&ID=AsinImage&MarketPlace=DE&ServiceVersion=20070822&WS=1&tag=httpwwwjoergl-21)
Heckmair, Bernd/Michl, Werner (2008): Erleben und Lernen. Einführung in die Erlebnispädagogik. München: Reinhardt.
![](http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&ASIN=3940562866&Format=_SL160_&ID=AsinImage&MarketPlace=DE&ServiceVersion=20070822&WS=1&tag=httpwwwjoergl-21)
Reiners, Annette (2007): Praktische Erlebnispädagogik. Augsburg: ZIEL.
![](http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&ASIN=3936369348&Format=_SL160_&ID=AsinImage&MarketPlace=DE&ServiceVersion=20070822&WS=1&tag=httpwwwjoergl-21)
Pum, Viktoria/Pirner, Manfred L./Lohrer, Jörg (Hrsg.) (2011): Erlebnispädagogik im christlichen Kontext. Dokumentation einer Tagung der Evangelischen Akademie Bad Boll, 2. bis 4. März 2009. Bad Boll: Evangelische Akademie.
- Großer, Achim/Oberländer, Rainer/Lohrer, Jörg/Wiedmayer, Jörg (2005/2011): Sinn gesucht Gott erfahren. Erlebnispädagogik im christlichen Kontext (Band 1 (2005)/Band 2 (2011)). Stuttgart: Buch und Musik.
- Heckmair, Bernd/Michl, Werner (2008): Erleben und Lernen. Einführung in die Erlebnispädagogik. München: Reinhardt.
- Reiners, Annette (2007): Praktische Erlebnispädagogik. Augsburg: ZIEL.
- Pum, Viktoria/Pirner, Manfred L./Lohrer, Jörg (Hrsg.) (2011): Erlebnispädagogik im christlichen Kontext. Dokumentation einer Tagung der Evangelischen Akademie Bad Boll, 2. bis 4. März 2009. Bad Boll: Evangelische Akademie.
#### Links
- Bundesverband Individual- und Erlebnispädagogik e.V. (BE): [https://www.bundesverband-erlebnispaedagogik.de/](https://www.bundesverband-erlebnispaedagogik.de/)

View File

@ -11,6 +11,31 @@ author: Jörg Lohrer
slug: "telegram-octopi"
lang: de
dir: ltr
images:
- file: octopi1.png
role: cover
alt: "Screenshot der OctoPrint-Plugin-Verwaltung während der Installation des Telegram-Plugins — Fortschrittsanzeige läuft"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: octopi2.png
alt: "Screenshot der Konfigurationsmaske des OctoPrint-Telegram-Plugins mit Eingabefeld für den Telegram-Bot-Token"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: octopi3.png
alt: "Screenshot der OctoPrint-Telegram-Plugin-Oberfläche nach erfolgreichem Token-Eintrag — Benutzerliste wird angezeigt, Rechte fehlen noch"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: octopi4.png
alt: "Screenshot der Benutzer-Rechte-Konfiguration mit gesetzten Häkchen bei 'Command' und 'Notify'"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -11,6 +11,45 @@ author: Jörg Lohrer
slug: "lutherkuerbis"
lang: de
dir: ltr
images:
- file: kuerbis-titelbild.jpg
role: cover
alt: "Fertig geschnitzter Kürbis mit dem Muster einer Lutherrose, innen beleuchtet — glüht warm in der Dunkelheit"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: lutherrose.png
alt: "Schwarz-weiße Vektorgrafik der Lutherrose: Kreuz im Herzen, umgeben von fünfblättriger Rose in einem Ring — als Schnitzschablone aufbereitet"
caption: "Vektorisierte Schablone, abgeleitet von einer Fotovorlage aus dem Web"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
modifications: "Vektorisierung per online-convert.com aus gemeinfreier Fotovorlage; Originalurheber der Fotovorlage unbekannt"
- file: kuerbis-aufschneiden.jpg
alt: "Hände schneiden mit großem Messer den Deckel von einem orangenen Kürbis ab"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: kuerbis-entkernen.jpg
alt: "Mit einem Löffel wird das Fruchtfleisch und die Kerne aus dem Inneren des aufgeschnittenen Kürbis herausgekratzt"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: schablone-aufbringen.jpg
alt: "Papier-Schablone mit Lutherrosen-Motiv wird auf die Außenhaut des entkernten Kürbis geklebt"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: kuerbis-ausschneiden.jpg
alt: "Mit einem Schnitzwerkzeug wird die Lutherrose entlang der Schablone aus der Kürbishaut herausgeschnitten"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -7,10 +7,23 @@ cover:
image: cura-plugin-change-filment-at-z.png
tags: [ "QR-Code", "3DDruck" ]
date: "2019-03-26"
slug: "Pflanzenschild-QR-Code"
slug: "pflanzenschild-qr-code"
author: Jörg Lohrer
lang: de
dir: ltr
images:
- file: cura-plugin-change-filment-at-z.png
role: cover
alt: "Screenshot des Cura-Slicers mit aktiviertem 'Change Filament at Z'-Plugin — Konfiguration eines Filamentwechsels in bestimmten Layern"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: qr-code-pflanzenschild.jpg
alt: "Dreieckiges 3D-gedrucktes Pflanzenschild mit aufgedrucktem zweifarbigem QR-Code, steckt in einem Pflanztopf"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -15,6 +15,48 @@ toc: true
toc_label: "Inhaltsverzeichnis"
toc_icon: "vr-cardboard"
toc_sticky: "true"
images:
- file: 04-aframe.jpg
role: cover
alt: "Screenshot einer A-Frame-WebVR-Szene: 3D-Objekte in einem Browser-Viewport, erstellt mit A-Frame-Framework"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
source_url: "https://codepen.io/joerglohrer/full/dyXQqWG"
- file: 01-immersion-wikipedia.jpg
alt: "Screenshot des Wikipedia-Artikels 'Immersive learning' mit Einstiegsdefinition"
license: UNKNOWN
authors: UNKNOWN
source_url: "https://en.wikipedia.org/wiki/Immersive_learning"
- file: 02-mittelalterliche-kirche.jpg
alt: "Screenshot eines 3D-Modells einer mittelalterlichen Kirche (Calatrava la Nueva, Spanien) auf Sketchfab, erstellt aus 76 Laser-Scans und 4100 Fotos"
license: "https://creativecommons.org/licenses/by-nc/4.0/"
authors: UNKNOWN
source_url: "https://sketchfab.com/3d-models/medieval-church-calatrava-la-nueva-spain-171a047c08bc4dd588cca5ac744e8065"
- file: 03-avatare-erstellen.jpg
alt: "Screenshot der Avatar-Erstellung im Ready Player Me Web-Interface"
license: UNKNOWN
authors: UNKNOWN
- file: 05-pupillendistanz.jpg
alt: "Screenshot der iOS-App 'EyeMeasure' bei der Messung des Pupillenabstands mittels iPhone-Kamera"
license: UNKNOWN
authors: UNKNOWN
- file: 06-vr-adapter-3ddruck.jpg
alt: "3D-gedruckter Adapter zur Befestigung einer VIVE Deluxe Audio Strap an der Oculus Quest 2, frisch aus dem 3D-Drucker"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 07-vive-straps-3ddruck.jpg
alt: "3D-gedruckte Halterungen der VIVE Deluxe Audio Strap, montiert an der Oculus Quest 2"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -8,9 +8,52 @@ cover:
tags: [ "ACF", "WordPress", "Formulare", "JSON", "Plugin" ]
date: "2021-11-17"
author: Jörg Lohrer
slug: "WordPress-Werkstatt"
slug: "wordpress-werkstatt"
lang: de
dir: ltr
images:
- file: 04-termine-neu.png
role: cover
alt: "Screenshot der WordPress-Beitragsübersicht mit eingefügtem Shortcode [relilab_termine], der eine Terminliste als Block rendert"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 01-json-import.png
alt: "Screenshot der ACF-Plugin-Oberfläche beim Import einer JSON-Datei mit Feldgruppen-Definitionen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 02-terminfelder.png
alt: "Screenshot eines WordPress-Beitrags mit zwei neuen ACF-Terminfeldern 'Startet am' und 'Endet am' als Datum-/Zeit-Picker"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 03-kategorien.png
alt: "Screenshot der WordPress-Kategorieverwaltung mit neu angelegter Kategorie 'Termine' samt Unterkategorien"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 05-php-storm.png
alt: "Screenshot der PhpStorm-IDE mit geöffneter PHP-Datei zum add_shortcode()-Aufruf"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 06-termine-listen.png
alt: "Screenshot des PHP-Codes für die Funktion 'termineAusgeben' mit get_posts()-Abfrage und Shortcode-Registrierung"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 07-external-library.png
alt: "Screenshot der PhpStorm-Konfiguration zur Einbindung von WordPress als External Library für Auto-Complete"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -15,6 +15,13 @@ toc: true
toc_label: "Inhaltsverzeichnis"
toc_icon: "futbol"
toc_sticky: "true"
images:
- file: bibelfussball1.png
role: cover
alt: "Tafel-Skizze eines Fußballfeldes mit Mittellinie, Strafräumen und zwei Toren — Magnetknopf markiert die aktuelle Ballposition"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -11,6 +11,31 @@ author: Jörg Lohrer
slug: "moodle-iomad-linux"
lang: de
dir: ltr
images:
- file: title-gif.gif
role: cover
alt: "Animiertes Titelbild des Artikels zur Moodle-Server-Installation mit Iomad unter Ubuntu"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 01-netzwerkbruecke.png
alt: "Screenshot der VirtualBox-Netzwerkeinstellungen mit aktivierter Netzwerkbrücke für die Ubuntu-VM"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 02-hosts-eintragen.png
alt: "Terminal-Screenshot mit geöffneter /etc/hosts-Datei im nano-Editor, neuer Eintrag 'moodle.local' wird hinzugefügt"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: "03-config generieren.png"
alt: "Screenshot des Moodle-Installationsassistenten beim automatischen Generieren der config.php"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -8,13 +8,200 @@ cover:
image: 29-autostartordner.jpg
tags: [ "Ubuntu", "Google Remote Desktop", "OBS", "Zoom", "relilabtutorial" ]
date: "2022-03-19"
slug: "OB-virtualcam"
slug: "ob-virtualcam"
lang: de
dir: ltr
toc: true
toc_label: "Inhaltsverzeichnis"
toc_icon: "house-laptop"
toc_sticky: "true"
images:
- file: 29-autostartordner.jpg
role: cover
alt: "Screenshot des Windows-Autostart-Ordners mit verknüpften OBS- und Zoom-Startlinks für automatischen Start beim Systemstart"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 01-deutsche-tastatur-ubuntu.png
alt: "Screenshot der Ubuntu-Terminal-Dialog zur Konfiguration der deutschen Tastatur via dpkg-reconfigure"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 02-chrome-remote-desktop.png
alt: "Screenshot der Chrome-Remote-Desktop-Installation im Ubuntu-Terminal"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 03-status-chrome-remote.png
alt: "Screenshot des systemctl-Status des chrome-remote-desktop-Dienstes als 'active (running)'"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 04-remotezugriff.png
alt: "Screenshot der Chrome-Remote-Desktop-Konfigurationsseite mit SSH-Befehl und PIN-Eingabe"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 05-systemctl-status.png
alt: "Screenshot der systemctl-status-Ausgabe für chrome-remote-desktop mit aktivem Dienst"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 06-cannot-open-video-device.png
alt: "Terminal-Screenshot der Fehlermeldung 'Cannot open device /dev/video0' bei v4l2-ctl --list-devices"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 07-jetzt-v412-ctl.png
alt: "Terminal-Screenshot der erfolgreichen v4l2-ctl-Geräteliste nach Installation von v4l2loopback"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 08-dummy-video-device.png
alt: "Terminal-Screenshot nach Reboot: virtuelle Kamera fehlt, Dummy-Video-Device muss neu geladen werden"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 09-relilab-technical-host.png
alt: "Screenshot der Chrome-Remote-Desktop-Geräteübersicht mit dem VM-Eintrag 'relilab-technical-host'"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 10-pin-remote-desktop.png
alt: "Screenshot des Chrome-Remote-Desktop-PIN-Eingabefelds für die Remote-Verbindung"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 11-keyboard-tastatur-umstellen.png
alt: "Screenshot der Linux-Keyboard-Einstellungen mit Umstellung auf deutsche Tastaturbelegung"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 12-apps-verknuepfen.png
alt: "Screenshot der Cinnamon-Desktop-Umgebung mit Drag-and-Drop-Verknüpfung von Anwendungen auf den Desktop"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 13-startvirtualcam.png
alt: "Screenshot der OBS-Verknüpfung mit dem Zusatzparameter --startvirtualcam im Startbefehl"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 14-OBS-deutsch-umstellen.png
alt: "Screenshot der OBS-Studio-Einstellungen beim Umschalten der Benutzeroberfläche auf Deutsch"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 15-obs-mit-virtual-cam-starten.png
alt: "Screenshot der OBS-Startbefehl-Konfiguration mit --startvirtualcam-Parameter für automatischen Kamera-Start"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 16-startup-application.png
alt: "Screenshot der Cinnamon-Startup-Applications-Verwaltung mit neu hinzugefügtem OBS-Eintrag"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 17-i-will-only-be-using-OBS.png
alt: "Screenshot des OBS-Auto-Configuration-Wizard mit ausgewählter Option 'I will only be using the virtual camera'"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 18-video1920.png
alt: "Screenshot der OBS-Video-Einstellungen mit Auflösung 1920x1080"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 19-szenensammlung-importieren-OBS.png
alt: "Screenshot des OBS-Menüs 'Szenensammlung importieren' mit Auswahl einer JSON-Datei"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 20-chrome-einrichten.png
alt: "Screenshot des Ubuntu-Keyring-Passwort-Dialogs beim ersten Chrome-Start"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 21-chrome-standard.png
alt: "Screenshot der Google-Chrome-Einstellungen mit gesetzter Option 'Als Standardbrowser festlegen'"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 22-chrome-anmeldung.png
alt: "Screenshot der Google-Account-Anmeldung in Chrome mit aktiviertem Sync"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 23-zoom-anmeldung.png
alt: "Screenshot der Zoom-Client-Anmeldemaske unter Linux"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 24-zoom-sprache-aendern.png
alt: "Screenshot des Zoom-Tray-Menüs mit Sprachauswahl-Untermenü zur Umstellung auf Deutsch"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 25-slides-emojis.png
alt: "Screenshot einer Präsentationsfolie im Chrome-Browser mit fehlenden Emoji-Zeichen als leere Platzhalter"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 26-keyring-problem.png
alt: "Screenshot der Ubuntu-GUI-Fehlermeldung beim Versuch, sich als Root einzuloggen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 27-startvirtualcam-verknuepft-OBS.jpg
alt: "Screenshot der Windows-Eigenschaften einer OBS-Desktop-Verknüpfung mit --startvirtualcam-Parameter"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 28-shell-startup.png
alt: "Screenshot des Windows-Run-Dialogs mit Befehl 'shell:startup' zum Öffnen des Autostart-Ordners"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: v412-ctl-fehlermeldung.png
alt: "Terminal-Screenshot der v4l2-ctl-Fehlermeldung beim Öffnen des Video-Gerätes"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: virtueller-desktop-titelbild.jpg
alt: "Stilisiertes Titelbild: virtueller Desktop-Arbeitsplatz mit mehreren Bildschirmen und Remote-Verbindung"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -11,6 +11,43 @@ date: "2023-02-26"
slug: "jojos-schoko-zimt-schnecken"
lang: de
dir: ltr
images:
- file: schneckennudeln-titel.jpg
role: cover
alt: "Goldbraun gebackene Hefeschnecken in einer Kuchenform, Titelbild des Rezepts"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Hefeteig-mit-Fuellung.jpg
alt: "Ausgerollter Hefeteig, bestrichen mit cremiger Kakao-Zimt-Zucker-Füllung, bereit zum Einrollen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: 16-Schneckennudeln.jpg
alt: "16 dicht an dicht aufgestellte, rohe Hefeschnecken in einer runden Kuchenform"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: hefeschnecken-in-capelle-backform.jpg
alt: "Gegangene, mit Eimilch bestrichene Hefeschnecken in Kapellen-Backform, bereit für den Ofen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: schneckennudeln-im-ofen.jpg
alt: "Hefeschnecken im Ofen während des Backens, Oberseite beginnt goldbraun zu werden"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: schneckennudeln-fertig.jpg
alt: "Fertig gebackene, goldbraune Hefeschnecken in der Kuchenform, bereit zum Servieren"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -11,6 +11,70 @@ date: "2023-03-23"
slug: "gleichnis-vom-saemann"
lang: de
dir: ltr
images:
- file: saemann-title.jpg
role: cover
alt: "Titelbild zum Gleichnis vom Sämann: Collage der fünf KI-generierten Illustrationen im Stil von Eric Carle"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "Collage aus Midjourney-generierten Bildern im Stil von Eric Carle, Prompts siehe Artikel"
- file: bild1-saemann.jpeg
alt: "Illustration im Stil von Eric Carle: Ein freundlicher Bauer streut Samen in einem offenen Feld, im Hintergrund vier Böden — felsig, dornig, vogelreich und fruchtbar"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5, Prompt im Stil von Eric Carle"
- file: bild1-alternativ-saemann.jpeg
alt: "Alternative Illustration im Stil von Eric Carle: Bauer beim Säen mit verschiedenen Bodenarten im Hintergrund"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5, alternative Variante zu Bild 1"
- file: bild2-saemann.jpeg
alt: "Illustration im Stil von Eric Carle: Kleine, schwache Pflanzen, die mit wenig Erde auf felsigem Boden zu wachsen beginnen"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5"
- file: bild2-alternativ-saemann.jpeg
alt: "Alternative Illustration im Stil von Eric Carle: Keimende Pflanzen auf steinigem Grund"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5, alternative Variante zu Bild 2"
- file: bild3-saemann.jpeg
alt: "Illustration im Stil von Eric Carle: Junge Pflanzen werden von Dornen umklammert und erstickt"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5"
- file: bild4-saemann.jpeg
alt: "Illustration im Stil von Eric Carle: Fröhliche Vögel picken Samen vom Boden und fressen sie, bevor sie keimen können"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5"
- file: bild5-saemann.jpeg
alt: "Illustration im Stil von Eric Carle: Große, gesunde Pflanzen tragen reiche Früchte auf fruchtbarem Boden, der Bauer steht lächelnd daneben"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v5"
- file: screen-chatgpt-saemann.png
alt: "Screenshot des ChatGPT-Dialogs: Eingabe der Anfrage zum Gleichnis vom Sämann für einen 8-Jährigen und KI-generierte Antwort in fünf Bildbeschreibungen"
license: "https://creativecommons.org/licenses/by-sa/3.0/de/"
authors:
- name: "Jörg Lohrer"
modifications: "Screenshot einer ChatGPT-4-Interaktion, Prompt vom Autor verfasst"
---
# Das Gleichnis vom Sämann

View File

@ -8,9 +8,46 @@ cover:
image: Hefefreuden.jpg
tags: [ "Dampfnudel", "Hefeteig", "Hefezopf" ]
date: "2023-04-07"
slug: "Dampfnudeln"
slug: "dampfnudeln"
lang: de
dir: ltr
images:
- file: Hefefreuden.jpg
role: cover
alt: "Titelbild: Dampfnudeln und Hefezopf auf einem Tisch, frisch aus Dampfgarer und Ofen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Hefeteig.jpg
alt: "Aufgegangener Hefeteig in einer Rührschüssel, glatt und elastisch, nach 30 Minuten Ruhezeit"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Dampfnudeln-auf-Lochblech.jpg
alt: "Sechs runde Hefeteigstücke zum Dampfgaren auf einem gelochten Dampfgarblech"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Dampfnudeln-im-Dampfgarer.jpg
alt: "Gegarte, aufgegangene Dampfnudeln im geöffneten Dampfgarer, glänzend und flaumig"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Dampfnudel-mit-Vanillesosse.jpg
alt: "Dampfnudel auf Teller angerichtet, übergossen mit goldgelber Vanillesoße"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: Hefezopf.jpg
alt: "Frisch gebackener, dreifach geflochtener Hefezopf, goldbraun glänzend nach dem Einpinseln mit Ei"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---

View File

@ -11,7 +11,25 @@ cover:
tags: [ "WordPress", "Padlet", "Kanban", "TaskCards", "horizontales Scrollen" ]
lang: de
dir: ltr
images:
- file: wordpress-horizontales-scrollen.gif
role: cover
alt: "Animierter Screenshot: WordPress-Seite mit horizontal scrollbaren Spalten, die Beiträge im Kanban-Stil nebeneinander zeigen"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: spalten-als-posts-block.png
alt: "Screenshot des Stackable 'posts block'-Plugins in WordPress mit Spaltenansicht nach Kategorien"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
- file: posts-per-drag-and-drop-sortieren.png
alt: "Screenshot der WordPress-Beitragsliste mit aktiviertem 'Intuitive Custom Post Order'-Plugin — Beiträge werden per Drag & Drop sortiert"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---
Eine **Sammlung von Liedern für den Religionsunterricht** ist auf Instagram in Zusammenarbeit von [@relimomente](https://www.instagram.com/relimomente/) [@ezpz.lemon.sqz](https://www.instagram.com/ezpz.lemon.sqz) und [@colibri260](https://www.instagram.com/colibri260) entstanden und wurde zunächst **[hier auf TaskCards](https://www.taskcards.de/#/board/16af7347-ec26-468e-a093-34549dd2dae3/view)** veröffentlicht.

View File

@ -11,6 +11,14 @@ author: Jörg Lohrer
tags: [ "Offenheit", "OER", "WordPress", "Nextcloud", "Element", "Community" ]
lang: de
dir: ltr
images:
- file: offenheit-wesentlich.png
role: cover
alt: "KI-generierte Aquarell-Illustration: Silhouetten von Menschen aller Geschlechter und Altersgruppen, die ineinander übergehen und sich überlappen — Symbol einer Community of Trust"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v6.0, Prompt: A Community of Trust based on Openness, silhouettes of people of all genders and ages that merge into each other and overlap, watercolors --v 6.0 --seed 1235164279"
---
# Offenheit - das Wesentliche

View File

@ -1,7 +1,7 @@
---
layout: post
title: "BottomUp -> MarkDown - 5V-Power für deine OER!"
slug: "BottomUp-MarkDown"
slug: "bottomup-markdown"
description: Open Educational Resources mit MarkDown
image: bottomup-markdown.png
cover:
@ -11,6 +11,13 @@ author: Jörg Lohrer
tags: [ "Offenheit", "OER", "MarkDown", "Community" ]
lang: de
dir: ltr
images:
- file: bottomup-markdown.png
role: cover
alt: "Titelbild zur OER-Camp-Session 'BottomUp MarkDown' — Symbol für die 5V-Freiheiten von Open Content in Verbindung mit der Markdown-Sprache"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
---
## Eine Session-Idee auf dem #OERcamp 24 in Hamburg

View File

@ -1,7 +1,7 @@
---
layout: post
title: "KIBedenken - Bewusstsein"
slug: "KIBedenken - Bewusstsein"
slug: "kibedenken-bewusstsein"
description: Intelligenz oder Bewusstsein?
image: kibedenken.png
cover:
@ -27,6 +27,15 @@ creator:
name: Comenius-Institut
id: https://ror.org/025e8aw85
type: Organization
images:
- file: kibedenken.png
role: cover
alt: "Ein junger Roboterjunge mit gesenktem Kopf betrachtet seine Spiegelung im Wasser, im fotorealistischen Stil einer Canon EOS 5D Mark IV"
caption: "Referenziert auf Narziss aus der griechischen Mythologie und die Illustration von Caravaggio (siehe [Wikipedia #Narziss](https://de.wikipedia.org/wiki/Narziss#))"
license: "https://creativecommons.org/publicdomain/zero/1.0/deed.de"
authors:
- name: "Jörg Lohrer"
modifications: "KI-generiert mit Midjourney v6.0, Prompt: photographed with the Canon EOS 5D Mark IV a young robot boy with his head down, looking at his reflection in water --v6.0"
---
# #KIBedenken - Bewusstsein

View File

@ -1,7 +1,7 @@
---
layout: post
title: "Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter"
slug: "Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter"
slug: "dezentrale-oep-oer"
description: "Einladung zum offenen Denken und Handeln in der Bildungsgemeinschaft. Der
Beitrag diskutiert, warum eine dezentrale Infrastruktur für Open Educational
Resources (OER) und Open Educational Practices (OEP) notwendig ist, um
@ -60,6 +60,16 @@ learningResourceType:
educationalLevel:
- https://w3id.org/kim/educationalLevel/level_A
datePublished: '2025-03-04'
images:
- file: dezentrale-oep-oer.png
role: cover
alt: "Ein in den Sand gezeichneter Strauß mit den Buchstaben 'OER' — Sinnbild für offene Bildung und freien Wissensaustausch, gleichzeitig Wortspiel-Verbindung zu Nostr (Ostrich = Strauß)"
caption: "Analog zum Ichthys-Fisch als geheimem Erkennungszeichen: Symbol einer Gemeinschaft, die Wissen offen, unabhängig und widerstandsfähig teilt"
license: "https://creativecommons.org/licenses/by/4.0/deed.de"
authors:
- name: "Jörg Lohrer"
- name: "Steffen Rörtgen"
- name: "Bastian Granas"
---
# Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter

View File

@ -0,0 +1,98 @@
---
layout: post
title: "KI-Kompetenzen in der religionsbezogenen Bildung Mitmachplattform"
slug: "ki-kompetenzen-mitmachplattform"
date: 2025-04-16
description: "Die Mitmachplattform 'KI-Kompetenzen für die religionsbezogene Bildung' ist ein offenes Lernangebot für Lehrkräfte, Hochschulangehörige und Engagierte in der kirchlichen Bildungsarbeit. Zentrales Element ist ein Kreismodell mit vier Kompetenzbereichen: Verstehen, Anwenden, Reflektieren, Gestalten. Inhalte sind über Nostr vernetzbar."
image: https://r2.primal.net/cache/e/2b/d0/e2bd042a0dd1961b5a6b91562d313fb33b4527875dee87b81eebab7e125d356f.png
tags:
- KI
- KI-Kompetenzen
- Religionspädagogik
- Mitmachplattform
- Nostr
- relilab
- OER
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
# Willkommen zur Mitmachplattform: KI-Kompetenzen für die religionsbezogene Bildung
Diese Plattform ist ein offenes, gemeinschaftliches Lernmodul für alle, die KI-Kompetenzen im Kontext der religionsbezogenen Bildung entdecken, vertiefen und weitergeben möchten.\
Hier können **Religionslehrende**, **Hochschulangehörige** und **ehrenamtlich Engagierte** gemeinsam Inhalte entwickeln, teilen und weiterentwickeln.
***
## Was ist das Ziel?
Wir schaffen eine offene Lernumgebung, in der Materialien zu KI-Kompetenzen für Schule, Hochschule und außerschulische Jugendarbeit kollaborativ entstehen.\
Die Plattform richtet sich an Lehrkräfte, Fortbildner:innen und Ehrenamtliche, die KI-Themen in der religiösen Bildung verantwortungsvoll erschließbar machen wollen.
***
## Unser Kompetenzmodell
Im Zentrum steht ein **Kreismodell** mit vier Kompetenzbereichen, die auf drei Niveaustufen ausdifferenziert sind:\
**Verstehen**, **Anwenden**, **Reflektieren** und **Gestalten**.\
Diese Bereiche bauen aufeinander auf, beeinflussen sich gegenseitig und werden durch die zentrale Kompetenz **AI Leadership** verbunden.
| Kompetenzbereich | Beschreibung |
| -------------------------------------------------------------------- | ---------------------------------------- |
| 🟢 [VERSTEHEN](https://ki-kompetenzen.npub.pro/tag/verstehen/) | Grundlagen und Mechanismen von KI |
| 🔵 [ANWENDEN](https://ki-kompetenzen.npub.pro/tag/anwenden/) | KI-Tools im Lernprozess nutzen |
| 🟠 [REFLEKTIEREN](https://ki-kompetenzen.npub.pro/tag/reflektieren/) | Kritische Auseinandersetzung mit KI |
| 🟣 [GESTALTEN](https://ki-kompetenzen.npub.pro/tag/gestalten/) | Aktive Weiterentwicklung von KI-Systemen |
Jeder Lerninhalt ist einem Kompetenzbereich (und wenn möglich einer Niveaustufe) zugeordnet farblich und inhaltlich klar erkennbar.
![](https://r2.primal.net/cache/e/2b/d0/e2bd042a0dd1961b5a6b91562d313fb33b4527875dee87b81eebab7e125d356f.png)
***
## So funktioniert die Mitmachplattform
* **Offene Inhalte:** Alle Materialien werden als Markdown-Dateien über Nostr gepflegt.
* **Mitmachen leicht gemacht:** Egal wo auf Nostr du publizierst, mit den Hashtags #relilab und dem jeweiligen Kompetenzbereich als Schlagwort (also #verstehen, #anwenden, #gestalten, #reflektieren) finden wir diese Hashtagkombination und können die Webseite mit deiner Idee oder Weiterentwicklung ergänzen.
* **Anleitungen:** Eine eigene Seite erklärt Schritt für Schritt, wie du dich beteiligen kannst ganz ohne Vorkenntnisse im Programmieren.
* **Suchfunktion:** Finde gezielt Inhalte nach Kompetenzbereich, Niveaustufe oder Schlagworten.
***
## Für wen ist diese Plattform?
* **Lehrkräfte** an Schulen und Hochschulen in religionsbezogenen Fächern
* **Ehrenamtliche** und Multiplikator:innen in der kirchlichen Jugendarbeit
* **Fort- und Weiterbildner:innen** im Bereich KI und Religion
***
## Mitmachen & Teilen
\&gt; **"Sharing is Caring":**\
\&gt; Unser KI-Kompetenzmodell und alle Lernmaterialien stehen unter der offenen Lizenz \[CC-BY 4.0].\
\&gt; Du darfst sie frei nutzen, anpassen und weitergeben mit Nennung der Urheber:innen.
***
## Starte jetzt!
***
***Gemeinsam gestalten wir KI-Kompetenzen für eine reflektierte, verantwortungsvolle und kreative religiöse Bildung im KI-Zeitalter.***
***
**Kontakt & Feedback:**\
Für Fragen, Anregungen oder Feedback
[https://ki-kompetenzen.npub.pro](https://ki-kompetenzen.npub.pro/)
\#OER #relilab
antworte einfach[ auf diese Nachricht](https://primal.net/e/nevent1qqsgpu7laypuzwvpyyuh9qwpmc43hgazh385mmd0qc0hapmsdfeqhms207uck) oder komm' in den Matrix/Element-Raum https://matrix.to/#/#relilab-ki:rpi-virtuell.de> zum Online-Austausch!

View File

@ -0,0 +1,100 @@
---
layout: post
title: "Bibel-Selfies"
slug: "bibel-selfies"
date: 2025-04-17
description: "Bibel-Selfies mit Midjourney — Prompts und Ergebnisse zu biblischen Figuren als Selfies aus der Ich-Perspektive."
image: https://cdn.midjourney.com/41d706d7-15ed-40ca-b507-5a2d727e312f/0_2.png
tags:
- KI-Bilder
- Midjourney
- Bibel
- Selfie
- Religionspädagogik
- relilab
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
# Bibel-Selfies
<iframe title="Pixelfed Post Embed" src="https://pixelfed.social/p/joerglohrer/724534050330570248/embed?caption=true&likes=false&layout=full" class="pixelfed__embed" style="max-width: 100%; border: 0" width="400" height="600" allowfullscreen="allowfullscreen"></iframe><script async defer src="https://pixelfed.social/embed.js"></script>
## Prompts
### Eva, Schlange, Apfel und Adam
![](https://pxscdn.com/publicgit/m/_v2/681776049104369468/0e43b1202-69768b/uIBBxUFlGmav/yn9gQAvrT3qAamt2WzicttXBaTzhtlY3BsqGC4Pp.png)
```
A selfie of a woman resembling eve in the time of old testament, blurred body, holding an apple, kneeling in front adam. he has a shocked expression with his mouth open and wide eyes, evoking a sense of both fear and surprise. The scene appears surreal, with a huge snake behind her. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with adam and eve, possibly at a place like garden eden. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudha --v 6.0
```
### Tochter des Pharao mit Mose
![](https://pxscdn.com/public/m/_v2/681776049104369468/0e43b1202-69768b/FdEAnn1B6Lct/QcjQI2bebUArUnnYZHsipjvfkp8YE9CPYXcEGgCm.png)
```A selfie of the biblical figure moabite woman with her baby in front of an oasis. She is wearing traditional and has black hair. The background shows water from the desert oasis and grasses around it. In the Background a wicker basket on the water. The photo was taken in the style of a selfie shot with GoPro camera```
### Simon Petrus
![](https://cdn.midjourney.com/1ca43d27-52a5-42cb-863f-9ed33729ebeb/0_3.png)
```A selfie of a man resembling Simon Petrus, wearing a white robe, surrounded by waves and thunderstorm. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with many waves behind him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with jesus at the dead sea, possibly at a place like the sea. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary --v 6.0```
### Zachäus auf dem Baum
![](https://pxscdn.com/public/m/_v2/681776049104369468/0e43b1202-69768b/cfl74pxu6bg6/Z3A4Y9gp8l14gw6BKY25WgjEAPNaUKZIeggq40g6.png)
```A selfie of a man resembling a roman in the time of jesus, wearing a glamorous robe, surrounded by the crown of a tree. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with many leaves behind him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with jesus walking by, possibly at a place like the jerusalem. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary --v 6.0```
### Maria am Ostermorgen
![](https://cdn.midjourney.com/3f0c0928-b5db-4587-b0f7-6d1e7643bdcf/0_0.png)
```A selfie of a woman resembling maria in the time of jesus, wearing a robe, kneeling in front of stone grave. she has a shocked expression with her mouth open and wide eyes, evoking a sense of both fear and surprise. The scene appears surreal, with the open glowing grave behind her. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with jesus resurrection, possibly at a place like the jerusalem. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary --v 6.0```
### Der verlorene Sohn bei den Schweinen
![](https://pxscdn.com/public/m/_v2/681776049104369468/0e43b1202-69768b/KO5dfzXq3Dm1/GpYpBXZ982RAIFFHzLs9jGHsCNVwEPyqJovR0qcU.png)
```A young ancient arabic man with short hair in the time of jesus, brown eyes, and a dirty face, covered in mud from working on his pig farm, takes an amateur selfie at dusk. He is surrounded by pig stables, with a barn visible in the background and pigs seen near the front. The photo captures a raw, authentic moment, as he gazes directly into the camera with an expression of excitement or wonder. The image has a realistic style, akin to Unsplash photography, and is meant to be posted on a primitive-themed social network. The resolution of the photo is high, style of selfie with gopro --v 6.0```
### Vater und Sohn vereint
![](https://cdn.midjourney.com/4d82a1c8-9e5e-4c85-b0ea-20e20652d405/0_0.png)
A selfie of an Arab father in simple garments in the time of jesus, embracing and hugging a young man. The father's face, visible in the foreground, radiates joy and relief. Only the back of the son's head is visible, as he faces away from the camera, returning the embrace in tattered clothing. In the background, a large ancient house and other family members can be seen watching from a distance, blurred. The photo is taken with a wide-angle lens using a GoPro, enhancing the dramatic and overwhelming effect of the scene --v 6.0
### Bartimäus
![](https://cdn.midjourney.com/35f98543-6b6e-4179-9d90-fbdf8708d46b/0_1.png)
```A selfie of a man resembling blind bartimaeus in the time of jesus, black and brown and white bandages on his head over his eyes and face, wearing a robe, kneeling in front of a market place. he has a shocked expression with his mouth open and wide eyes still covered with black and brown and white bandages on his head, evoking a sense of both fear and surprise. The scene appears surreal, with many sand behind him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with jesus healing the blind, possibly at a place like the jerusalem. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudha --v 6.0```
### Daniel in der Löwengrube
![](https://pxscdn.com/public/m/_v2/681776049104369468/0e43b1202-69768b/GerS1pfoBhp3/XsRoniHyJpfnWLDa2gHl072mIjtrBGtnZX5NC6QF.png)
```A selfie of a man resembling Jesus, wearing a beige hoodie, surrounded by lions and cheetahs. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with many lions behind him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a lion's den, possibly at a place like the Grand Tabahar. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary```
### David und Goliath
![](https://cdn.midjourney.com/27459a01-52e3-4a50-b8d2-99da65a2fe78/0_2.png)
```selfie of a the boy and shepherd david holding his slingshot resembling a fight with the giant goliath in the time of old testament, wearing a glamorous sligshot focusing on his giant opponent. David has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with a desert surrounding him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of the scene of David fighting with the giant goliath with his slingshot, possibly at a place like the jerusalem. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary --v 6.0```
### Simson im Philistertempel
![](https://cdn.midjourney.com/41d706d7-15ed-40ca-b507-5a2d727e312f/0_2.png)
```A selfie of a man resembling simson in the time of old testament, wearing a glamorous beard and long hair, surrounded by thousands of ancient fighters. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with a temple surrounding him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with jesus walking by, possibly at a place like the jerusalem. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary --v 6.0 ```
### Jona und der Wal
![](https://cdn.midjourney.com/562bb203-4f5c-456c-81bc-faeaa7a92466/0_3.png)
```A selfie of a man resembling israeli jona in the time of old testament,`wearing a glamorous beard and long hair, inside the body of a whale. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with the ocean surrounding him. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene in the bible. The style of the photo blends surrealism with humor, similar to the style of Yasir Khan Chaudhary```
![](https://cdn.midjourney.com/3ad2a9a1-3bf5-4326-b161-5084b7d3634d/0_1.png)
### Jakob und Isaak
![](https://cdn.midjourney.com/f8ffa33a-2a42-48f0-9cca-5dc6df82d66b/0_0.png)
```A selfie of a young man resembling an ancient Arabic in clothes made of skins of goats and furs of the goats, looking overwhelmed and distressed as he betrays his father, who blesses him. The scene shows a dawn sky with hints of the sunrise, evoking a surreal and dramatic atmosphere. The scene is set in ancient Jerusalem, with stone buildings. in the background an old man with a gesture of blessing, rising his hands to the sky, The photo is taken with a wide-angle lens, blending surrealism with humor. The style is reminiscent of a GoPro selfie, capturing the intense moment with a sense of both fear and surprise```
### Petrus und der Hahn
![](https://cdn.midjourney.com/98327861-2694-4a43-ae92-28e8cb27e469/0_2.png)
```A selfie of a man resembling ancient young arabic man saint in traditional biblical attire, being eaten by a whale,. he has a shocked expression with his mouth pressed and wide eyes, evoking a sense of both fear and surprise. The scene appears surreal, with one rooster crowing out loud behind the man. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene with peter and the rooster, possibly at a place in jerusalem . The style of the photo blends surrealism with humor, go pro selfie, morning dawn near sunrise setting```
### Josef im Brunnen
![](https://cdn.midjourney.com/44bafbdc-a276-407d-81e9-d79f64220b8f/0_2.png)
```A selfie of an ancient israelian man with a magical dreamcoat clothing in a deep well, looking at the camera from above, captured in the style of a go pro selfie stick```
### Elia und die Raben
![](https://cdn.midjourney.com/cda9a8b5-2a64-40fd-be97-4861fb300f4c/0_3.png)
```A close-up selfie of a bearded man (Elijah) in biblical clothing, smiling gratefully. He is standing near a stream in a secluded, rocky area. Several black ravens are perched on his shoulders and arms, holding pieces of bread and meat in their beaks. The scene has a warm, golden light, symbolizing God's provision. Photorealistic style, high detail.```
### Absalom im Baum
![](https://cdn.midjourney.com/4b201ce4-57c3-4c76-b99b-dd92f9413030/0_1.png)
```A selfie of a man resembling of a young man (Absalom) with long hair knotted arount the branches of a large oak tree.. He has a shocked expression with his mouth open and wide eyes, evoking a sense of both humor and surprise. The scene appears surreal, with all of his hairs knotted around the tree. The photo is taken with a wide-angle lens, adding to the dramatic and humorous effect. The setting is reminiscent of a scene of a robin hood movie in the forest . The style of the photo blends surrealism with humor```
## Ruth und Boas im Weizenfeld
![](https://cdn.midjourney.com/9d90bd06-cb88-46c3-84e0-69d89d8cf355/0_2.png)
```A selfie of a young woman resembling Ruth, with a radiant smile and sun-kissed skin. She's standing in a golden wheat field at sunset, her arms filled with freshly gathered sheaves of wheat. Her hair is partially covered with a simple headscarf, with loose strands blowing in the wind. She has a look of joy and gratitude in her eyes. The scene appears idyllic, with wheat stalks seeming to embrace her. In the background, a distinguished older man (Boaz) can be seen watching from a distance, his expression a mix of curiosity and admiration. The photo is taken with a wide-angle lens, capturing the vastness of the field, the warmth of the setting sun, and Boaz in the distance. The setting is reminiscent of a biblical harvest scene. The style of the photo blends realism with a touch of romantic nostalgia.```
<p class="attribution">"<a target="_blank" rel="noopener noreferrer" href="https://git.rpi-virtuell.de/Comenius-Institut/KI-religionsbezogen/src/branch/main/bibel-selfies.md">Bibel-Selfies</a>" von <a target="_blank" rel="noopener noreferrer" href="https://reliverse.social/@joerglohrer">Jörg Lohrer</a> Lizenz: <a target="_blank" rel="noopener noreferrer" href="https://creativecommons.org/publicdomain/zero/1.0/deed.de">CC0 1.0 <img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" style="height: 1em; margin-right: 0.125em; display: inline;"></img><img src="https://mirrors.creativecommons.org/presskit/icons/zero.svg" style="height: 1em; margin-right: 0.125em; display: inline;"></img></a>.</p>`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
---
layout: post
title: "📢 Empowering Learners for the Age of AI der neue Review Draft des AI Literacy Frameworks für Schule ist da!"
slug: "ai-literacy-framework-review-draft"
date: 2025-05-22
description: "Das neue Review Draft des AI Literacy Frameworks für Schule von OECD und EU-Kommission beschreibt vier zentrale Domänen der KI-Kompetenz jede mit einem klaren Profil aus Wissen, Fertigkeiten und Haltungen. Sie lassen sich hervorragend mit vier Kompetenzbereichen verbinden."
image: https://blossom.primal.net/07b7736254aa6cbdf5423c542ad131a9fd4115909674a8ef5c350b315a35b18a.png
tags:
- KI
- AI-Literacy
- OECD
- EU-Kommission
- Kompetenzrahmen
- Schule
- Bildung
- relilab
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
🧠 Entwickelt von OECD & EU-Kommission jetzt zur Rückmeldung freigegeben:\
👉 <https://ailiteracyframework.org/>
Das Framework beschreibt vier zentrale **Domänen der KI-Kompetenz** jede mit einem klaren Profil aus **Wissen**, **Fertigkeiten** und **Haltungen**. Diese lassen sich hervorragend mit den **vier Kompetenzbereichen** verbinden:
🔹 **Engaging with AI** ↔ 🟢 *Verstehen*
Lernende erkennen KI in ihrem Alltag, verstehen ihre technischen Grundlagen (📘 *Knowledge*) und entwickeln die Fähigkeit, Ausgaben kritisch zu analysieren (🛠️ *Skills*), begleitet von einer neugierigen und verantwortungsbewussten Einstellung (🧭 *Attitudes*).
🔹 **Creating with AI** ↔ 🔵 *Anwenden*
Durch den kreativen Einsatz generativer KI entstehen neue Lernprodukte. Benötigt werden technisches Verständnis (📘 z.B. zu Trainingsdaten), Anwendungskompetenz (🛠️ z.B. Promptgestaltung), sowie eine innovationsorientierte Haltung (🧭 Ownership, Urheberrecht, Attribution).
🔹 **Managing AI** ↔ 🟠 *Reflektieren*
Hier geht es um bewusste Entscheidungen: Wann ist KI sinnvoll? Wie wirken sich ihre Vorschläge auf mein Denken aus? Das verlangt (📘) Orientierungswissen, (🛠️) strategisches Problemlösen und (🧭) eine ethisch begründbare Reflexion.
🔹 **Designing AI** ↔ 🟣 *Gestalten*
Lernende analysieren und entwerfen KI-Systeme: Welche Daten nutze ich? Wer profitiert? Mit welchen Folgen? Die Verbindung aus (📘) systemischem Wissen, (🛠️) Gestaltungskompetenz und (🧭) ethischer Haltung eröffnet Bildungsräume im digitalen Wandel.
📬 Rückmeldungen zum Entwurf sind willkommen eure Expertise aus der Praxis zählt!
👉 \[<https://teachai.org/ailiteracy/review](https://teachai.org/ailiteracy/review)>

View File

@ -0,0 +1,109 @@
---
layout: post
title: "Religionsbezogene Bildung mit Rollenkarten: KI-Bilder als Impulsgeber"
slug: "rollenkarten-ki-bilder-impulsgeber"
date: 2025-06-02
description: "In diesem Beitrag verweise ich auf zwei inspirierende Blogartikel von Frau Schütze zum Einsatz von KI-Bildern und Rollenkarten im Ethik-/Philosophieunterricht und zeige, wie sich diese Methoden auf die religionsbezogene Bildung übertragen lassen."
image: https://blossom.primal.net/f099c12882809b51620342738116f9e211cb76b2b8575493376932c5e53eb298.png
tags:
- relilab
- Religionsunterricht
- KI-Bilder
- Rollenkarten
- Bildung
- Reflektieren
- Gestalten
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
Im Rahmen unseres Workshops möchte ich auf zwei inspirierende Beiträge von [Frau Schütze](https://frauschuetze.de) hinweisen und dich einladen, deren Ansätze für die religionsbezogene Bildung weiterzudenken. Die beiden Ausgangspunkte sind:
1. [**Werkstattbericht: KI-Bilder im Ethik/Philosophie-Unterricht**](https://frauschuetze.de/?p=7946)\
In diesem Beitrag beschreibt Frau Schütze, wie sie mithilfe verschiedener KI-Modelle (Midjourney, DALL·E, Ideogram.ai) Bilder generiert, um Schülerinnen und Schüler zum Nachdenken über philosophische und ethische Theorien anzuregen. So wurden zum Beispiel KI-Bilder erstellt, die eine Glückstheorie illustrieren, oder symbolische Darstellungen zum ontologischen und kosmologischen Gottesbeweis (etwa das „größte denkbare Wesen“ bzw. den Laplaceschen Dämon). Dabei erfahren die Lernenden nicht nur einen visuellen Zugang zu abstrakten Gedankenexperimenten, sondern reflektieren zugleich die Grenzen und Tücken von KI-gestützter Bildbeschreibung und -verarbeitung.\
<https://frauschuetze.de/?p=7946>
2. [**Rollenkarten Moralphilosophie / angewandte Ethik**](https://frauschuetze.de/?p=7640)\
Hier stellt Frau Schütze Rollenkarten vor, die sie für den Oberstufenunterricht entwickelt hat, um Diskussionen zur angewandten Ethik zu strukturieren. Die Karten enthalten Porträts wichtiger Moralphilosoph*innen (z. B. Diogenes, Spaemann) und weitere Rollen wie Hinterfrager*innen oder Zweifler\*innen, die sich schnell in Fallanalysen, Fishbowl-Formate oder Philosophencafés einbringen lassen. Die Porträts wurden ebenfalls mit Midjourney erstellt, wobei die Prompts konkret beschreiben, wie die Figuren dargestellt werden sollen (z. B. „xxx as a character in a fantasy story, portrait“).\
<https://frauschuetze.de/?p=7640>
***
## Übertragung auf die religionsbezogene Bildung
Für religionsbezogene Bildungsszenarien eröffnen sich hier viele spannende Möglichkeiten. Im Folgenden findest du ein paar Ideenimpulse, wie du KI-Bilder und Rollenkarten gezielt einsetzen kannst, um religiöse Fragen, Traditionen und Identitäten in den Mittelpunkt zu rücken:
1. **KI-Bilder zur Visualisierung religiöser Konzepte**
* **Symbolik und Ikonographie erschließen**\
Nutze KI-Modelle, um die Symbolwelten verschiedener Religionen (z. B. Christentum, Islam, Buddhismus, Hinduismus) visuell zu erkunden. Überlege: Wie lässt sich das Kreuz in unterschiedlichen Stilrichtungen (klassisch, modern, abstrakt) darstellen? Welche Bildwelten entstehen, wenn du nach einer fusionierten Ikonographie fragst, die christliche, buddhistische und indigene Symbole kombiniert? Durch den kreativen Prozess mit KI lernst du, welche Metaphern und Traditionen hinter religiösen Zeichen stehen und wie sie bewusst oder unbewusst von Algorithmen interpretiert werden. Dabei kannst du auch die **Grenzen von KI** thematisieren: Welche Vorurteile oder Fehldeutungen schleichen sich in die Bildgenerierung ein, wenn religiöse Themen verarbeitet werden?
* **Gedankenexperimente zu Gott und Transzendenz**\
Analog zu den KI-Bildern für Gottesbeweise könntest du Aufgaben stellen wie:
1. „Erstelle ein KI-Bild, das die Idee von Theodizee visuell darstellt.“
2. „Lass die KI eine Szene generieren, in der gläubige und atheistische Perspektiven im Dialog stehen.“\
Anschließend diskutierst du mit der Gruppe, inwiefern die Bilder die jeweiligen Konzepte treffend abbilden oder eher stereotyp und eindimensional bleiben. Auf diese Weise vermittelst du nicht nur Content-Wissen, sondern entwickel auch die Medienkompetenz, indem du Fragen nach Intention, Deutungshoheit und algorithmischer Verzerrung aufwirfst.
2. **Rollenkarten für religiöse Perspektiven und Diskussionsformate**
* **Personen aus religiösen Traditionen als Rollen**\
Statt ausschließlich Moralphilosoph*innen *zu berücksichtigen, kannst du Rollenkarten mit Porträts von Religions*stifter*innen (z. B. Jesus von Nazareth, Maria, Mohammed, Buddha, Krishna), Reformern (Martin Luther, Savonarola) oder zeitgenössischen Theolog\*innen (z. B. Dorothee Sölle) gestalten. Die KI-gestützten Bilder können dabei in unterschiedlichen künstlerischen Stilrichtungen entstehen von historischer Malerei bis hin zu zeitgenössischer Street-Art-Adaption. Jede Rolle enthält einen kurzen Steckbrief mit zentralen Glaubensvorstellungen, biografischen Eckpunkten und einem charakteristischen Argument oder Zitat. So kannst du die Lernenden in Rollendebatten schicken, etwa:
* „Wie würde Luther heute auf die Klimakrise blicken?“
* „Welche theologische Argumentation könnte Dorothee Sölle zum Thema Gewaltlosigkeit einbringen?“
* **Szenarien für kontroverse Debatten**
1. **Religiöse Vielfalt versus Säkularismus**\
Verteilt Karten, in denen Rollen wie „konservativer Christ“, „liberaler Muslim“, „selbstbewusste/r Konfessionslose/r“, „Theologieprofessor/in“ oder „politische/r Aktivist/in“ eingenommen werden. Die KI-Porträts unterstützen die Visualisierung, verleihen den Rollen ein Gesicht und erleichtern das Einfühlen in andere Perspektiven.
3. **Interaktive Formate im Religionsunterricht**
* **Glaubenscafé (analog zum Philosophencafé)**\
In Kleingruppen diskutiert ihr verschiedene Glaubenspositionen. KI-Bilder dienen als Ausgangspunkt: Ein Bild, das etwa die Drei-Tage-Phase von Tod und Auferstehung Jesu künstlerisch darstellt, oder ein generiertes Motiv zu einem hinduistischen Fest (z. B. Holi), wird an die Wand projiziert. Anschließend reflektiert ihr gemeinsam: Welche Emotionen, Symbole, Bedeutungen nehmt ihr wahr? Danach schlüpft ihr in Rollenkarten (z. B. theologische/r Fachreferent/in, Religionskritiker/in, Gemeindemitglied) und erarbeitet Positionen, die ihr in einem moderierten Glaubenscafé präsentiert.
* **Fishbowl-Diskussionen**\
Nutzt Rollenkarten zu religionsspezifischen Rollen (z. B. Rabbiner/in, Pfarrerin, Atheist/in, Kleriker/in einer traditionellen Religion, spirituelle/r Influencer/in) für eine Fishbowl-Diskussion zum Thema „Sinnsuche in der Postmoderne“. Die KI-Bilder dienen zu Beginn als visuelle Reize: „Welche Elemente im Bild sprechen für Spiritualität, welche eher für Skepsis?“ So wird die Distanz zwischen digitaler Darstellung und gelebter religiöser Erfahrung erlebbar.
4. **Methodische Hinweise und Reflexion**
* **Prompt-Kompetenz schulen**\
Wie schon im Ethikunterricht festgestellt, erfordert das präzise Beschreiben von Bildwünschen viel Übung. Die Lernenden lernen, welche Schlüsselbegriffe notwendig sind und wie kulturelle Vorannahmen in Prompts stecken bleiben. Im religionspädagogischen Kontext könnt ihr dies gezielt thematisieren: Wie formuliert man z. B. den Prompt „Stelle eine friedvolle interreligiöse Konferenz zwischen Christentum, Islam und Judentum dar“ so, dass keine Stereotype reproduziert werden?
* **Ethik des Bilderzeugens**\
Diskutiert gemeinsam, inwiefern KI-Bilder beim Umgang mit heiklen religiösen Themen (z. B. Darstellungen des Propheten Mohammed im Islam) kulturelle oder religiöse Grenzen überschreiten können. Legt zusammen ethische Leitlinien fest, bevor ihr KI zur Bildproduktion nutzt: Welche religiösen Bilder sind "sakrosankt", welche dürfen manipuliert werden und was bedeutet das für Religionsfreiheit und Respekt?
* **Reflexion über Urheberrecht und OER**\
Wie Frau Schütze in den Rollenkarten-Anleitungen betont, sind ihre Materialien unter CC-BY-Lizenz verfügbar. Erörtert, was es bedeutet, religiöse Bild- und Textmaterialien unter Open-Content-Lizenzen zu verwenden und welche Implikationen das für Schule, Gemeinde und Zivilgesellschaft hat. Das sensibilisiert für Fragen von Teilhabe und Gemeineigentum im digitalen Raum.
***
## Einladung zum Weiterdenken
Ich lade dich herzlich ein, die vorgestellten Methoden und Materialien in eigenen Projekten auszuprobieren und weiterzuentwickeln. Diskutiere in Kleingruppen oder in einer offenen Runde:
* **Welche religiösen Themen lassen sich besonders gut mit KI-Bildern visualisieren?**\
Beispiele: Zehn Gebote, Fastenrituale, Schöpfungsmythen, Visionen von Heiligen oder Heiligenschauen. Welche Prompts würdest du verwenden, um diese Szenen zu erzeugen?
* **Wie könnten Rollenkarten zu spezifischen religiösen Traditionen aussehen?**\
Entwickle gemeinsam mit anderen kurze Steckbriefe und Bildprompts für Rollen wie „Sufi-Derwisch“, „Gottesleugner/in der Aufklärung“, „Ökumenische/r Pastor/in“, „Religionspädagog\*in“, „Katholischer Laienbruder“ oder „Jüdische Rabbinerin“. Achte dabei auf unterschiedliche religiöse Sichtweisen und Geschlechterperspektiven.
* **Welche Herausforderungen ergeben sich beim Einsatz von KI-Bildern im Religionsunterricht?**\
Erörtert mögliche Missverständnisse, kulturelle Fehlinterpretationen oder ethische Konflikte (z. B. pietätslose Darstellungen von Figuren, die in bestimmten Glaubensgemeinschaften als heilig gelten). Entwickelt gemeinsam Kriterien oder einen Leitfaden, um solche Risiken zu minimieren.
* **Wie lassen sich interreligiöse Dialoge durch digitale Methoden fördern?**\
Gebt Impulse, wie man mit KI-Bildern und Rollenkarten einen „virtuellen Tempelraum“ gestalten kann, in dem Symbole unterschiedlicher Religionen nebeneinanderstehen und zu Dialog anregen. Welche Fragen stellen sich dabei hinsichtlich Toleranz, Respekt und theologischer Pluralität?
***
### Zusammenfassung und Ausblick
Die beiden Beiträge von Frau Schütze bieten hervorragende Grundlagen, um im religionsbezogenen Unterricht visuelle und interaktive Zugänge zu schaffen. Durch KI-Bilder gewinnen die Teilnehmenden neue Zugänge zu Symbolik, Theodizee und Gottesvorstellungen, während Rollenkarten Dialogkompetenz und Empathie für unterschiedliche religiöse Perspektiven stärken. Diese Ideenimpulse sollen dich ermutigen, die Methoden selbst zu erproben und weiterzuentwickeln. Im digitalen Zeitalter können wir so das gemeinsame Lernen und interkulturelle Verständnis gerade im sensiblen Feld der Religionsbildung bereichern.
Ich freue mich auf deine kreativen Umsetzungen und den Austausch über Erfahrungen, Herausforderungen und Fortschritte in diesem spannenden Feld!Antworte gerne auf diesen Beitrag und kommentiere mit deinen Gedanken und Assoziationen oder poste selbst etwas als Impuls, das andere anregt und weiterverwendet werden darf mit dem Hashtag #relilab.\
\
Template: Avery Re <https://opengameart.org/content/trading-card-template> unter der Lizenz CC-by <https://creativecommons.org/licenses/by/4.0/deed.de> / Einige Texte und Bezeichnungen von Frank Schlegel <https://digitaldurstig.de/rollenkarten/> unter der Lizenz CC-by-SA 4.0, <https://creativecommons.org/licenses/by-sa/4.0/deed.de>\
Lizenz: cc-by-sa 4.0 / [frauschuetze.de](http://frauschuetze.de) / Juli 2023

View File

@ -0,0 +1,139 @@
---
layout: post
title: "Nostr und Open Educational Practices (OEP)"
slug: "nostr-und-open-educational-practices"
date: 2025-06-04
description: "Der Beitrag zeigt, wie das dezentrale Protokoll Nostr mit seiner clientseitigen Architektur und kryptografischen Identitätsverwaltung eine zensurresistente, transparente und partizipative Alternative zu zentralisierten Bildungsplattformen bietet. Durch die Integration von Nostr in Open Educational Practices (OEP) können Lernende und Lehrende volle Kontrolle über Inhalte und Identitäten übernehmen und gemeinsam an frei zugänglichen Bildungsressourcen arbeiten."
image: https://blossom.primal.net/a93ea30b4651700874b7817e5aabf1755e1c1b07b312e4f1b8a18d70f3d17335.jpg
tags:
- Nostr
- OEP
- Open Educational Practices
- OER
- Dezentral
- Bildung
- Kryptografie
- Zensurresistenz
- Partizipation
- Commons
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
## 1. Einleitung
Dieser Beitrag bietet eine detaillierte Einführung in Nostr - ein dezentrales Protokoll für soziale Medien, das sich grundlegend von traditionellen und föderierten Plattformen unterscheidet - und erweitert seine Diskussion, um das Potenzial für die Integration von Nostr in die Entwicklung von Bildungsplattformen im Sinne von Open Educational Practices (OEP) zu untersuchen. Nostr ist als leichtgewichtiger, zensurresistenter Mechanismus konzipiert, der in erster Linie auf einer kryptografischen Schlüsselverwaltung und einer Client-zentrierten Architektur basiert, die zusammen eine dezentrale Identitätskontrolle und Autonomie über digitale Inhalte ermöglichen. Im Gegensatz zu konventionellen zentralisierten Modellen, die sich auf servergestützte Kontrolle oder föderierte Systeme wie Mastodon, die Moderation und Identitätsspeicherung zusammenfassen, stützt sich Nostr auf ein Netzwerk von "Relais", die ohne direkte Interkommunikation arbeiten, wobei alle Funktionen der Datenaggregation, des Austauschs von Inhalten zwischen den Räumen und der Benutzerinteraktion an die Client-Anwendungen delegiert werden. Ziel dieses Beitrags ist es daher, einen umfassenden technischen und konzeptionellen Überblick über die Architektur des Protokolls und eine kritische Diskussion seiner Anwendbarkeit im Kontext dezentraler Bildungsplattformen zu geben, die eine offene, nutzergesteuerte Erstellung, Verwaltung und Verteilung von Inhalten fördern und damit den Prinzipien des OEP entsprechen.
## 2. Hintergrund und Motivation
Die Entwicklung der sozialen Medien in den letzten zwei Jahrzehnten war geprägt von zunehmend zentralisierten Plattformen, die sowohl die digitale Identität der Nutzer als auch die von ihnen erstellten Inhalte kontrollieren, was oft zu Zensur, algorithmischer Manipulation und Einzelausfällen führt. Im Gegensatz dazu haben sich dezentralisierte Social-Media-Protokolle wie Nostr als innovative Antwort auf diese Zentralisierungsfallen entwickelt, die offene, kryptografisch sichere und verteilte Architekturen verwenden. Nostr wurde mit dem ausdrücklichen Schwerpunkt konzipiert, den Nutzern die volle Kontrolle über ihre Kontoauthentifizierung und digitale Identität zu geben, was durch die Verwaltung von kryptografischen Schlüsselpaaren analog zu denen in Bitcoin-Wallets erreicht wird. Dieses Modell verbessert nicht nur die zensurresistenten Eigenschaften der Plattform, sondern verändert auch grundlegend die Machtdynamik in traditionellen sozialen Netzwerken, da es die Abhängigkeit von zentralen Behörden minimiert und die Möglichkeiten der Ausbeutung und Überwachung verringert. Im Kontext von Bildungsökosystemen bieten diese Merkmale eine solide Grundlage für die Entwicklung von Plattformen, die transparent, partizipatorisch und widerstandsfähig sind, da sie die gemeinsame Nutzung und kontinuierliche Weiterentwicklung von Bildungsressourcen ohne zentralisierte Kontrolle oder Unternehmenssteuerung unterstützen.
## 3. Technische Architektur von Nostr
Die Architektur von Nostr basiert in erster Linie auf zwei integralen Komponenten: Clients und Relays. Clients sind die benutzerseitigen Anwendungen, die die Erstellung, Veröffentlichung und Aggregation von Inhalten ermöglichen, während Relays Server sind, die von den Benutzern generierte Nachrichten, so genannte "Events", empfangen, speichern und weiterleiten. Eine der wichtigsten Neuerungen des Nostr-Protokolls ist die radikale Entkopplung der traditionellen Client-Server-Beziehung: Die Relais müssen sich nicht synchronisieren oder miteinander kommunizieren, was bedeutet, dass die Replikation von Inhalten und die Aggregation von Daten zwischen den Räumen vollständig auf der Client-Seite stattfindet. Dies minimiert die zentrale Kontrolle und schafft ein verteiltes, relaisbasiertes System, das von Natur aus resistent gegen Zensur ist, da die Dichte der verfügbaren Relays und die Möglichkeit der Nutzer, sich mit mehreren Relays zu verbinden, sicherstellen, dass die Inhalte auch dann zugänglich bleiben, wenn bestimmte Relays ausfallen oder kompromittiert sind.
In Nostr sind Ereignisse als JSON-Objekte strukturiert, die einem definierten Schema mit verschiedenen "Arten" (NIPs) entsprechen, um spezifische Funktionalitäten und andere zukünftige Erweiterungen zu unterstützen. Jedes Ereignis wird mit dem privaten kryptografischen Schlüssel des Nutzers digital signiert, was die Authentizität und Integrität der Daten garantiert und gleichzeitig eine sichere, pseudonyme Interaktion ermöglicht. Da innerhalb des Protokolls keine zentrale Behörde oder ein einziger Identitätsspeicher existiert, behalten die Nutzer die vollständige Kontrolle über ihre digitalen Identitäten, und wenn der private Schlüssel verloren geht, ist das entsprechende Konto unwiederbringlich. Dieser Ansatz verankert den Grundsatz, dass Identität sowohl dezentralisiert als auch unveränderlich ist, und stärkt die individuelle Kontrolle über persönliche Daten und Inhalte.
Da die Relays unabhängig voneinander von verschiedenen Betreibern verwaltet und nicht von zentralen Diensten koordiniert werden, können die Benutzer mehrere Relays auswählen, um die Bereitstellung und Replikation von Inhalten zu optimieren. Diese architektonische Entscheidung verbessert die Redundanz und Ausfallsicherheit erheblich, bringt aber gleichzeitig technische Herausforderungen in Bezug auf Standardisierung, Moderation und effiziente Kommunikation zwischen den Clients mit sich. Nostr integriert auch Bitcoin-basierte Mikrozahlungen (bekannt als "Zaps"), um finanzielle Anreize für den Betrieb von Relay-Servern zu schaffen und so die Herausforderungen der wirtschaftlichen Nachhaltigkeit in einer dezentralen Infrastruktur zu bewältigen. Die Design-Entscheidungen in Nostr fördern Offenheit und Innovation, indem sie es jeder Person oder Organisation mit ausreichenden technischen Fähigkeiten erlauben, ein Relay zu hosten oder einen Client zu entwickeln, was zu einem lebendigen Ökosystem von verschiedenen Anwendungen und Diensten führt.
## 4. Identitätsmanagement und Moderation
Im Mittelpunkt des dezentralen Paradigmas von Nostr steht der Ansatz der Identitätsverwaltung, der sich von traditionellen Systemen unterscheidet, bei denen Identitäten als Kontodaten von zentralen Servern verwaltet werden. Bei Nostr verfügt jeder Benutzer über ein einzigartiges kryptografisches Schlüsselpaar, wobei der öffentliche Schlüssel als dauerhafte digitale Kennung und der private Schlüssel als Mittel zum Signieren aller Aktionen und Ereignisse dient. Dieses kryptonative Modell gewährleistet nicht nur robuste Sicherheit und Authentizität, sondern überlässt den Nutzern auch die alleinige Verantwortung für die Verwaltung ihrer Schlüssel - was sowohl die Freiheit als auch das Risiko eines vollständig dezentralen Identitätssystems unterstreicht.Dieses empfindliche Gleichgewicht unterstreicht die Autonomie der Nutzer und steht im Einklang mit dem breiteren Ethos dezentraler sozialer Medien, in denen keine einzelne Instanz die Identität einer Person ohne Zugang zu ihrem privaten Schlüssel außer Kraft setzen oder manipulieren kann.
Die Moderation in Nostr ist aufgrund des dezentralen Kuratierungsmodells noch komplexer. Die Inhalte werden nicht von einer zentralen Behörde moderiert, sondern durch eine Kombination aus relaisspezifischen Richtlinien und clientseitigen Kontrollen, wie z. B. benutzerdefinierte Listen für persönliche Kuratoren oder Sperr-/Stummschaltungsfunktionen. Dieser Ansatz fördert zwar die freie Meinungsäußerung und unterstützt eine Vielzahl von Standpunkten, kann aber auch zu uneinheitlichen Moderationsrichtlinien und fragmentierten Nutzererfahrungen auf verschiedenen Relays und Clients führen, was für die Nutzer eine erhebliche technische und verhaltensbezogene Belastung darstellt, wenn sie ihre digitalen Umgebungen kuratieren wollen.
## 5. Skalierbarkeit und Widerstandsfähigkeit gegen Zensur
Ein entscheidendes Merkmal des Nostr-Protokolls ist sein hohes Maß an Skalierbarkeit und Zensurresistenz, das durch seine verteilte, relaisbasierte Architektur erreicht wird. Durch die Dezentralisierung der Speicherung und Verbreitung von Inhalten auf zahlreiche unabhängige Relays entschärft Nostr die üblichen Schwachstellen zentralisierter sozialer Plattformen, wie z. B. Single Points of Failure oder das Potenzial für staatliche oder unternehmensgesteuerte Zensur. Mehrere Relays können gleichzeitig arbeiten, wodurch sichergestellt wird, dass die Stabilität des Netzwerks auch dann erhalten bleibt, wenn einige von ihnen vom Netz genommen werden oder behördlichem Druck ausgesetzt sind, und dass Inhalte für Kunden, die mit alternativen Relays verbunden sind, zugänglich bleiben.
Die Skalierbarkeit von Nostr wird dadurch verbessert, dass sich die Nutzenden gleichzeitig mit mehreren Relays verbinden können, was die Replikation von Inhalten über ein globales Netz von Servern ermöglicht, das sich über zahlreiche geografische Regionen und autonome Systeme erstreckt. Dieser Replikationsmechanismus bringt jedoch Herausforderungen mit sich, wie z. B. eine erhöhte Redundanz bei der Speicher- und Bandbreitennutzung, die eine sorgfältige Optimierung und die Entwicklung innovativer clientseitiger Strategien erfordern, um den Datenabruf zu verwalten und den Overhead zu reduzieren, ohne die Verfügbarkeit zu beeinträchtigen. Darüber hinaus trägt die Dezentralisierung der Zuständigkeiten für Kuratierung und Moderation zu einer dynamischen Umgebung bei, in der Inhalte sowohl gegen erzwungene Löschungen resistent als auch an die Bedürfnisse der Nutzer anpassbar sind, wenn auch auf Kosten von Einheitlichkeit und vorhersehbarer Nutzererfahrung.
## 6. Vergleich mit föderierten und zentralisierten Modellen
Das Aufkommen dezentraler Protokolle wie Nostr steht in scharfem Kontrast zu föderierten Netzwerken wie Mastodon, die das ActivityPub-Protokoll verwenden. Während föderierte Systeme die Kontrolle über unabhängige Instanzen verteilen, verlassen sie sich immer noch auf ein gewisses Maß an Zentralisierung auf der Ebene jeder einzelnen Instanz, von denen jede ihre eigenen Richtlinien bezüglich Moderation, Identitätsüberprüfung und Inhaltszensur durchsetzt. In diesen Systemen ist die Verwaltung zwar auf mehrere Knotenpunkte verteilt, aber es besteht eine Abhängigkeit von Serveradministratoren, die erhebliche Macht über den Inhalt und die Identität der Benutzer haben; dies steht im Gegensatz zu Nostrs vollständig kundenorientiertem Kurations- und Identitätsverwaltungsmodell, das darauf abzielt, jeden einzelnen Kontrollpunkt zu eliminieren.
In zentralisierten Architekturen werden Benutzerdaten und -identitäten auf proprietäre und undurchsichtige Weise verwaltet, wodurch Inhalte den Launen von Unternehmensrichtlinien und externem Druck ausgesetzt werden, was zu Überwachung, gezielter Zensur oder einseitiger algorithmischer Manipulation von Inhalten führen kann. Das Design von Nostr - das auf kryptografischer Authentifizierung, clientseitiger Datenaggregation und dem erlaubnisfreien Betrieb von Relay-Servern beruht - stellt eine radikale Abkehr dar, die das Gleichgewicht der Macht fest in die Hände der einzelnen Nutzer und nicht in die Hände zentraler Behörden legt.
## 7. Implikationen für Bildungsplattformen
Das Potenzial für die Integration des dezentralen Protokolls von Nostr in Bildungsplattformen bietet eine transformative Gelegenheit zur Unterstützung von Open Educational Practices (OEP). OEP legt den Schwerpunkt auf Inklusion, Autonomie der Lernenden, kollaborative Wissenserstellung und offenen Zugang zu Bildungsressourcen. Herkömmliche Bildungsplattformen sind oft durch zentralisierte Architekturen eingeschränkt, die den Zugang behindern und Innovationen unterdrücken können, da sie auf einzelne administrative Kontrollen und proprietäre Content-Management-Systeme angewiesen sind.
Durch die Nutzung der dezentralen Infrastruktur von Nostr können Bildungsplattformen aufgebaut werden, die es Lehrenden und Lernenden ermöglichen, die volle Kontrolle über ihre digitalen Identitäten und Bildungsinhalte zu behalten, ohne das Risiko von Zensur oder externer Einmischung. In solchen Systemen verwaltet jeder Teilnehmer seine eigene Identität mithilfe von kryptografischen Schlüsseln, wodurch sichergestellt wird, dass Bildungsleistungen, Beiträge und Zeugnisse auf überprüfbare und unveränderliche Weise aufgezeichnet werden. Dies fördert ein Umfeld, in dem die Schaffung und Verbreitung von Wissen wirklich von den Teilnehmern selbst bestimmt wird, frei von den Zwängen, die von zentralen Torwächtern auferlegt werden.
Darüber hinaus bietet die relaisbasierte Architektur von Nostr eine beispiellose Skalierbarkeit und Ausfallsicherheit - Eigenschaften, die für ein Bildungsökosystem, das eine Vielzahl von Nutzern mit unterschiedlichem geografischen und sozioökonomischen Hintergrund unterstützen muss, unerlässlich sind. Bildungsinhalte - von digitalen Lehrbüchern und Multimedia-Vorlesungen bis hin zu interaktiven Diskursen und gemeinschaftlichen Projekten - können gespeichert und über mehrere Relays repliziert werden, um sicherzustellen, dass der Zugang auch bei lokalen Netzwerkausfällen oder Zensurversuchen erhalten bleibt.
Das offene Protokoll von Nostr ist außerdem eine ideale Grundlage für die Entwicklung interoperabler Bildungsplattformen, auf denen Drittentwickler und Bildungseinrichtungen maßgeschneiderte Clients und Anwendungen für pädagogische Zwecke erstellen können. Dieser Ansatz fördert ein vielfältiges Ökosystem von Bildungswerkzeugen und -diensten, die nahtlos zusammenarbeiten, ohne auf eine einzige proprietäre Plattform beschränkt zu sein, und steht damit im Einklang mit dem Ethos des OEP. Pädagogen können digitale Lernumgebungen entwerfen, die die kollaborative Erstellung von Inhalten, Peer-Reviews und Community-gesteuerte Kuration unterstützen und gleichzeitig von der Sicherheit und Transparenz profitieren, die das kryptografische Grundgerüst von Nostr bietet.
Darüber hinaus gewährleistet die inhärente Zensurresistenz von Nostr, dass Bildungsinhalte zugänglich und frei von staatlichen oder unternehmerischen Eingriffen bleiben - ein besonders wichtiger Aspekt für Lernende in Regionen, in denen der Zugang zu Informationen eingeschränkt oder überwacht wird. Diese Widerstandsfähigkeit untermauert nicht nur die demokratische Verbreitung von Wissen, sondern unterstützt auch den Grundsatz eines digitalen Gemeinguts, bei dem Bildungsressourcen kollektiv verwaltet und gemeinsam genutzt werden. In diesem Zusammenhang kann Nostr genutzt werden, um Bildungsplattformen zu entwickeln, die nicht nur robust und integrativ sind, sondern auch auf die Bedürfnisse marginalisierter oder unterrepräsentierter Gemeinschaften abgestimmt sind.
Zusätzlich zu diesen strukturellen Vorteilen kann die finanzielle Nachhaltigkeit dezentraler Bildungsplattformen durch die Integration von auf Kryptowährungen basierenden Anreizmechanismen gestärkt werden - wie die Unterstützung von Nostr für Bitcoin Lightning "Zaps" zeigt. Diese Micropayment-Systeme eröffnen Möglichkeiten für neuartige Finanzierungsmodelle, die Pädagogen und Inhaltsersteller direkt belohnen und so die Abhängigkeit von werbebasierten Einnahmen oder zentralisierten Finanzierungsströmen verringern, die häufig die Integrität der Bildung gefährden. Solche wirtschaftlichen Anreize können auch den Betrieb und die Wartung von Relay-Servern unterstützen und so sicherstellen, dass die dezentrale Infrastruktur belastbar und kontinuierlich für Bildungszwecke verfügbar bleibt.
## 8. Möglichkeiten für kollaboratives und kultursensibles Lernen
Über die technische Stabilität und Skalierbarkeit hinaus bietet die dezentrale Natur von Nostr bedeutende Möglichkeiten zur Förderung von kollaborativem Lernen und kultursensibler Pädagogik. Dezentralisierte Bildungsplattformen, die auf Nostr aufbauen, können lokale Gemeinschaften in die Lage versetzen, Inhalte zu erstellen und zu kuratieren, die kontextuell relevant und kulturell angemessen sind - ein entscheidender Faktor bei der Förderung von Bildungsgerechtigkeit und Inklusivität. In Umgebungen, in denen Mainstream-Bildungstechnologien als von oben nach unten betrachtet werden und nicht mit lokalen Werten oder pädagogischen Ansätzen übereinstimmen, ermöglicht eine dezentralisierte Infrastruktur die Entwicklung maßgeschneiderter digitaler Lernumgebungen, die lokale Kulturen, Sprachen und Traditionen widerspiegeln.
Dieser gemeinschaftsorientierte Bildungsansatz fördert das Gefühl der Eigenverantwortung bei Lernenden und Lehrenden gleichermaßen und ermutigt zur gemeinschaftlichen Schaffung von Wissen, das unterschiedliche Erkenntnistheorien respektiert. So können Pädagogen beispielsweise dezentrale "Lernräume" einrichten, in denen Open-Source-Lernmaterialien, Werkzeuge für die digitale Kompetenz und innovative Lehrmethoden gemeinsam genutzt und durch partizipatives Engagement kontinuierlich weiterentwickelt werden. In einem solchen Modell verschwimmen die Grenzen zwischen Lernenden und Lehrenden, was einen fließenden Ideenaustausch und die gemeinsame Schaffung von Wissen ermöglicht, die im Mittelpunkt offener Bildungspraktiken stehen.
Darüber hinaus ermöglicht die Flexibilität der Client-zentrierten Architektur von Nostr die Entwicklung von spezialisierten Bildungsclients, die auf die jeweiligen Lernstile und Disziplinen zugeschnitten sind. Diese Clients könnten Funktionen wie erweiterte Such- und Filterfunktionen für Bildungsinhalte, integrierte Anmerkungswerkzeuge für die gemeinsame Bearbeitung von Dokumenten und interoperable Identitätsmanagementsysteme enthalten, die es den Lernenden ermöglichen, ihre Anmeldedaten nahtlos in verschiedene digitale Lernumgebungen zu übertragen. Solche Innovationen würden nicht nur die Rolle der Nutzerautonomie beim digitalen Lernen stärken, sondern auch die Entwicklung von Bildungsnetzwerken fördern, die belastbar und anpassungsfähig sind und auf die sich verändernden pädagogischen Herausforderungen reagieren können.
## 9. Herausforderungen und Abhilfestrategien
Trotz der vielversprechenden Möglichkeiten ist die Integration von Nostr in Bildungsplattformen nicht unproblematisch. Gerade die Funktionen, die den Nutzern mehr Möglichkeiten bieten - wie selbstverwaltete kryptografische Schlüssel und dezentralisierte Inhaltsmoderation - führen auch Komplexitäten ein, die die Benutzerfreundlichkeit und Zugänglichkeit behindern können, insbesondere für Personen ohne fortgeschrittene technische Kenntnisse. Die Schlüsselverwaltung stellt beispielsweise eine kritische Schwachstelle dar; der Verlust eines privaten Schlüssels führt unwiderruflich zum Verlust der Identität und des Zugriffs auf die eigenen digitalen Beiträge - ein Risiko, das durch solide Bildungsinitiativen und Mechanismen zur Unterstützung der Nutzer angegangen werden muss.
Die technische Kompetenz von Lehrenden und Lernenden ist in diesem Zusammenhang von größter Bedeutung und erfordert die Entwicklung umfassender Einführungsverfahren, benutzerfreundlicher Schnittstellen und Hilfsmittel, die kryptografische Konzepte entmystifizieren und die Interaktion mit dezentralen Netzwerken vereinfachen. Zu diesem Zweck könnte der Einsatz intuitiver Bildungsmodule, interaktiver Tutorials und von der Community geleiteter Schulungen technische Barrieren abbauen und gleichzeitig die Entwicklung digitaler Kompetenzen fördern, die für die Navigation in dezentralen Netzwerken unerlässlich sind. Darüber hinaus muss die Herausforderung einer inkonsistenten Moderation und fragmentierten Nutzererfahrung aufgrund der heterogenen Richtlinien unabhängiger Relais und Clients durch die Einrichtung von Community-Governance-Rahmenwerken und interoperablen Standards angegangen werden, die eine Grundlinie der Qualität und Sicherheit auf der gesamten Plattform gewährleisten.
Strategien zur Bewältigung des mit der Skalierbarkeit verbundenen Overheads - wie etwa die redundante Replikation von Beiträgen über eine übermäßige Anzahl von Relays - sollten sich auf die Implementierung clientseitiger Optimierungstechniken konzentrieren, die die Ineffizienz von Bandbreite und Speicherplatz verringern, ohne die Belastbarkeit und Zugänglichkeit des Netzwerks zu beeinträchtigen. Kontinuierliche Forschung und iterative Verfeinerung werden notwendig sein, um ein Gleichgewicht zwischen den Vorteilen der Dezentralisierung und der ihr innewohnenden betrieblichen Komplexität zu finden und sicherzustellen, dass auf Nostr basierende Bildungsplattformen sowohl robust als auch benutzerfreundlich bleiben können.
## 10. Zukünftige Richtungen und Forschungsmöglichkeiten
Die Einbindung von Nostr in Bildungsplattformen stellt ein fruchtbares Gebiet für Forschung und Entwicklung dar, das zahlreiche Möglichkeiten zur Erkundung bietet. Zukünftige Arbeiten könnten das Design interoperabler Bildungsrahmenwerke erforschen, die die dezentrale Architektur von Nostr mit aufkommenden Technologien wie künstlicher Intelligenz, Blockchain-basiertem Credentialing und fortschrittlichen kryptographischen Techniken integrieren, um die Sicherheit, Zugänglichkeit und Authentizität in digitalen Lernumgebungen weiter zu verbessern. Die Erforschung gemeinschaftsbasierter Governance-Modelle ist ebenfalls gerechtfertigt, da dezentrale Plattformen Mechanismen für die kollektive Entscheidungsfindung und Konfliktlösung erfordern, die mit den Grundsätzen einer offenen, partizipativen Bildung vereinbar sind.
Ein möglicher Weg ist die Entwicklung modularer, quelloffener Bildungs-Clients, die sich nahtlos mit verschiedenen Relais verbinden lassen und den Lehrkräften leistungsstarke Tools zur Kuratierung und Moderation von Inhalten bieten. Diese Clients könnten anpassbare Dashboards, Module für die Zusammenarbeit in Echtzeit und integrierte Unterstützung für Mikrozahlungen oder Anreizstrukturen bieten, um sicherzustellen, dass Bildungsinhalte sowohl dynamisch als auch finanziell nachhaltig bleiben. Darüber hinaus könnte die Einrichtung digitaler Identitätsrahmen, die auf den kryptografischen Prinzipien von Nostr aufbauen, zu innovativen Modellen für die Überprüfung akademischer Zeugnisse, die Verfolgung des Lernfortschritts und die Erleichterung der plattformübergreifenden Anerkennung von Bildungsleistungen führen, und das alles ohne die Aufsicht zentralisierter Registrierungsbehörden.
Die Schnittstelle zwischen digitaler Kompetenz und ethischer digitaler Bürgerschaft ist ein weiteres vielversprechendes Forschungsgebiet. In dem Maße, in dem Pädagogen dezentrale Bildungstechnologien in die Lehrpläne integrieren, wird es zwingend notwendig, Lehrmethoden zu entwickeln, die nicht nur technische Kenntnisse, sondern auch die kritischen und moralischen Fähigkeiten vermitteln, die für die Arbeit in einem dezentralen digitalen Ökosystem erforderlich sind. Solche pädagogischen Strategien sollten Schulungen zum Schlüsselmanagement, zur Verifizierung digitaler Identitäten und zum Verständnis der Kompromisse umfassen, die mit dezentralisierten und zentralisierten Technologiemodellen einhergehen, um den Schülern eine ausgewogene Perspektive auf digitale Rechte, Verantwortlichkeiten und Möglichkeiten zu vermitteln.
Die Zusammenarbeit zwischen akademischen Institutionen, Open-Source-Gemeinschaften und Innovatoren dezentraler Technologien wird entscheidend sein, um diese Forschungsagenden voranzutreiben und die Entwicklung von Bildungslabors und Pilotprojekten zu fördern, die die praktischen Auswirkungen und Vorteile dezentraler Bildungsplattformen auf der Grundlage von Nostr erforschen. Solche Initiativen könnten als lebende Laboratorien zum Testen neuer Werkzeuge, Methoden und Governance-Strategien dienen, mit dem Ziel, das Gleichgewicht zwischen dem Versprechen der Dezentralisierung und ihren praktischen Herausforderungen schrittweise zu verfeinern.
## 11. Schlußfolgerung
Zusammenfassend lässt sich sagen, dass Nostr eine bahnbrechende Entwicklung in der Architektur sozialer Medien darstellt, die die Beziehung zwischen Nutzern, Inhalten und Netzwerkinfrastruktur durch ein dezentralisiertes, kryptographisch gesichertes und klientenzentriertes Modell neu definiert. Das relaisbasierte Design, die dezentrale Identitätsverwaltung und die offene Architektur bieten erhebliche Vorteile gegenüber herkömmlichen zentralisierten und föderierten Systemen in Bezug auf Zensurresistenz, Skalierbarkeit und Benutzerautonomie. Diese technischen Innovationen haben weitreichende Auswirkungen auf die Entwicklung von Bildungsplattformen, die den Grundsätzen der Open Educational Practices entsprechen. Durch die Unterstützung von nutzergesteuerten Identitäten, stabiler Replikation von Inhalten und gemeinschaftsgesteuerter Verwaltung bietet Nostr eine Infrastruktur für dezentrale Bildungsökosysteme, die Inklusion, Zusammenarbeit und kontinuierliche Innovation fördern.
Der Einsatz von Nostr in Bildungskontexten verspricht, Lernende und Lehrende in die Lage zu versetzen, Bildungsinhalte frei von zentralisierten Einschränkungen und externer Zensur zu erstellen, zu teilen und gemeinschaftlich weiterzuentwickeln. Gleichzeitig erfordern die Herausforderungen, die mit der technischen Komplexität, der Schlüsselverwaltung und der inkonsistenten Moderation verbunden sind, kontinuierliche Forschung und die Entwicklung unterstützender Werkzeuge. Wenn diese Probleme durch gezielte Maßnahmen wie benutzerfreundliche Schnittstellen, umfassende Programme für digitale Kompetenz und gemeinschaftsbasierte Governance-Modelle angegangen werden, können die Beteiligten das volle Potenzial von Nostr nutzen, um dezentrale, offene und partizipative Bildungsplattformen zu schaffen, die den Geist von Open Educational Practices wirklich verkörpern.
Letztendlich stellt die Integration von Nostr in dezentralisierte Bildungsplattformen nicht nur eine technische Entwicklung dar, sondern auch einen gesellschaftspolitischen Wandel hin zu größerer Dezentralisierung, Ermächtigung der Nutzer und digitaler Souveränität. Diese Ausrichtung an den Werten des OEP definiert nicht nur die Verbreitung und den Konsum von Bildungsinhalten neu, sondern ist auch Katalysator für breitere Diskussionen über digitale Rechte, Inklusion und ethische Teilhabe an der digitalen Allmende. Das Potenzial solcher Plattformen, kontextrelevante, zensurresistente und kollaborativ kuratierte Bildungsressourcen bereitzustellen, ist ein entscheidender Schritt, um die Zukunft der digitalen Bildung neu zu gestalten.
## Fazit
Zusammenfassend lässt sich sagen, dass die innovative Architektur von Nostr, die auf einer kryptografischen Schlüsselverwaltung und dezentralen Relay-Netzwerken basiert, den Grundstein für eine neue Generation von Bildungsplattformen legt, die die Einschränkungen zentralisierter Systeme überwinden können. Ihre Fähigkeit, belastbare, sichere und nutzerzentrierte digitale Umgebungen bereitzustellen, steht in perfektem Einklang mit den Grundsätzen offener Bildungspraktiken und bietet Lehrenden und Lernenden eine noch nie dagewesene Kontrolle über ihre digitalen Interaktionen und Inhalte. Kontinuierliche Forschung, interdisziplinäre Zusammenarbeit und gezielte Bildungsinitiativen werden unerlässlich sein, um dieses Potenzial voll auszuschöpfen und sicherzustellen, dass die Vorteile der Dezentralisierung allen Mitgliedern der Bildungsgemeinschaft zugänglich sind.
Durch den strategischen Einsatz von Nostr-basierten Bildungsplattformen kann die Zukunft des Lernens als ein Bereich neu konzipiert werden, in dem offener Zugang, Autonomie der Lernenden und gemeinschaftsgesteuerte Wissensbildung nicht nur möglich, sondern unvermeidlich sind - eine Zukunft, in der dezentrale Infrastrukturen eine gerechtere, transparentere und partizipativere digitale Bildungslandschaft ermöglichen.
Dieser Beitrag hat daher eine ausführliche Einführung in die technischen und konzeptionellen Grundlagen von Nostr gegeben, die Mechanismen beschrieben, mit denen Nostr Dezentralisierung und Zensurresistenz erreicht, und seine bedeutenden Auswirkungen auf die Zukunft von Bildungsplattformen im Rahmen von Open Educational Practices untersucht. Die Synergie zwischen dem dezentralen Paradigma von Nostr und dem demokratischen, inklusiven Ethos von OEP birgt ein transformatives Versprechen für die Neugestaltung der digitalen Bildung in einer Weise, die Offenheit, Sicherheit und lebenslanges Lernen für eine globale Gemeinschaft fördert.
Indem sie aufkommende dezentralisierte Protokolle wie Nostr als grundlegende Infrastrukturkomponenten von Bildungsplattformen nutzen, können Stakeholder eine ganzheitliche digitale Transformation vorantreiben, die Technologie, Pädagogik und Ethik vereint und ein Paradigma einführt, in dem Bildungsgerechtigkeit und digitale Selbstbestimmung keine Wünsche, sondern realisierte Prinzipien unserer vernetzten Gesellschaft sind.
***
Verwendete Quellen:
* Seeing the Politics of Decentralized Social Media Protocols. S Hwang, AX Zhang. ArXiv (2025). <<https://doi.org/10.48550/arxiv.2505.22962>>
* Navigating Decentralized Online Social Networks: An Overview of Technical and Societal Challenges in Architectural Choices. Ujun Jeong, L. Ng, K. Carley, Huan Liu. ArXiv (2504). <<https://doi.org/10.48550/arxiv.2504.00071>>
* Towards Decentralized Applications. X Ma. 2024. <<https://escholarship.org/content/qt0j7044w0/qt0j7044w0_noSplash_c638cdfda170244b9abf750f654399f1.pdf>>
* Decentralized Social Networking Protocol (DSNP) and User Empowerment: An Analysis of Online Identity Ownership, Data Privacy, and Comparative Assessment with Other Decentralized Protocols. M Nay . 2024. <<https://dspace.mit.edu/bitstream/handle/1721.1/156782/nay-mnay-meng-eecs-2024-thesis.pdf?sequence=1&isAllowed=y>>
* A Kantian Right to Fediverse Access, or: for a digital enlightenment on the social web. JN Bingemann . <<https://philarchive.org/archive/BINAKR>>
* Exploring the Nostr Ecosystem: A Study of Decentralization and Resilience. Yiluo Wei, Gareth Tyson. ArXiv (2402). <<https://doi.org/10.48550/arxiv.2402.05709>>
* Money and Trust in Metaverses, Bitcoin and Stablecoins in global social XR. John O'Hare, Allen J. Fairchild, U. Ali. ArXiv (2207). <<https://doi.org/10.48550/arxiv.2207.09460>>
* FEDSTR: Money-In AI-Out | A Decentralized Marketplace for Federated Learning and LLM Training on the NOSTR Protocol. Konstantinos E. Nikolakakis, George Chantzialexiou, Dionysis Kalogerias. ArXiv (2024). <<https://doi.org/10.48550/arxiv.2404.15834>>
* "Sumud" as Connected Learning: Towards a Collective Digital Commons in Palestine.. H Scott, M Ujvari, AMA Bakeer. 2025. <<https://files.eric.ed.gov/fulltext/EJ1464483.pdf>>
***
Erstellt mit Unterstützung von [FutureHouse](https://platform.futurehouse.org/trajectories/08dc7ad8-9767-49b6-a795-1e9c305fabd1), [Perplexity](https://www.perplexity.ai/search/erstelle-mir-eine-detaillierte-4WWgUToWQamxryM370lgag), [Deepl](https://www.deepl.com/de/translator), [Ideogram](https://ideogram.ai/g/JH8xVlA4Scy8lGHELEE-1Q/0) und [ChatGPT](https://chatgpt.com/).

View File

@ -0,0 +1,71 @@
---
layout: post
title: "Die Kraft der Gemeinschaft: Wahre Stärke liegt nicht in Strukturen, sondern in Prozessen"
slug: "kraft-der-gemeinschaft-prozesse"
date: 2025-08-22
description: "Der Artikel im FOERBICO-Projekt betont die Überlegenheit von anpassungsfähigen Prozessen über starre Strukturen für langlebige Bildungs-Communities. Inspiriert vom Ise-Schrein in Japan und Open-Source-Prinzipien wie Linux, wird die Kraft der Gemeinschaft durch kollektive Fürsorge und Erneuerung hervorgehoben."
image: https://blossom.primal.net/83d6a7144e5e939c9bc93b8653fd8a15ef584976663038790671cd7cdcbfab7c.jpg
tags:
- FOERBICO
- Community
- OEP
- Nostr
- Dezentral
- Bildung
- Ise-Schrein
- Open-Source
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
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:
## Prozess statt Bauwerk Die Kraft der Gemeinschaft am Beispiel des Ise-Schreins
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.
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.
## Open Source: Liebe als erneuerbares Baumaterial
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***&#x65;rwenden, zu ***v***&#x65;rarbeiten, zu ***v***&#x65;rmischen, zu ***v***&#x65;rvielfältigen und zu ***v***&#x65;rbreiten, bleiben Bildungsmedien in Plattformen gefangen und ***v***&#x65;rwahrt. 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/)

View File

@ -0,0 +1,51 @@
---
layout: post
title: "Richter oder Rächer? Banksy als moderner Prophet vor dem High Court"
slug: "banksy-high-court-prophet"
date: 2025-09-09
description: "Banksys neues Mural vor dem Londoner High Court zeigt einen Richter, der auf einen Demonstranten einschlägt. Als moderner Prophet prangert Banksy staatliche Repression an und fordert zur Auseinandersetzung mit Fragen von Gerechtigkeit, Gewalt und Barmherzigkeit heraus. Die schnelle Verhüllung des Werks unterstreicht die Brisanz seiner Botschaft."
image: https://blossom.primal.net/c8dd447472a9d764711e089fba71d962f10cb6fc9cc98b0dbd080f07894f2ca6.jpg
tags:
- Banksy
- Streetart
- London
- Justiz
- Gerechtigkeit
- Prophetie
- Bildung
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
Am 8. September 2025 tauchte ein neues Mural von Banksy vor dem **Royal Courts of Justice** in London auf. Es zeigt einen Richter in Perücke und Robe, der mit seinem Hammer auf einen am Boden liegenden Demonstranten einschlägt, dessen Plakat blutverschmiert ist. Banksy bestätigte die Echtheit über Instagram mit dem schlichten Titel *„Royal Courts of Justice“*.
## Politischer Kontext und Timing
Das Werk erscheint kurz nach den Massenverhaftungen von fast 900 Aktivist:innen bei pro-palästinensischen Protesten. Die britische Regierung hatte zuvor die Gruppe **Palestine Action** als Terrororganisation eingestuft. Das Bild wird weithin als Kritik an staatlicher Repression und dem Missbrauch juristischer Macht gelesen.
## Kunsthistorische Einordnung
Banksy bleibt seiner Linie treu: Schablonentechnik, klare Botschaft, öffentlicher Raum als Bühne. Schon frühere Werke thematisierten Machtverhältnisse und Unterdrückung. Das neue Stück reiht sich in diese Tradition ein nur noch unmittelbarer, da es die Justiz selbst ins Zentrum der Kritik rückt.
## Symbolische Kraft
Der Standort ist nicht zufällig gewählt. Direkt an einer Außenwand des Queen's Building, Teil des High-Court-Komplexes, prangert Banksy staatliche Gewalt an. Damit verwandelt er das Symbol der Rechtsstaatlichkeit in ein Sinnbild für Unterdrückung eine subtile, aber kraftvolle Umkehr der Rollen.
## Schnelle Zensur
Wie so oft bei Banksy wurde auch dieses Werk binnen Stunden abgedeckt. Mitarbeiter:innen des Gerichts verhüllten es mit schwarzer Folie und Metallgittern; die Behörden kündigten an, es zu entfernen mit Verweis auf den Denkmalschutz des Gebäudes. Ironischerweise bestätigt dieses Vorgehen die Kritik: eine unmittelbare „Cover-up"-Reaktion gegen unliebsame Kunst.
## Impulse für die Bildung
Banksy beweist erneut, wie politisch aufgeladene Street Art öffentliche Debatten auslösen kann. Das Werk wirft unbequeme Fragen auf:
* Welche Rolle spielt die Justiz im Umgang mit Protestbewegungen?
* Wo liegen die Grenzen von Meinungs- und Versammlungsfreiheit?
* Und wie reagiert der Staat auf kritische Kunst im öffentlichen Raum?
***
### Quellen
[Banksy on Instagram](https://www.instagram.com/p/DOVoHlVDMIU/) · [AP News](https://apnews.com/article/c08b2cef093ea6a0520302eacfbd871f) · [The Guardian](https://www.theguardian.com/artanddesign/2025/sep/08/court-staff-cover-up-banksy-image-of-judge-beating-a-protester) · [El País](https://elpais.com/cultura/2025-09-08/banksy-regresa-a-londres-con-un-mural-en-el-que-un-juez-golpea-a-un-manifestante-tirado-en-el-suelo.html)

235
docs/HANDOFF.md Normal file
View File

@ -0,0 +1,235 @@
# Handoff — Nächste Session
Du (Claude, nächste Session) oder ich (Jörg, später) kommen hier zurück.
Dieses Dokument sagt: was ist der Zustand, was wartet, wo liegen die Fäden.
## Zustand (Details in `STATUS.md`)
**Cutover + Reimport am 2026-04-18 abgeschlossen.** `joerg-lohrer.de`
läuft als SvelteKit-SPA, rendert 26 Nostr-Langform-Posts live aus 5
Relays, Bilder auf Blossom. Repo ist alleinige Quelle der Wahrheit.
Pipeline-Subcommands `publish` + `delete` decken den kompletten
Content-Lifecycle ab.
**Das inhaltliche Kernziel des Gesamtprojekts ist erreicht.** Der Rest
sind optionale Verbesserungen.
## Alltags-Workflow: neuen Post veröffentlichen
**Kompletter Happy-Path, kein manueller Publish nötig:**
1. Neuen Ordner anlegen: `content/posts/YYYY-MM-DD-<slug>/`
2. `index.md` schreiben mit Frontmatter (siehe Template unten).
3. Bilder in den Ordner legen und im Markdown als `![alt](bildname.jpg)`
referenzieren.
4. Lokal validieren: `cd publish && deno task validate-post ../content/posts/<dir>/index.md`
5. Commit + `git push origin main` — fertig.
**Was automatisch passiert:**
- Forgejo-Push-Mirror synct nach GitHub.
- GitHub Actions triggert auf `content/posts/**`-Änderung.
- Workflow läuft diff-modus: nur geänderte/neue Posts werden publiziert.
- Pipeline hasht lokale Bilder → Upload auf beide Blossom-Server → URLs
im Event ersetzen.
- Event wird signiert (Amber-Bunker via `CLIENT_SECRET_HEX`) und auf alle
5 Write-Relays publiziert.
- SPA holt den neuen Post beim nächsten Besuch automatisch vom Relay.
**Vorbedingung:** Amber muss für den Client-Key (aus `CLIENT_SECRET_HEX`)
die Permissions `get_public_key` + `sign_event` auf „Allow + Always"
gesetzt haben. Das gilt so lange, bis der Client-Key rotiert wird.
**Minimal-Frontmatter für einen neuen Post:**
```yaml
---
title: "Titel des Posts"
slug: "url-freundlicher-slug"
date: 2026-04-18
description: "Kurzbeschreibung für SEO und den summary-Tag im Event."
image: hauptbild.jpg
tags:
- Tag1
- Tag2
lang: de
license: https://creativecommons.org/publicdomain/zero/1.0/deed.de
---
Body in Markdown…
```
Bilder mit voller Attribution (NIP-standardisiert nach unserer Konvention,
siehe `docs/superpowers/specs/2026-04-16-image-metadata-convention.md`):
```yaml
images:
- file: hauptbild.jpg
role: cover
alt: "Alt-Text für Barrierefreiheit"
caption: "Bildunterschrift (optional)"
license: https://creativecommons.org/licenses/by/4.0/deed.de
authors:
- name: "Autor:in"
```
**Manuell publizieren** (falls CI aus ist oder einzelner Post nochmal):
```sh
cd publish
deno task publish --post <slug> # einzelner Post
deno task publish --dry-run # was würde der diff-modus publisht?
deno task publish # diff-modus real
deno task publish --force-all # alle 26 Posts neu
```
## Was optional als Nächstes ansteht
### Option B — SPA respektiert NIP-09-Deletion-Events
**Status:** aktuell filtert die SPA nicht nach NIP-09. Wenn ein Event per
`kind:5`-Referenz gelöscht wurde, zeigen Relays es meist nicht mehr aus —
aber die SPA würde es trotzdem rendern, falls ein Relay es doch liefert.
**Zu tun:** im `kind:30023`-Loader (`app/src/lib/nostr/…`) einen
Cross-Check auf `kind:5`-Events einbauen. Events, deren Addressable-Pointer
(`30023:pubkey:d-tag`) in einem `kind:5` referenziert ist, werden
gefiltert. Defensive Maßnahme für zukünftige Duplikate / Soft-Deletes.
### Option C — Postfach `webmaster@joerg-lohrer.de`
User-Task: im All-Inkl KAS als Weiterleitung anlegen. Der Link im
Footer und in den Social-Icons zeigt bereits darauf.
### Option D — Mehrsprachigkeit (Translation-of)
**Grundlage steht:** Pipeline taggt seit 2026-04-18 jedes Event mit
NIP-32 `['L', 'ISO-639-1']` + `['l', 'de', 'ISO-639-1']` (default),
überschreibbar per `lang:`-Frontmatter.
**Zu tun für einen bilingualen Post:**
1. Zweiter Markdown-Ordner, z. B. `content/posts/<date>-<slug>-en/index.md`,
mit `slug: <slug>-en`, `lang: en`, englischem Body.
2. Publish → eigenes `kind:30023`-Event mit `lang=en`.
3. (Noch zu bauen) Pipeline erweitern: `translation_of:`-Frontmatter-Feld,
das ein `['a', '30023:pubkey:<slug-de>']`-Tag ins Event setzt. Damit
erkennen Clients wie Habla die Verwandtschaft.
4. (Optional) SPA bekommt Language-Switcher auf der Post-Detailseite.
Nicht dringend, erst wenn echter englischer Content entsteht.
### Option E — Pipeline weg von GitHub (self-hosted CI)
**Wann:** Wenn der Optiplex-Server steht und ein zentraler Ort für Dienste
existiert.
**Varianten:**
- **Cron / systemd-Timer** auf dem Optiplex, der alle X Minuten `git pull &&
deno task publish` macht. Einfach, minimaler Setup.
- **Woodpecker-CI** als Docker-Container neben Forgejo. Volle Push-getriggerte
Pipeline ohne GitHub.
Der Pipeline-Code selbst (`publish/src/**`) ist CI-agnostisch — nur die
Trigger-Konfiguration ändert sich.
### Option F — Design-Refinements
**Wann:** irgendwann, wenn Lust drauf ist.
- Parallax-Effekte, Animationen
- Dark-Mode-Toggle (aktuell nur `prefers-color-scheme`)
- Typografie-Experimente (Variable Fonts)
- Bildergalerie-Komponente für Posts mit vielen Bildern
Alles nicht-blockierend, die SPA funktioniert solide.
## Schnell-Orientierung für die nächste Claude-Session
Lies in dieser Reihenfolge:
1. `docs/STATUS.md` (5 min)
2. `docs/HANDOFF.md` (= dieses Dokument)
3. Für CI-Themen: `docs/github-ci-setup.md`
4. Für Pipeline-Fragen: `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`
## Dev-Kommandos
```sh
# SPA-Tests
cd app && npm run test:unit
cd app && npm run test:e2e
cd app && npm run check
cd app && npm run dev
# SPA-Build + Deploy
DEPLOY_TARGET=staging ./scripts/deploy-svelte.sh
DEPLOY_TARGET=prod ./scripts/deploy-svelte.sh
# Publish-Pipeline
cd publish && deno task check # pre-flight
cd publish && deno task publish --dry-run # diff-modus simulation
cd publish && deno task publish # diff-modus echt
cd publish && deno task publish --force-all # alle posts
cd publish && deno task publish --post <slug> # einen post
cd publish && deno task delete --event-id <hex> [--event-id <hex>] [--reason "text"]
cd publish && deno task validate-post ../content/posts/<dir>/index.md
cd publish && deno task test # tests
```
## Bekannte Stolperfallen
- **Amber-Bunker:** bei neuer Bunker-URL müssen die zwei Permissions
(`get_public_key`, `sign_event`) in Amber auf „Allow + Always" gesetzt
werden, bevor Publish-Requests verarbeitet werden. Siehe
`docs/github-ci-setup.md` für Details.
- **`CLIENT_SECRET_HEX`** in `.env.local` identisch mit GitHub-Secret —
sorgt dafür, dass sich beide Umgebungen bei Amber mit derselben App
anmelden. Rotieren nur bei bewusstem Neu-Pairing in Amber.
- **`relay.damus.io`** bestätigt Events manchmal nicht mit `OK`. Bekanntes
Damus-Verhalten, wird toleriert (MIN_RELAY_ACKS=2, andere 4 Relays sind
zuverlässig).
- **Svelte 5 Runes:** `$props()`-Werte via `$derived()` in lokale Variablen.
- **Hugo-quotierte Dates:** `date: "2023-02-26"` ist ein YAML-String, nicht
ein Date-Objekt. `validatePost` coerced das automatisch; in neuen Posts
am besten ohne Quotes schreiben.
- **Deploy-Targets:** `svelte` → Entwicklung, `staging` → Pre-Prod,
`prod``joerglohrer26/` (Produktion seit Cutover). Script parst
`.env.local` per awk (wegen Sonderzeichen in FTP-Passwörtern).
- **Slug-Hygiene:** nur `[a-z0-9-]`, keine Umlaute/Emojis/Doppelpunkte.
Der Slug landet als `d`-Tag im Event und wird zur URL. Einmal
publiziert, ist Umbenennen nur über Delete + Re-Publish mit neuem Slug
möglich.
- **Clients, die Markdown ignorieren:** Yakihonne/Habla kennen NIP-32
Sprach-Tags; kurzen Text in `description:` halten, damit die Vorschau
überall sinnvoll aussieht.
## Offene UNKNOWN-Einträge zur späteren Recherche
Im VR-Post (`content/posts/2021-08-15-virtual-reality/index.md`) sind
4 Bilder als `license: UNKNOWN / authors: UNKNOWN` markiert:
- `01-immersion-wikipedia.jpg` (Wikipedia-Screenshot)
- `02-mittelalterliche-kirche.jpg` (Sketchfab — Lizenz ist CC BY-NC, Fotograf fehlt)
- `03-avatare-erstellen.jpg` (Ready Player Me)
- `05-pupillendistanz.jpg` (EyeMeasure iOS App)
Pipeline loggt Warnungen, publisht aber trotzdem. Recherche-Notizen in
`docs/redaktion-bild-metadaten.md`.
## Session-Kontext
Hilfreich beim Wiedereinstieg mit Claude:
- Branch-Check: `git log --oneline -10 main`
- Live-Check: `curl -sI https://joerg-lohrer.de/`
- Event-Count Repo vs. Relays:
```sh
ls content/posts/ | wc -l
nak req -k 30023 -a 4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41 wss://relay.edufeed.org 2>/dev/null | jq -r '.tags[]|select(.[0]=="d")|.[1]' | sort -u | wc -l
```
- Pipeline-Tests: `cd publish && deno task test`
## Community-Wiki-Entwürfe
Liegen im Repo, noch nicht extern veröffentlicht:
- `docs/wiki-entwurf-nostr-bild-metadaten.md` — DE
- `docs/wiki-draft-nostr-image-metadata.md` — EN
Können als NIP-Proposal oder auf nostrbook.dev eingebracht werden, jetzt wo
die Konvention in der Praxis validiert ist.

Some files were not shown because too many files have changed in this diff Show More