Compare commits

...

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

419 changed files with 18025 additions and 4052 deletions

View File

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

View File

@ -1,180 +0,0 @@
---
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

3
.gitmodules vendored Normal file
View File

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

0
.hugo_build.lock Normal file
View File

View File

@ -1,46 +1,3 @@
# joerg-lohrer.de # joerglohrerde
Persönliche Webseite. In Transition von einer Hugo-basierten, statischen Seite update
hin zu einer SvelteKit-SPA, die Blog-Posts live aus signierten Nostr-Events
(NIP-23, `kind:30023`) rendert.
## Aktueller Stand
- **`https://joerg-lohrer.de/`** — Hugo-Seite, läuft noch.
- **`https://spa.joerg-lohrer.de/`** — Vanilla-HTML-Mini-Spike (Proof of Concept).
- **`https://svelte.joerg-lohrer.de/`** — produktive SvelteKit-SPA (Ziel).
Detailliert in [`docs/STATUS.md`](docs/STATUS.md).
## 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)
- 🛠 **SvelteKit-SPA-Plan:** [`docs/superpowers/plans/2026-04-15-spa-sveltekit.md`](docs/superpowers/plans/2026-04-15-spa-sveltekit.md) (35 Tasks, abgeschlossen)
- 🤖 **Claude-Workflow-Skill:** [`.claude/skills/joerglohrerde-workflow.md`](.claude/skills/joerglohrerde-workflow.md)
## Branches
- **`main`** — kanonisch (Content, Specs, Pläne, Deploy-Scripts, Skill).
- **`spa`** — aktueller Arbeitszweig mit allen SvelteKit-Commits. Wird beim
Cutover nach `main` gemerged.
- **`hugo-archive`** — eingefrorener Hugo-Zustand als Orphan-Branch.
Rollback über `git checkout hugo-archive && hugo build`.
## Repo-Struktur
```
content/posts/ Markdown-Posts (Quelle für Nostr-Events)
app/ SvelteKit-SPA (Ziel-Implementation)
preview/spa-mini/ Vanilla-HTML-Mini-Spike (Referenz)
scripts/deploy-svelte.sh FTPS-Deploy nach svelte.joerg-lohrer.de
static/ Site-Assets (Favicons, Profilbild)
docs/ Specs, Pläne, Status, Handoff
.claude/ Claude-Code-Sessions (transparenz) + Skills
```
## Lizenz
Siehe [LICENSE](LICENSE).

5
archetypes/default.md Normal file
View File

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

BIN
assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
assets/icons/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 847 B

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 787 B

View File

@ -0,0 +1,7 @@
<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>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -7,7 +7,7 @@ cover:
image: cura-plugin-change-filment-at-z.png image: cura-plugin-change-filment-at-z.png
tags: [ "QR-Code", "3DDruck" ] tags: [ "QR-Code", "3DDruck" ]
date: "2019-03-26" date: "2019-03-26"
slug: "pflanzenschild-qr-code" slug: "Pflanzenschild-QR-Code"
author: Jörg Lohrer author: Jörg Lohrer
lang: de lang: de
dir: ltr dir: ltr

View File

@ -8,7 +8,7 @@ cover:
tags: [ "ACF", "WordPress", "Formulare", "JSON", "Plugin" ] tags: [ "ACF", "WordPress", "Formulare", "JSON", "Plugin" ]
date: "2021-11-17" date: "2021-11-17"
author: Jörg Lohrer author: Jörg Lohrer
slug: "wordpress-werkstatt" slug: "WordPress-Werkstatt"
lang: de lang: de
dir: ltr dir: ltr
--- ---

View File

@ -8,7 +8,7 @@ cover:
image: 29-autostartordner.jpg image: 29-autostartordner.jpg
tags: [ "Ubuntu", "Google Remote Desktop", "OBS", "Zoom", "relilabtutorial" ] tags: [ "Ubuntu", "Google Remote Desktop", "OBS", "Zoom", "relilabtutorial" ]
date: "2022-03-19" date: "2022-03-19"
slug: "ob-virtualcam" slug: "OB-virtualcam"
lang: de lang: de
dir: ltr dir: ltr
toc: true toc: true

View File

@ -8,7 +8,7 @@ cover:
image: Hefefreuden.jpg image: Hefefreuden.jpg
tags: [ "Dampfnudel", "Hefeteig", "Hefezopf" ] tags: [ "Dampfnudel", "Hefeteig", "Hefezopf" ]
date: "2023-04-07" date: "2023-04-07"
slug: "dampfnudeln" slug: "Dampfnudeln"
lang: de lang: de
dir: ltr dir: ltr
--- ---

View File

@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "BottomUp -> MarkDown - 5V-Power für deine OER!" title: "BottomUp -> MarkDown - 5V-Power für deine OER!"
slug: "bottomup-markdown" slug: "BottomUp-MarkDown"
description: Open Educational Resources mit MarkDown description: Open Educational Resources mit MarkDown
image: bottomup-markdown.png image: bottomup-markdown.png
cover: cover:

View File

@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "KIBedenken - Bewusstsein" title: "KIBedenken - Bewusstsein"
slug: "kibedenken-bewusstsein" slug: "KIBedenken - Bewusstsein"
description: Intelligenz oder Bewusstsein? description: Intelligenz oder Bewusstsein?
image: kibedenken.png image: kibedenken.png
cover: cover:

View File

@ -1,7 +1,7 @@
--- ---
layout: post layout: post
title: "Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter" title: "Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter"
slug: "dezentrale-oep-oer" slug: "Gemeinsam die Bildungszukunft gestalten: Dezentrale OEP und OER als Wegbereiter"
description: "Einladung zum offenen Denken und Handeln in der Bildungsgemeinschaft. Der description: "Einladung zum offenen Denken und Handeln in der Bildungsgemeinschaft. Der
Beitrag diskutiert, warum eine dezentrale Infrastruktur für Open Educational Beitrag diskutiert, warum eine dezentrale Infrastruktur für Open Educational
Resources (OER) und Open Educational Practices (OEP) notwendig ist, um Resources (OER) und Open Educational Practices (OEP) notwendig ist, um

View File

@ -1,140 +0,0 @@
# 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 (siehe `STATUS.md` für Details)
Die SvelteKit-SPA unter `svelte.joerg-lohrer.de` ist **fertig und live**.
35 geplante Tasks + einige Erweiterungen abgeschlossen. Branch `spa` hat
alle Commits. Ein Git-Merge nach `main` und Deploy auf die Hauptdomain ist
**noch nicht** erfolgt — das kommt erst nach dem Cutover-Plan.
## Was als Nächstes ansteht
Drei Optionen, ordered by natürlichkeit der Fortsetzung:
### Option 1 — Publish-Pipeline bauen
**Warum:** aktuell muss Jörg jeden neuen Post manuell mit `nak event` signieren
und publishen (siehe `preview/spa-mini/README.md`, Referenzbefehl in den
Brainstorm-Notizen). Eine Publish-Pipeline automatisiert:
1. Markdown-Post in `content/posts/` bearbeiten / neu anlegen
2. Git-Commit + push auf `main`
3. GitHub Action signiert Event via NIP-46 (Amber-Bunker), pushed zu allen
Relays aus `kind:10002`, lädt Bilder zu Blossom, lädt Altbild-Assets
ggf. zu All-Inkl via SSH/rsync.
**Was existiert:** Spec vollständig unter
`docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`. Plan
**noch nicht geschrieben.**
**Nächster Konkreter Schritt:**
```
superpowers:writing-plans
```
mit dem Publish-Spec als Input.
**Vorarbeiten:**
- SSH-Zugang zu All-Inkl klären (Premium-Tarif angefragt, Status prüfen)
- Deno ≥ 2.x installiert?
- GitHub Actions-Repo-Secrets vorbereiten (`BUNKER_URL`, `ALLINKL_DEPLOY_ROOT`,
`SSH_DEPLOY_KEY`, `AUTHOR_PUBKEY_HEX`)
### Option 2 — Menü-Navigation + Impressum auf der SPA
**Warum:** kleine UX-Ergänzung, die das SPA-Erlebnis runder macht.
- Header-Navigation in `app/src/routes/+layout.svelte` ergänzen (Home, Archiv,
Impressum, evtl. Mastodon-Link)
- `/impressum/`-Route anlegen mit rechtlichem Text
- ggf. Archives-Route als eigene Liste mit Gruppierung nach Jahr
**Aufwand:** ~30-60 min je nach Layout-Wunsch. Kein Spec-Update nötig,
ist in SPA-Spec §2 bereits als Ziel erwähnt.
### Option 3 — Cutover auf Hauptdomain
**Warum:** `joerg-lohrer.de` liefert aktuell noch Hugo aus. Sobald genug
Altposts als Events publiziert sind und die Publish-Pipeline läuft, kann die
SvelteKit-SPA auf die Hauptdomain umziehen. Das ist aber **kein Task jetzt**
— muss auf Publish-Pipeline warten, sonst brechen Backlinks zu Posts, die
noch nicht als Events existieren.
**Reihenfolge:** Option 1 → Publish-Pipeline + einmaliger Massen-Import der
übrigen 17 Altposts → dann Option 3.
## 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. Die relevante Spec, je nachdem was drankommt:
- Publish-Pipeline: `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`
- SPA-Anpassungen: `docs/superpowers/specs/2026-04-15-nostr-page-design.md`
Nutze den Skill unter `.claude/skills/joerglohrerde-workflow.md` für
wiederkehrende Kommandos.
## Dev-Kommandos
```sh
# Unit-Tests (Vitest)
cd app && npm run test:unit
# E2E-Tests (Playwright)
cd app && npm run test:e2e
# Type-Check
cd app && npm run check
# Dev-Server (Port 5173)
cd app && npm run dev
# Production-Build + Deploy
cd app && npm run build && cd .. && ./scripts/deploy-svelte.sh
```
## Manuelles Publishen (bis Publish-Pipeline fertig ist)
Einen Post aus `content/posts/<ordner>/index.md` als kind:30023-Event
publizieren:
```sh
# Body ohne Frontmatter extrahieren
awk 'BEGIN{in_fm=0; past_fm=0} NR==1 && /^---$/ {in_fm=1; next} in_fm && /^---$/ {in_fm=0; past_fm=1; next} past_fm {print}' content/posts/<ordner>/index.md > /tmp/body.md
# Bunker-URL aus .env.local
BUNKER_URL=$(grep -E '^BUNKER_URL=' .env.local | sed 's/^BUNKER_URL=//')
# Event bauen, signieren, zu Relays pushen
# (Tags: d, title, summary, image, published_at, t×n)
# Siehe "dezentrale-oep-oer"-Beispiel in der Brainstorm-Historie
```
Für Bilder: Upload zu Blossom mit `nak blossom upload`:
```sh
nak blossom upload --server https://blossom.edufeed.org --sec "$BUNKER_URL" <bild>
```
## Bekannte Stolperfallen
- **Amber-Bunker:** bei neuer Bunker-URL müssen globale Permissions in Amber
zurückgesetzt werden, sonst hängt `nak` auf den Signatur-Request.
- **All-Inkl FTPS:** bricht mit TLS 1.3 die Data-Connection ab. Script
nutzt `--tls-max 1.2`. Bei SSH-Umstellung: rsync fixen, TLS-Flag raus.
- **Svelte 5 Runes:** `$props()`-Werte müssen via `$derived()` in lokale
Variablen, sonst `state_referenced_locally`-Warning.
- **applesauce-relay API:** ist RxJS-basiert. `pool.request(relays, filter)`
returned `Observable<NostrEvent>` (nicht die Tupel-`subscribe({next: msg
if msg[0]==='EVENT'})`-Form).
- **Slug-Normalisierung:** alle Frontmatter-Slugs sind lowercase (Commit
`d17410f`). Beim Publishen 1:1 übernehmen, keine Runtime-Transformation.
## Session-Kontext
Hilfreich beim Wiedereinstieg mit Claude:
- Branch-Check: `git log --oneline -10 spa main hugo-archive`
- Live-Check: `curl -sI https://svelte.joerg-lohrer.de/`
- Publish-Status: `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])}'`

View File

@ -1,105 +0,0 @@
# Projekt-Status: joerg-lohrer.de → Nostr-basierte SPA
**Stand:** 2026-04-15
## Kurzfassung
Jörg Lohrers persönliche Webseite wird von einem Hugo-basierten statischen
Site-Generator zu einer dezentralen Nostr-basierten SPA überführt. Posts
existieren als signierte Events (NIP-23, `kind:30023`) auf Public-Relays und
werden zur Laufzeit im Browser gerendert.
## Drei parallele Webseiten
| URL | Status | Rolle |
|---|---|---|
| `https://joerg-lohrer.de/` | live, unverändert | **Hugo-Altbestand** (wird noch nicht ersetzt) |
| `https://spa.joerg-lohrer.de/` | live | **Vanilla-HTML-Mini-Spike** (Proof of Concept, ~250 Zeilen HTML+JS) |
| `https://svelte.joerg-lohrer.de/` | live | **SvelteKit-SPA** (35-Task-Plan komplett) |
Die SvelteKit-SPA unter `svelte.joerg-lohrer.de` ist die Ziel-Implementierung.
`spa.joerg-lohrer.de` bleibt als schlanke Referenz erhalten. Hugo läuft weiter,
bis die Publish-Pipeline steht und der Cutover auf die Hauptdomain erfolgt.
## Was auf Nostr liegt
- **Autoren-Pubkey:** `npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9`
(hex: `4fa5d1c413e2b45e10d40bf3562ab701a5331206e359c90baae0e99bfd6c6e41`)
- **Publizierte Events:** ~10 Langform-Posts (`kind:30023`), darunter
`dezentrale-oep-oer`, `offenheit-das-wesentliche`, `gleichnis-vom-saemann`,
`bibelfussball`, `dampfnudeln` u. a.
- **Relay-Liste** (`kind:10002`): `relay.damus.io`, `nos.lol`,
`relay.primal.net`, `relay.tchncs.de`, `relay.edufeed.org`
- **Blossom-Server** (`kind:10063`): `blossom.edufeed.org`, `blossom.primal.net`
Bilder des ersten „experimentell publizierten" Posts (`dezentrale-oep-oer`)
liegen auf Blossom. Weitere 17 Altposts haben ihre Bilder noch unter dem
ursprünglichen Hugo-Permalink auf All-Inkl.
## Repo-Struktur
```
joerglohrerde/
├── content/posts/ # Markdown-Quelle (18 Posts, wird vom Publish-Skript gelesen)
├── app/ # SvelteKit-SPA (Ziel-Implementation)
├── preview/spa-mini/ # Vanilla-HTML-Mini-Spike (Referenz)
├── scripts/
│ └── deploy-svelte.sh # FTPS-Deploy nach svelte.joerg-lohrer.de
├── docs/
│ ├── STATUS.md # Dieses Dokument
│ ├── HANDOFF.md # Wie man hier weitermacht
│ └── superpowers/
│ ├── specs/ # SPA-Spec + Publish-Pipeline-Spec
│ └── plans/ # SPA-Implementation-Plan (35 Tasks, abgeschlossen)
├── .claude/
│ ├── skills/ # Repo-spezifischer Claude-Skill
│ └── settings.local.json # Claude-Session-State (nicht committen? aktuell schon)
└── .env.local # Gitignored: FTP-Creds + Bunker-URL
```
## Branch-Layout (Git)
- **`main`** — kanonischer Zweig. Enthält Content, Specs, Pläne, Deploy-Scripts,
`.claude/`-Skill. Schlanker als früher (kein Hugo-Artefakt mehr).
- **`spa`** — aktueller Arbeits-Branch. SvelteKit-SPA in `app/` komplett
implementiert und live. **Aktuell vor `main` mit allen `spa:`-Commits.**
- **`hugo-archive`** — Orphan-Branch mit dem letzten funktionierenden
Hugo-Zustand, eingefroren. Rollback über `git checkout hugo-archive && hugo build`.
## Setup-Zustand
Einmalig manuell erledigt:
- ✅ Amber-Bunker-URL in `.env.local` als `BUNKER_URL`
- ✅ SPA-FTP-Creds (`spa.joerg-lohrer.de`) in `.env.local` als `SPA_FTP_*`
- ✅ SvelteKit-FTP-Creds (`svelte.joerg-lohrer.de`) in `.env.local` als `SVELTE_FTP_*`
- ✅ `kind:10002`-Event publiziert
- ✅ `kind:10063`-Event publiziert
- ✅ Subdomains mit TLS + HSTS (`max-age=300`)
Alles in `.env.local` — gitignored, nicht committet.
## Offene Punkte / Nicht-in-Scope
- **Publish-Pipeline** (Spec vorhanden unter `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md`, Plan noch nicht geschrieben)
- **Menü-Navigation** in der SPA (Home / Archiv / Impressum / Kontakt)
- **Impressum-Seite** (braucht rechtlichen Text)
- **Meta-Stubs für Social-Previews und SEO** (wird Teil der Publish-Pipeline)
- **SSH-Zugang zu All-Inkl** (laut Notiz von Jörg: Premium-Tarif im Kommen → rsync statt FTPS möglich)
- **Cutover auf `joerg-lohrer.de`** (Hauptdomain bekommt dann die SvelteKit-SPA)
## Live-Verifikation
Jederzeit:
```sh
curl -sI https://svelte.joerg-lohrer.de/ | head -3
curl -sI https://spa.joerg-lohrer.de/ | head -3
```
## Kontakt zur Implementierung
Alle Design-Entscheidungen in:
- `docs/superpowers/specs/2026-04-15-nostr-page-design.md` (SPA)
- `docs/superpowers/specs/2026-04-15-publish-pipeline-design.md` (Publish)
- `docs/superpowers/plans/2026-04-15-spa-sveltekit.md` (35-Task-Plan, abgeschlossen)
Für die nächste Session: `docs/HANDOFF.md` lesen.

File diff suppressed because it is too large Load Diff

View File

@ -55,32 +55,19 @@
## 2. URL-Struktur, Routing, Event-Kontrakt ## 2. URL-Struktur, Routing, Event-Kontrakt
### URL-Schema ### URL-Schema (kompatibel zur bestehenden Hugo-Struktur)
**Kanonische Form — kurz und teilbar:**
| URL | Inhalt | | URL | Inhalt |
|---|---| |---|---|
| `/` | SPA-Shell. SPA rendert Startseite (Profilkachel + Post-Liste). | | `/` | SPA-Shell. SPA rendert Startseite (Profilkachel + Post-Liste). |
| `/<dtag>/` | **Kanonische Post-URL.** SPA-Router extrahiert `<dtag>` und lädt Event. | | `/YYYY/MM/DD/<dtag>.html/` | SPA-Shell (via `.htaccess`-Fallback). SPA-Router extrahiert `<dtag>` und lädt Event. |
| `/archives/` | SPA-Shell, SPA rendert chronologische Liste. |
| `/tag/<name>/` | SPA-Shell, SPA rendert Tag-Filter. | | `/tag/<name>/` | SPA-Shell, SPA rendert Tag-Filter. |
| `/impressum/` | Statisches HTML (rechtlicher Content, liegt wirklich auf Server). | | `/impressum/` | Statisches HTML (rechtlicher Content, liegt wirklich auf Server). |
| `/YYYY/MM/DD/<dtag>.html/<bildname>` | echte Bilddateien der 18 Altposts, liegen unter dem jeweiligen Post-Permalink. |
| `/favicon.ico`, `/logo.png`, `/robots.txt` | globale Site-Assets. | | `/favicon.ico`, `/logo.png`, `/robots.txt` | globale Site-Assets. |
**Legacy-Form — nur für Backlink-Kompatibilität:** **Datum in der URL** dient nur der Backlink-Kompatibilität. Die SPA benötigt zur Event-Abfrage nur den `dtag`; sie fragt Relays mit `kinds:[30023], authors:[<pubkey>], #d:[<dtag>]`. Das Datum ist URL-Dekoration.
Die bestehenden Hugo-URLs haben die Form `/YYYY/MM/DD/<dtag>.html/`. Externe Backlinks (Google, Mastodon-Bookmarks etc.) zeigen auf diese URLs. Die SPA behandelt sie so:
1. `.htaccess` rewriet den Pfad auf `index.html` (wie alle SPA-Routen).
2. Der Client-Router erkennt das Legacy-Muster, extrahiert den `<dtag>` am Ende.
3. Via `history.replaceState()` wird die URL in der Adressleiste ohne Reload auf die kanonische kurze Form `/<dtag>/` umgeschrieben.
4. Post wird über normalen kanonischen Weg geladen.
So bleiben alle alten Links funktional, aber die geteilten/bookmarkten URLs konvergieren zur kurzen Form. **Neue Posts werden nur unter der kurzen Form verbreitet** — die Datumsform ist keine aktive URL-Strategie mehr, nur Legacy.
**Datum in der URL** dient nur der Legacy-Kompatibilität. Die SPA benötigt zur Event-Abfrage nur den `dtag`; sie fragt Relays mit `kinds:[30023], authors:[<pubkey>], #d:[<dtag>]`.
**Bildpfade der 18 Altposts** folgen dem historischen Hugo-Schema (`/YYYY/MM/DD/<dtag>.html/<bildname>`), weil die Bilder bereits dort auf All-Inkl liegen und relative Verweise in den Markdown-Bodies auf diese absoluten URLs umgeschrieben werden. Neue Posts nutzen Blossom-URLs (siehe Publish-Spec).
### `.htaccess` ### `.htaccess`
@ -129,45 +116,32 @@ Die SPA unterscheidet die beiden Eras nicht — sie rendert Markdown, der Browse
SvelteKit mit `adapter-static`, `ssr: false`, Fallback-Page `index.html`. Routen: SvelteKit mit `adapter-static`, `ssr: false`, Fallback-Page `index.html`. Routen:
- `/` → Home (Profil + Beitragsliste) - `/` → Home
- `/[dtag]/` → PostView (kanonische Form) - `/[year]/[month]/[day]/[dtag].html/` → PostView (nur `dtag` genutzt)
- `/archives/` → Archives
- `/tag/[name]/` → TagView - `/tag/[name]/` → TagView
- `/impressum/` → Impressum - `/impressum/`eigener Impressum-Pfad (oder statisch außerhalb der SPA)
**Legacy-Normalisierung:** Pfade der Form `/YYYY/MM/DD/<dtag>.html/` werden beim Routing erkannt, via `history.replaceState` auf `/<dtag>/` umgeschrieben, danach die reguläre PostView-Route aufgerufen. Kein HTTP-Redirect nötig — rein clientseitig. **Hinweis zum `.html`-Suffix im Routing:** SvelteKit unterstützt statische Dateiendungen in dynamischen Segmenten nicht direkt. Lösung: Entweder den `.html`-Suffix im Parameter-Wert mitbehandeln (`[dtag]` matched den String inklusive `.html`, davon wird beim Auslesen `.html` abgeschnitten) oder den Pfad als Catch-All-Route `[...slug]` aufnehmen und die Teile im Load-Handler selbst parsen. Details in der Implementation.
### Relay-Konfiguration ### Relay-Konfiguration
Relay-Liste kommt aus dem NIP-65-Outbox-Event (`kind:10002`) des Autors. Das Event wird einmalig manuell publiziert (siehe Publish-Spec, Abschnitt „Pre-Flight-Setup") und enthält die bevorzugten Read- und Write-Relays. Fest im Bundle hinterlegte Default-Liste (Konfig-Datei, nicht hartcodiert):
**Auflösung zur Laufzeit:**
1. SPA kennt genau einen hartcodierten **Bootstrap-Relay** (`wss://relay.damus.io`).
2. Beim Boot: SPA fragt Bootstrap-Relay nach `{ kinds:[10002], authors:[PUBKEY] }`.
3. Aus dem Event werden die `["r", <url>]`-Tags extrahiert (Read-Relays für die SPA-Abfragen).
4. Diese Liste wird für alle weiteren Nostr-Requests genutzt.
**Fallback:** Falls Bootstrap-Relay nicht antwortet oder `kind:10002` nicht existiert, nutzt die SPA eine hartcodierte Fallback-Liste.
```ts ```ts
// src/lib/nostr/config.ts // src/lib/nostr/config.ts
export const BOOTSTRAP_RELAY = 'wss://relay.damus.io' export const READ_RELAYS = [
// TODO bei Implementierung: npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9
// in hex decodieren (nip19.decode) und hier eintragen.
export const AUTHOR_PUBKEY_HEX = '<hex wird bei Implementierung aus npub abgeleitet>'
// Nur Fallback: wenn kind:10002 nicht geladen werden kann.
export const FALLBACK_READ_RELAYS = [
'wss://relay.damus.io', 'wss://relay.damus.io',
'wss://nos.lol', 'wss://nos.lol',
'wss://relay.nostr.band', 'wss://relay.nostr.band',
'wss://nostr.wine', 'wss://nostr.wine',
] ]
// TODO bei Implementierung: npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9
// in hex decodieren (nip19.decode) und hier eintragen.
export const AUTHOR_PUBKEY_HEX = '<hex wird bei Implementierung aus npub abgeleitet>'
``` ```
**Vorteil:** Änderungen an der Relay-Liste (z. B. späteres Hinzufügen eines eigenen Relays) erfordern nur ein neues `kind:10002`-Event, keinen Code-Deploy. Später erweiterbar um eigenen Relay per Konfig-Änderung, kein Code-Umbau.
Blossom-Server-Liste wird analog aus `kind:10063` (BUD-03) aufgelöst — siehe Publish-Spec für das normative Schema beider Events.
--- ---
@ -339,40 +313,6 @@ Schätzung gzip:
## 4. Hosting, Deployment, Migrationspfad ## 4. Hosting, Deployment, Migrationspfad
### Warum Webspace für SvelteKit ausreicht
Häufige Sorge: „kann mein einfaches Webspace-Paket eine SvelteKit-App hosten, ich habe ja keinen vServer und kein SSH?" Antwort: ja, problemlos. Hier warum.
**SvelteKit produziert reine statische Dateien.** Mit dem `adapter-static` (siehe §3) erzeugt `npm run build` einen Ordner `build/`, der nichts anderes enthält als:
- `index.html` — eine HTML-Datei
- `_app/` mit JS/CSS-Bundles — kompilierte Dateien
- weitere statische Files (favicon etc.)
Das ist exakt das gleiche Material, das Hugo bisher in `public/` produziert hat — nur mit JS statt vielen einzelnen HTML-Seiten. **Beides ist statisches Hosting, beides funktioniert auf jedem Webspace, der HTML ausliefern kann.**
**Was du brauchst:**
- Ordner zum Hochladen ✅ (jeder Webspace)
- Webserver, der HTML/JS/CSS ausliefert ✅ (jeder Webspace)
- Apache mit `mod_rewrite` für SPA-Fallback (eine `.htaccess`) ✅ (All-Inkl-Standard)
**Was du nicht brauchst:**
- ❌ Node.js auf dem Server
- ❌ Datenbank
- ❌ vServer/SSH (für *Hosting* nicht; SSH wird nur für komfortablen *Upload* via rsync genutzt — FTP funktioniert genauso)
- ❌ irgendetwas, das dauerhaft serverseitig läuft
**Unterschied Hugo vs. SvelteKit aus Server-Sicht:**
- Hugo erzeugt **viele HTML-Dateien**, eine pro Post. Server liefert pro URL eine spezifische Datei.
- SvelteKit (SPA) erzeugt **eine HTML-Datei** und ein JS-Bundle. Server liefert immer dieselben Dateien, der Browser entscheidet per JavaScript, was angezeigt wird (basierend auf der URL).
Für den Server ist Variante 2 sogar **simpler** — er hat weniger Dateien zu verwalten und muss nichts dynamisch generieren.
**Was die `.htaccess` macht:** wenn jemand `https://joerg-lohrer.de/2025/03/04/dezentrale-oep-oer.html/` aufruft, gibt es diese Datei nicht physisch auf dem Server — der Pfad ist eine virtuelle SPA-Route. Apache würde 404 antworten. Eine kleine `.htaccess`-Datei sagt Apache: „wenn die angeforderte Datei nicht existiert, liefere `/index.html` aus." Browser bekommt die SPA-Shell, JavaScript liest die URL, lädt das richtige Event vom Relay, rendert den Post.
**Was am Ende auf dem Webspace liegt** (siehe Dateistruktur weiter unten): ungefähr 3080 Dateien, zusammen 100200 KB. Weniger als ein einziges Foto. Komplett statisch, kein Backend.
### Hosting bei All-Inkl ### Hosting bei All-Inkl
Webhosting-Paket, Standardfeatures: Webhosting-Paket, Standardfeatures:
@ -508,14 +448,6 @@ Für externe Links: identische URL, identische Inhaltsanzeige. Backlinks aus Mas
Jeder Phasenwechsel: additiv oder lokal begrenzter Refactor, kein Rewrite. Jeder Phasenwechsel: additiv oder lokal begrenzter Refactor, kein Rewrite.
### Daten-Transparenz vs. defensive Korrektur
Die SPA soll Events **wahrheitsgetreu** rendern und **nicht still Daten korrigieren**. Wenn ein Event Auffälligkeiten enthält (doppelte `t`-Tags, leeres `d`, inkonsistente Groß-/Kleinschreibung gegenüber anderen Events desselben Autors), soll die SPA das sichtbar lassen — nicht transparent wegdedupen. Grund: der Autor merkt sonst nicht, dass seine Events Daten-Mängel haben.
Daten-Bereinigung gehört in **separate Audit-Werkzeuge** (siehe Publish-Spec, z. B. künftiger `deno task audit`), die auf Basis von Relay-Queries einen Report erstellen und mögliche Korrektur-Commits in den Markdown-Quelltext vorschlagen.
Der Mini-SPA-Spike (`preview/spa-mini/`) dedup'te pragmatisch; die produktive SPA tut das nicht.
### Success-Kriterien Phase 1 ### Success-Kriterien Phase 1
- Alle 18 alten Post-URLs liefern korrekten Inhalt (Visual-Parity, nicht pixelgenau). - Alle 18 alten Post-URLs liefern korrekten Inhalt (Visual-Parity, nicht pixelgenau).

View File

@ -1,677 +0,0 @@
# Publish-Pipeline für Nostr-Events — Design-Spec
**Datum:** 2026-04-15
**Status:** Entwurf, ausstehende User-Freigabe
**Scope:** Toolchain, die Markdown-Posts aus `content/posts/*/index.md` in signierte Nostr-Events (`kind:30023`, NIP-23) umwandelt, zu Relays publiziert, und die zugehörigen Bilder zum Asset-Host (All-Inkl für Altposts, Blossom für neue) hochlädt.
Diese Spec ist die Schwester-Spec zu [`2026-04-15-nostr-page-design.md`](2026-04-15-nostr-page-design.md) und teilt sich mit ihr den Event-Kontrakt für `kind:30023` und die Konfiguration über `kind:10002` / `kind:10063`.
---
## 1. Gesamtarchitektur
```
Auslöser
┌───────────────────┬───────────────────┐
│ │ │
▼ ▼ ▼
Lokaler CLI GitHub Action workflow_dispatch
`deno task (push auf main, (--force-all, z. B.
publish` wenn content/ für Migration oder
posts/** geändert) Reimport)
│ │ │
└───────────────────┴───────────────────┘
┌─────────────────────────────┐
│ Publish-Pipeline (Deno) │
│ gemeinsame Library + CLI │
│ │
│ 1. Nostr-Kontext laden: │
│ • kind:10002 (Relays) │
│ • kind:10063 (Blossom) │
│ 2. Change-Detection │
│ (Git-Diff oder force) │
│ 3. Pro Post: │
│ a. Frontmatter parsen │
│ b. Markdown transform │
│ c. Bilder upload │
│ (legacy/blossom) │
│ d. Event bauen │
│ e. Via NIP-46 signieren │
│ f. Zu Relays pushen │
└──────┬──────────────────────┘
┌──────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼
Amber Public Blossom- All-Inkl
(NIP-46 Nostr- Server (rsync
Signer Relays (primal, over SSH,
via aus später eigen) Altbilder
Relay) kind:10002 aus der 18
kind:10063) Migrations-
posts)
```
### Kernprinzipien
- **Deno als Runtime.** Native TypeScript, Permissions-Modell, keine `node_modules`.
- **Gemeinsame Library + CLI.** Kernlogik in Modulen, sowohl von lokaler CLI als auch von CI-Workflow importiert. Keine Duplikation.
- **Nostr als Source-of-Truth für Konfiguration.** Relay-Liste aus `kind:10002`, Blossom-Serverliste aus `kind:10063`. Keine YAML-Config im Repo.
- **NIP-46 Bunker für Signaturen.** Der private Schlüssel liegt nie in der Pipeline-Umgebung (nicht lokal, nicht in CI-Secrets). Bunker-Stufe Amber zum Start, Bunker-Stufe Optiplex nachrüstbar ohne Code-Change.
- **Git-Diff als Change-Detection.** Pipeline publisht nur geänderte Posts. Override-Flag für Migration und Reimport.
- **State-los im Repo.** Keine Lock-Files, kein Commit-zurück. CI ist read-only auf Repo-Content.
- **Idempotenz.** Wiederholte Läufe ohne inhaltliche Änderung erzeugen keine neuen Events (Git-Diff filtert).
### Kostenübersicht
- Deno: 0 €.
- Amber, Public-Relays, Public-Blossom: 0 €.
- GitHub-Actions: im Free-Tier für persönliche Repos ausreichend.
- All-Inkl: unverändert, bereits Premium-Tarif für SSH.
- **Zusatzkosten: keine.**
### Out-of-Scope
Diese Spec behandelt nicht:
- **Kommentare/Reactions auf Posts.** Die kommen von Besuchern über die SPA via NIP-07 (siehe SPA-Spec §3). Publish-Pipeline publisht ausschließlich Autor-eigene `kind:30023`.
- **SPA-Deployment** (SvelteKit-Bundle-Upload). Wird in einem separaten Deploy-Mechanismus behandelt oder als optionaler Subcommand nachgerüstet.
- **Domain-Verwaltung, TLS-Zertifikate, All-Inkl-Paketwahl.** Infrastruktur-seitig außerhalb der Pipeline.
---
## 2. Pre-Flight-Setup
Bevor der erste Publish-Lauf erfolgen kann, müssen folgende Bedingungen einmalig manuell erfüllt sein. Der Subcommand `deno task check` verifiziert die Punkte und gibt klare Fehlermeldungen aus, wenn etwas fehlt.
### 2.1 Nostr-Identität
- Pubkey des Autors: **npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9**
- Privater Schlüssel (`nsec`) existiert nur in **Amber** auf dem Handy des Autors. Keine andere Instanz hält ihn.
### 2.2 NIP-46-Bunker-Pairing (Amber)
1. Auf dem Handy: Amber öffnen, Account wählen.
2. In Amber: „Generate Bunker URL" o. ä. — erzeugt eine `bunker://<hex-pubkey>?relay=wss://...&secret=...` URL.
3. Im Handy-Amber: Permission-Regeln setzen:
- `kind:30023` signieren → **auto-approve** für die Publish-Pipeline-App
- alle anderen Kinds → prompt (Sicherheitsnetz, sollte nicht aufschlagen)
4. Bunker-URL in die Pipeline-Umgebung einfügen:
- **Lokal:** in `.env` als `BUNKER_URL=bunker://...` (in `.gitignore`)
- **CI:** als GitHub-Actions-Secret `BUNKER_URL`
5. Amber muss während CI-Runs online sein (WLAN oder mobile Daten). Akku-Optimierung für Amber auf dem Handy deaktivieren.
### 2.3 Relay-Liste (`kind:10002`)
Einmalig manuell publizieren via Nostr-Client (z. B. nostrudel.ninja mit Amber-Login, oder direkt aus Amber).
**Schema (NIP-65):**
```json
{
"kind": 10002,
"pubkey": "<hex>",
"tags": [
["r", "wss://relay.damus.io"],
["r", "wss://nos.lol"],
["r", "wss://relay.nostr.band"],
["r", "wss://nostr.wine"]
],
"content": "",
"created_at": <unix>
}
```
**Lese-Semantik der Pipeline (NIP-65):**
- `["r", <url>]` ohne drittes Element → Relay ist sowohl Read als auch Write.
- `["r", <url>, "read"]` → nur Read; Pipeline **ignoriert** beim Publish.
- `["r", <url>, "write"]` → nur Write; Pipeline nutzt beim Publish.
Phase 1: alle Einträge ohne drittes Element (beides). Spätere Differenzierung möglich, ohne Code-Änderung.
Replaceable Event (kein `d`-Tag) — bei späteren Updates (z. B. eigener Relay hinzu) wird einfach ein neues `kind:10002` publiziert, das das alte ersetzt.
### 2.4 Blossom-Serverliste (`kind:10063`)
Einmalig manuell publizieren. Phase-1-Inhalt: ein Server.
**Schema (BUD-03):**
```json
{
"kind": 10063,
"pubkey": "<hex>",
"tags": [
["server", "https://blossom.primal.net"]
],
"content": "",
"created_at": <unix>
}
```
Phase-5-Erweiterung (eigener Blossom-Server): zusätzliches `["server", "https://blossom.joerg-lohrer.de"]` wird vorne in die Liste aufgenommen, neues Event publiziert.
### 2.5 SSH-Deploy-Key für All-Inkl
1. Lokal Keypair erzeugen, **dediziert für Deploys**, nicht persönlicher SSH-Key:
```
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_joerglohrerde_deploy -C "deploy-joerglohrerde"
```
Ohne Passphrase (CI braucht non-interactive Zugang).
2. Public-Key-Inhalt (`*.pub`) in All-Inkl-KAS unter „SSH-Zugänge" → „Authorized Keys" eintragen.
3. Verbindung testen: `ssh -i ~/.ssh/id_ed25519_joerglohrerde_deploy w00xxxxx@ssh.all-inkl.com`
4. Private-Key bereitstellen:
- **Lokal:** liegt in `~/.ssh/` und wird von rsync automatisch genutzt.
- **CI:** als GitHub-Actions-Secret `SSH_DEPLOY_KEY` (Inhalt der privaten Key-Datei). Im Workflow wird er in `~/.ssh/id_ed25519` gechrieben und `chmod 600` gesetzt.
### 2.6 All-Inkl Deploy-Root
Nach Tarifwechsel auf Premium: Pfad im KAS unter „Dateiverwaltung" ablesen. Typisch: `w00xxxxx@ssh.all-inkl.com:joerg-lohrer.de/`.
- **Lokal:** in `.env` als `ALLINKL_DEPLOY_ROOT`
- **CI:** als GitHub-Actions-Secret
### 2.7 `deno task check`
Dieser Subcommand verifiziert alle obigen Punkte:
- `BUNKER_URL` gesetzt, Bunker antwortet auf Ping, Pubkey stimmt mit `AUTHOR_PUBKEY_HEX` überein.
- `kind:10002` auf Bootstrap-Relay gefunden, mindestens 1 Relay eingetragen.
- `kind:10063` auf Bootstrap-Relay gefunden, mindestens 1 Server eingetragen.
- SSH-Verbindung zu `ALLINKL_DEPLOY_ROOT` erfolgreich (`ssh ... echo ok`).
- Deno-Version und benötigte Permissions.
Bei jedem Fehler: klare Text-Meldung, was zu tun ist (z. B. „kind:10002 fehlt — publiziere es manuell mit folgendem Schema: ...").
---
## 3. Event-Kontrakt (normativ)
### 3.1 `kind:30023` — Blog-Post (NIP-23)
**Pflicht-Tags:**
- `["d", "<slug>"]` — Slug-String, identisch mit Frontmatter `slug:`. Lowercase und URL-kompatibel (az, 09, `-`). Ist Teil des Tupels `(pubkey, kind, d)` für Replaceable-Semantik.
- `["title", "<title-string>"]` — aus Frontmatter `title:`.
- `["published_at", "<unix-seconds>"]` — aus Frontmatter `date:`, als Unix-Zeitstempel in Sekunden. **Stabil** über Edits hinweg — ändert sich nie.
**Empfohlene Tags (wenn im Frontmatter vorhanden):**
- `["summary", "<summary>"]` — aus Frontmatter `description:`.
- `["image", "<absolute-url>"]` — aus Frontmatter `cover.image:` (oder `image:`), transformiert zur absoluten URL gemäß Abschnitt 4.
- `["t", "<tag>"]` — ein Tag-Element pro Eintrag in Frontmatter `tags:`. Tag-Strings unverändert übernommen (Groß-/Kleinschreibung erhalten, weil Tag-Konvention im Nostr-Ökosystem case-sensitive ist).
**Event-Header:**
- `kind`: 30023
- `pubkey`: `AUTHOR_PUBKEY_HEX`
- `created_at`: Unix-Zeitstempel des Signatur-Zeitpunkts (ändert sich bei jedem Edit).
- `content`: Markdown-Body nach Bild-URL-Transformation (Abschnitt 4).
**Nicht gemappt** (Hugo-spezifische Frontmatter-Felder ohne Nostr-Entsprechung):
`layout`, `cover.caption`, `cover.alt`, `author`, `lang`, `dir`, `toc`, `toc_label`, `toc_icon`, `comments`, `weight`, `menus`, `aliases`, `draft`.
**Draft-Behandlung:** `draft: true` im Frontmatter → Pipeline publisht diesen Post **nicht** (überspringt ohne Fehler, loggt Info).
### 3.2 `kind:10002` — NIP-65 Outbox-Relays
Siehe Abschnitt 2.3. Von der Publish-Pipeline nur **gelesen** (Bootstrap beim Start); nicht von der Pipeline publiziert.
### 3.3 `kind:10063` — BUD-03 Blossom-Serverliste
Siehe Abschnitt 2.4. Von der Publish-Pipeline nur **gelesen** (vor Blossom-Upload); nicht von der Pipeline publiziert.
### 3.4 Signing
Alle ausgehenden Events werden via **NIP-46 Bunker** signiert (nicht NIP-07 — dieser Flow ist rein browserseitig und für die CLI nicht anwendbar). Implementierung via `applesauce-signers` `Nip46Signer`:
```ts
const signer = new Nip46Signer(BUNKER_URL)
await signer.getPublicKey() // initialisiert Verbindung
const signed = await signer.signEvent(unsignedEvent)
```
---
## 4. Markdown- und Bild-Transformation
### 4.1 Frontmatter-Parsing
YAML-Frontmatter zwischen `---`-Trennern. Parser: `jsr:@std/yaml` oder `npm:gray-matter`.
Slug kommt als **lowercase String** aus dem Frontmatter-Feld `slug:`. Ist bereits normalisiert (siehe Commit `d17410f`) — Pipeline muss nichts ableiten oder lowercasen.
**Validierung:**
- `title`, `date`, `slug` müssen vorhanden sein; sonst harter Fehler für diesen Post.
- `slug` muss regex `^[a-z0-9][a-z0-9-]*$` matchen; sonst harter Fehler.
### 4.2 Bild-URL-Transformation
Ziel: alle relativen Bild-Referenzen im Markdown-Body werden zu absoluten URLs.
**Erkannte Muster:**
- `![alt](filename)` — reguläre Markdown-Bild-Syntax.
- `[![alt](filename)](link)` — Bild-in-Link-Konstrukt.
- `![alt](filename =WxH)` — mit Größen-Suffix (Obsidian/PaperMod-Erweiterung).
**Regeln:**
1. Wenn `filename` ein Schema enthält (`http://`, `https://`, `//`), nicht transformieren — ist schon absolut.
2. Ansonsten zu absoluter URL machen; URL-Kodierung pro Pfad-Segment via `encodeURIComponent()`.
3. `=WxH`-Suffix entfernen; die SPA skaliert Bilder per CSS responsiv.
**Basis-URL je nach `image_source`-Frontmatter:**
- Wenn `image_source: legacy``https://joerg-lohrer.de/<YYYY>/<MM>/<DD>/<dtag>.html/<encoded-filename>`
- `YYYY/MM/DD` aus `date:`-Frontmatter, nicht aus dem Signatur-Zeitpunkt.
- `<dtag>` ist identisch mit `slug`.
- Wenn `image_source` fehlt oder `image_source: blossom` → Blossom-URL; siehe Abschnitt 5.
### 4.3 `image_source`-Flag
**Einmaliger Migrationsschritt (vor erstem Publish-Lauf):** Die 18 Altposts bekommen `image_source: legacy` ins Frontmatter geschrieben. Das ist ein separater Commit, kein Pipeline-Feature.
**Neue Posts:** kein Flag nötig, Default = `blossom`. Wenn ein zukünftiger Post explizit auf All-Inkl zeigen soll (außergewöhnlich), kann `image_source: legacy` gesetzt werden.
### 4.4 Cover-Image-Tag
Das `image`-Tag im Event (für Listen-Previews/OG-Vorschau in Nostr-Clients) kommt aus dem Frontmatter (nicht aus dem Markdown-Body):
- Quelle: `cover.image:` (Hugo-Page-Bundle-Konvention); Fallback `image:` auf Top-Level.
- Ist typischerweise ein relativer Dateiname.
- Wird durch denselben URL-Bauer wie die Body-Bilder geschickt (Abschnitt 4.2), aber der Input ist ein direkter Dateiname aus YAML, nicht aus Markdown-Syntax. Keine `=WxH`-Suffix-Erkennung nötig.
- Ergebnis: absolute URL gemäß `image_source`-Policy.
---
## 5. Upload-Pfade
### 5.1 Legacy-Upload (All-Inkl)
Betrifft: die 18 Altposts, Bilder darin.
**Mechanik:** `rsync` over SSH via `Deno.Command("rsync", [...])`.
**Befehlsschema:**
```
rsync -avz --no-perms --no-times \
-e "ssh -i $DEPLOY_KEY_PATH -o StrictHostKeyChecking=accept-new" \
<post-folder>/*.{png,jpg,jpeg,gif,webp,svg} \
$ALLINKL_DEPLOY_ROOT<YYYY>/<MM>/<DD>/<dtag>.html/
```
- **Idempotent:** rsync überträgt nur neue/geänderte Dateien.
- **Nicht-löschend:** ohne `--delete`. Alte Bilder bleiben auf dem Server liegen, keine automatische Bereinigung. Manueller Aufräum-Bedarf wird hingenommen (Tote Dateien verursachen keinen Schaden, Storage ist billig).
- **Zielordner erzeugen:** rsync legt fehlende Ordner per `--mkpath` oder (wenn Version zu alt) per vorgeschaltetem `ssh ... mkdir -p` an.
**Neuer Post-Edit mit alten Bildern:** falls jemand mal einen Post editiert, der `image_source: legacy` hat und neue Bilder hinzufügt → diese werden auch zu All-Inkl geschoben. Das ist okay. Das Flag steuert nur den URL-Basispfad, nicht die Intention „nie wieder All-Inkl".
### 5.2 Blossom-Upload
Betrifft: alle neuen Posts (`image_source: blossom` oder fehlend).
**Mechanik:** BUD-01 HTTP-Upload zu allen Servern aus `kind:10063`-Liste, parallel.
**Schritte pro Bild:**
1. SHA256-Hash der Datei berechnen.
2. Authorization-Event (`kind:24242`) bauen und via Bunker signieren (enthält Hash, Verb `upload`, Expiration).
3. HTTP `PUT /upload` gegen alle Server gleichzeitig mit Auth-Header `Nostr <base64-signed-event>`.
4. Antworten sammeln: pro Server entweder `200 { url, sha256, ... }` oder Fehler.
5. Erfolg: mindestens 1 Server hat die Datei akzeptiert. Optimal: alle.
6. Markdown-URL nutzt die URL des **ersten Servers** aus der `kind:10063`-Liste (deterministisch, reproduzierbar).
**Failure-Modi:**
- Alle Server lehnen ab → harter Fehler, Pipeline bricht für diesen Post ab.
- Manche Server OK, manche Fehler → Warnung in Log, Pipeline fährt fort mit erfolgreichem Upload.
**Retry:** 2 Versuche pro Server mit exponentiellem Backoff.
---
## 6. Change-Detection und Workflow
### 6.1 Welche Posts werden publiziert?
**Modus 1 — Git-Diff (Standard):**
Pipeline vergleicht Dateiliste zwischen `HEAD~1` (lokal) bzw. `${{ github.event.before }}` (CI) und `HEAD`. Alle `.md` in `content/posts/**/`, die darin als `A` (added), `M` (modified) oder `R` (renamed) auftauchen, werden publiziert.
**Modus 2 — `--force-all` (Migration / Reimport):**
Alle `content/posts/**/*.md` werden publiziert, unabhängig von Git-Diff. Verwendet für:
- Initiale Migration der 18 Altposts (einmaliger lokaler Lauf).
- Nachträgliches Reimport nach Schema-Änderungen.
**Modus 3 — `--post <slug>` (Einzel-Post, für Debug):**
Nur der Post mit dem angegebenen Slug wird verarbeitet.
### 6.2 Trigger
**Lokal:** `deno task publish [--force-all | --post <slug> | --dry-run]`.
**GitHub Action:**
```yaml
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
type: boolean
default: false
```
- Push auf `main` mit Content-Änderung → automatischer Publish im Git-Diff-Modus.
- Manual-Trigger via GitHub-UI → optional `force_all=true`, dann `--force-all`-Lauf.
### 6.3 Idempotenz und Doppelpublikationen
Bei ausschließlicher Nutzung einer Variante (nur lokal ODER nur CI) ist Git-Diff-Detection präzise.
**Edge Case:** Wenn du lokal einen Post publisht, *ohne* zu pushen, und später CI läuft (z. B. für eine Content-Änderung an einem anderen Post), bekommt CI keinen Diff für den schon-lokal-publizierten Post — keine Doppelpublikation. Wenn du *pushst*, sieht CI im Diff die Änderung und publisht den Post erneut (dank replaceable-Semantik ist das funktional harmlos, nur etwas Relay-Bandbreite-Waste).
**Akzeptable Redundanz.** Spec dokumentiert es, aber keine aktive Mitigation.
### 6.4 Updates bestehender Posts
Ein Edit eines bereits publizierten Posts führt zu einem neuen `kind:30023`-Event mit:
- Selbem `d`-Tag, selbem `pubkey`, selbem `kind` → ersetzt das alte Event (Replaceable-Semantik).
- Selbem `published_at` (Datum aus Frontmatter, unverändert).
- Neuem `created_at` (Signaturzeit).
- Geändertem `content` und ggf. Tags.
Die Pipeline loggt explizit „**UPDATE**" vs. „**NEU**", indem sie vor dem Publish das Relay befragt, ob bereits ein Event für `(pubkey, kind, dtag)` existiert. Rein informativ; beide Pfade nutzen denselben Code.
---
## 7. Fehlerbehandlung und Retries
### 7.1 Relay-Publish
Pro Post wird das signierte Event an alle Relays aus der `kind:10002`-Liste parallel geschickt. Pro Relay:
- Bis zu 2 Retries mit exponentiellem Backoff (1s, 3s).
- Erfolg = Relay antwortet mit `OK true`.
- Timeout pro Versuch: 10 Sekunden.
**Erfolgskriterium pro Post:** mindestens 2 von 4 Relays haben bestätigt. Weniger → harter Fehler, Post wird als „failed" markiert, Pipeline fährt mit nächstem Post fort, am Ende Exit-Code != 0.
**Log pro Relay:** Status (OK / fail / timeout), Roundtrip-Zeit.
### 7.2 Blossom-Upload
Siehe Abschnitt 5.2. Pro Server 2 Retries, mindestens 1 Server muss akzeptieren.
### 7.3 Legacy-Upload
rsync-Aufruf wird bei Exit-Code != 0 einmal wiederholt (1 Retry, 3 s Pause). Bleibt der Aufruf fehlerhaft, wird der Post als failed markiert und die Pipeline fährt mit dem nächsten fort.
### 7.4 Bunker-Signing
- Timeout 30 Sekunden pro Signatur-Request (Handy-Wake-up berücksichtigen).
- 1 Retry bei Timeout.
- Fehler (Permission denied, Bunker offline) → harter Abbruch der gesamten Pipeline (ohne Signaturen geht nichts).
### 7.5 Logging
Pipeline schreibt pro Run ein strukturiertes JSON-Log:
```json
{
"run_id": "<uuid>",
"started_at": "<iso>",
"mode": "diff | force-all | post-single",
"posts": [
{
"slug": "offenheit-das-wesentliche",
"status": "success | failed | skipped-draft",
"action": "new | update",
"event_id": "<64-hex>",
"relays_ok": ["wss://..."],
"relays_failed": [],
"blossom_servers_ok": [],
"images_uploaded": 3,
"duration_ms": 1234
}
],
"ended_at": "<iso>",
"exit_code": 0
}
```
- **stdout:** in menschenlesbarer Form gedruckt.
- **CI:** zusätzlich als Artefakt `publish-log.json` hochgeladen (30 Tage Retention). Keine Repo-Commits zurück.
- **Lokal:** zusätzlich in `./logs/publish-<timestamp>.json` (lokal in `.gitignore`).
---
## 8. Modul- und Dateistruktur
```
publish/
├── deno.jsonc # Imports, Tasks, Permissions
├── .env.example # Dokumentation (Commit), keine Werte
├── .gitignore # .env, logs/
├── README.md # Quickstart
├── src/
│ ├── cli.ts # CLI-Entrypoint (mit `@std/cli`)
│ ├── core/
│ │ ├── config.ts # BOOTSTRAP_RELAY, AUTHOR_PUBKEY_HEX
│ │ ├── frontmatter.ts # parseFrontmatter(md): { fm, body }
│ │ ├── validation.ts # validateSlug, validatePost
│ │ ├── markdown.ts # transformImageUrls, stripSizeHints
│ │ ├── event.ts # buildKind30023(fm, body)
│ │ ├── signer.ts # NIP-46 Bunker-Wrapper
│ │ ├── relays.ts # loadOutboxRelays, publishEvent
│ │ ├── blossom.ts # loadServerList, uploadBlob
│ │ ├── legacy-upload.ts # rsync SSH wrapper
│ │ ├── change-detection.ts # gitDiff, allPostFiles, forceMode
│ │ └── log.ts # structured logger + JSON writer
│ └── subcommands/
│ ├── publish.ts # Hauptbefehl, alle 3 Modi inkl. --dry-run
│ ├── check.ts # Pre-Flight-Validation
│ └── validate-post.ts # Einzel-Post-Check ohne Upload (nur Frontmatter/Bilder)
├── tests/
│ ├── frontmatter_test.ts
│ ├── validation_test.ts
│ ├── markdown_test.ts
│ ├── event_test.ts
│ ├── change-detection_test.ts
│ └── fixtures/
│ └── sample-post.md
└── .github/
└── workflows/
└── publish.yml # CI-Workflow
```
**Tests:** Deno-Standard-Test-Runner. Fokus auf Unit-Tests für pure Transformationen (frontmatter, markdown, event-bauen); Integration-Tests mit Mock-Relay und Mock-Bunker.
---
## 9. Testing-Strategie
### 9.1 Unit-Tests
- `parseFrontmatter`: diverse Real-Beispiele aus den 18 Altposts, Edge Cases (Leerzeichen in Strings, YAML-Blocks).
- `validateSlug`: Regex-Matching-Grenzen.
- `transformImageUrls`: alle Markdown-Bild-Muster, Leerzeichen in Dateinamen, bereits absolute URLs.
- `buildKind30023`: Frontmatter → Event-Objekt, Tag-Mapping, draft-Behandlung.
- `gitDiff`: Mock `git` subprocess.
### 9.2 Integration-Tests
- Mock-Relay (`jsr:@welshman/relay-mock` oder einfacher in-memory WebSocket-Mock).
- Mock-Bunker: Test-Signer mit bekanntem Key.
- Full-Flow: Sample-Post → signieren → publish gegen Mock-Relay → Event vom Mock abrufen → Inhalt vergleichen.
### 9.3 End-to-End (manuell, einmalig)
- Auf Testnetz: Dedicated Test-Relay, Test-Pubkey, Test-Amber-Account.
- Einen Sample-Post durchschieben, in Habla.news verifizieren.
### 9.4 Pre-Flight-Check als Test
`deno task check` wird auch von CI vor jedem Publish-Run ausgeführt. Failed Check → Pipeline bricht ab bevor irgendwas publiziert wird.
---
## 10. GitHub-Actions-Workflow
```yaml
name: Publish Nostr Events
on:
push:
branches: [main]
paths: ['content/posts/**']
workflow_dispatch:
inputs:
force_all:
type: boolean
default: false
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # für git-diff
- uses: denoland/setup-deno@v1
with:
deno-version: v2.x
- name: Setup SSH-Deploy-Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan ssh.all-inkl.com >> ~/.ssh/known_hosts
- name: Pre-Flight Check
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
ALLINKL_DEPLOY_ROOT: ${{ secrets.ALLINKL_DEPLOY_ROOT }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
run: deno task check
- name: Publish
env:
BUNKER_URL: ${{ secrets.BUNKER_URL }}
ALLINKL_DEPLOY_ROOT: ${{ secrets.ALLINKL_DEPLOY_ROOT }}
AUTHOR_PUBKEY_HEX: ${{ secrets.AUTHOR_PUBKEY_HEX }}
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
run: |
if [ "${{ inputs.force_all }}" = "true" ]; then
deno task publish --force-all
else
deno task publish
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: publish-log
path: ./logs/publish-*.json
retention-days: 30
```
---
## 11. Beziehung zur SPA-Spec
Diese Publish-Pipeline und die SPA sind komplementär, aber voneinander entkoppelt:
**Gemeinsame Verträge** (normativ festgelegt in dieser Spec, Abschnitt 3):
- `kind:30023` Event-Schema — Publish produziert, SPA konsumiert.
- `kind:10002` Relay-Liste — Publish liest, SPA liest.
- `kind:10063` Blossom-Liste — Publish liest beim Upload, SPA liest für Bild-Fallback (zukünftig).
- Bild-URL-Konvention für Altposts `/YYYY/MM/DD/<dtag>.html/<file>` — Publish schreibt, SPA erwartet.
**Unabhängige Entwicklung möglich:**
- Publish kann gegen Mock-Relay und Mock-Bunker entwickelt und getestet werden, ohne dass die SPA existiert.
- SPA kann gegen manuell via `nak` o. ä. geschriebene Test-Events entwickelt werden, ohne dass die Publish-Pipeline existiert.
**Abhängigkeit beim Cutover (SPA-Migrationsschritte C + D):**
- SPA kann erst live gehen, wenn die 18 Altposts als Events auf Relays liegen.
- **Schritt C** der SPA-Migration bedeutet konkret: einmaliger lokaler Lauf `deno task publish --force-all` mit dem vollständigen Altbestand. Dieser Schritt liegt zeitlich **vor** Schritt D (dem tatsächlichen Cutover auf All-Inkl).
- Voraussetzung ist, dass die Publish-Pipeline zu diesem Zeitpunkt vollständig implementiert und durch `deno task check` validiert ist.
**Laufender Betrieb:**
- Neue Posts: Markdown committen, CI triggert Publish.
- SPA zeigt neuen Post beim nächsten Seitenreload an (Relay-Abfrage ist live).
- Zwei unabhängige Deploy-Zyklen (Publish bei Content-Änderung, SPA-Bundle bei Code-Änderung) ohne Kopplung.
---
## 12. Risiken und Mitigationen
| Risiko | Wahrsch. | Auswirkung | Mitigation |
|---|---|---|---|
| Amber offline während CI | mittel | hoch (Pipeline bricht ab) | Clear Error; Nutzer retriggert manuell nachdem Handy verfügbar |
| Bunker-Secret leakt (Repo-Secret) | niedrig | mittel | Secret rotierbar: in Amber Pairing löschen, neu pairen, Secret aktualisieren |
| SSH-Deploy-Key leakt | niedrig | mittel | Dedicated Key, in All-Inkl-KAS revokebar |
| `kind:10002` versehentlich überschrieben (Relay-Liste leer) | niedrig | hoch | check-Subcommand prüft vor jedem Run; Pipeline bricht bei leerer Liste ab |
| Relay-Zensur (Events werden gelöscht) | niedrig | mittel | Multi-Relay-Push; zusätzlich bezahltes nostr.wine als Durability-Anker |
| Git-Diff übersieht Post (Rebase, Force-Push) | niedrig | niedrig | `--force-all` als Fallback, dokumentiert |
| Blossom-Server löscht Bild | mittel | mittel | Multi-Upload zu mehreren Servern sobald kind:10063 erweitert ist |
| `encodeURIComponent` vs. All-Inkl Apache: URL-Matching fällt auseinander | niedrig | mittel | Tests gegen reale URLs; Normalisierungs-Regel (lowercase Slugs, ASCII-Filenames bevorzugt) |
| Privater Schlüssel-Recovery | niedrig | **katastrophal** | Amber hat Backup-Mechanismus; `nsec` zusätzlich offline auf Hardware sichern |
---
## 13. Evolutionspfad
**Jetzt (Bunker-Stufe Amber, Phase 1 Blossom):**
- Handy mit Amber als einziger Signer, online während Publish-Runs.
- Ein Blossom-Server in `kind:10063` (primal).
- Legacy-Bilder auf All-Inkl für die 18 Altposts.
- Relay-Liste mit 4 Public-Relays.
**Bunker-Stufe Optiplex (sobald Proxmox-Container läuft):**
- Self-hosted Bunker (z. B. `nak bunker` als Container), 24/7 online.
- Connection-URL im Secret rotieren; Amber bleibt als Backup/manueller Signer.
- Keine Code-Änderung in der Pipeline.
**Phase 5 Blossom (eigener Blossom-Server auf Optiplex):**
- Zusätzlicher `server`-Tag in `kind:10063` (`https://blossom.joerg-lohrer.de`).
- Neue Posts werden automatisch auch dorthin hochgeladen (Multi-Upload).
- Markdown-URL zeigt auf Primär-Server (= erster Eintrag in Liste). Soll das der eigene sein: Liste entsprechend ordnen.
**Optional später:**
- `deno task mirror` — Subcommand, der bestehende Bilder (z. B. vom ersten Server) auch zu später hinzugefügten Servern spiegelt. Hilft bei Blossom-Server-Wechsel.
---
## 14. Success-Kriterien Phase 1
- `deno task check` ohne Fehler.
- 18 Altposts via einmaligem `deno task publish --force-all` publiziert.
- Jeder Post in mindestens 2 Public-Relays abrufbar, in Habla.news korrekt gerendert.
- Bilder der 18 Posts via `/YYYY/MM/DD/<dtag>.html/<bildname>` auf All-Inkl erreichbar.
- Ein neuer Test-Post via CI auf `main`-Push publiziert in unter 90 Sekunden ab Push.
- `publish-log.json` enthält aussagekräftige Einträge pro Post.
- Pipeline läuft ohne nsec-Exposition in irgendeiner Umgebung.
---
## Anhang: Begriffe
- **NIP-23:** Nostr-Langform-Events, `kind:30023`, replaceable per `d`-Tag.
- **NIP-46:** Nostr-Remote-Signer-Protokoll (Bunker). Signatur-Anfrage und -Antwort verschlüsselt über Relays.
- **NIP-65:** Outbox-Model, `kind:10002`, definiert Read/Write-Relays pro Autor.
- **BUD-01:** Blossom-Upload-Definition: `PUT /upload` mit Nostr-Auth-Header.
- **BUD-03:** Blossom-User-Description-03, `kind:10063` mit Server-Liste.
- **Amber:** Android-App, die als NIP-46-Signer fungiert.
- **Replaceable Event:** Ersetzt vorherige Events mit gleichem `(pubkey, kind, d)`-Tupel auf dem Relay.

140
hugo.toml Normal file
View File

@ -0,0 +1,140 @@
baseURL = 'https://joerg-lohrer.de/'
languageCode = 'de'
defaultContentLanguage = 'de'
title = 'Jörg Lohrer'
theme = 'papermod'
# SEO keywords and description for your site.
keywords = "OER, Religionspädagogik, Bildung, Religion, Hugo"
subtitle = "Jörg Lohrer - Webseite"
[permalinks]
posts = "/:year/:month/:day/:slug.html"
[outputs]
home = [ "HTML", "RSS", "JSON" ]
[menus]
[[menus.main]]
name = 'Home'
pageRef = '/'
weight = 10
[[menus.main]]
name = 'Impressum'
pageRef = '/Impressum'
weight = 30
[[menus.main]]
name = 'Blog'
pageRef = '/archives'
weight = 20
[[menus.main]]
name = 'Mastodon'
pre = '<i class="fa fa-heart"></i>'
url = 'https://reliverse.social/@joerglohrer'
weight = 40
[menus.main.params]
rel = 'external'
[params.header]
logo = "joerg-profil-2024.webp"
[params]
social = true
comments = true
[[params.jsonLD]]
enabled = true
# Konfiguration PaperMod-Theme
[params.profileMode]
enabled = true
title = "Hi 🖖"
subtitle = "Willkommen auf meinem Blog 🤗"
imageUrl = "joerg-profil-2024.webp"
imageTitle = "Mein Profilbild"
imageWidth = 120
imageHeight = 120
[[params.profileMode.buttons]]
name = "Archiv"
url = "/archives"
[[params.profileMode.buttons]]
name = "tag"
url = "/tag"
[[params.socialIcons]]
name = "Mastodon"
url = "https://reliverse.social/@joerglohrer"
[[params.socialIcons]]
name = "email"
url = "mailto:lohrer@comenius?subject=Kontakt%20%C3%BCber%20joerg-lohrer.de&body=Hallo%20J%C3%B6rg,%0D%0A%0D%0Aich%20nehme%20%C3%BCber%20deine%20Webseite%20Kontakt%20mit%20dir%20auf."
[[params.socialIcons]]
name = "linkedin"
url = "https://www.linkedin.com/in/joerglohrer"
[[params.socialIcons]]
name = "rss"
url = "/index.xml"
[[params.socialIcons]]
name = "cv"
url = "/index.json"
# Social Icons Tailwind-Theme
[params.social_media]
[[params.social_media.items]]
enabled = true
title = 'Mastodon'
icon = 'brand-mastodon'
link = 'https://reliverse.social/@joerglohrer'
[[params.social_media.items]]
enabled = true
title = 'nostr'
icon = 'brand-nostr'
link = 'https://nostter.app/npub1f7jar3qnu269uyx5p0e4v24hqxjnxysxudvujza2ur5ehltvdeqsly2fx9'
[[params.social_media.items]]
enabled = true
title = 'Bluesky'
icon = 'brand-bluesky'
link = 'https://bsky.app/profile/joerglohrer.bsky.social'
[[params.social_media.items]]
enabled = true
title = 'Instagram'
icon = 'brand-instagram'
link = 'https://www.instagram.com/joerglohrer'
[taxonomies]
category = "category"
tag = "tag"
series = "series"
[markup.goldmark.renderer]
unsafe = true
[markup.highlight]
noClasses = false
[pwa]
enabled = false
icon = "logo.png" # should be in assets
icon_sizes = [192, 512]
# will be used as the manifest.json file and merge the default one
# https://developer.mozilla.org/en-US/docs/Web/Manifest
# https://web.dev/add-manifest/
[pwa.manifest]
description = "Webseite Jörg Lohrer"

View File

@ -0,0 +1,191 @@
{{ with .Params.comments }}
<section id="comments" class="article-content">
<h2>Kommentare</h2>
<p>Mit einem Mastodon- oder Fediverse-Account kannst du <a href="https://{{ .host }}/@{{ .username }}/{{ .id }}">hier antworten</a></p>
<p id="mastodon-comments-list"><button id="load-comment">Kommentare laden</button></p>
<div id="comments-wrapper">
<noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://{{ .host }}/@{{ .username }}/{{ .id }}">the original post</a> on Mastodon.</p></noscript>
</div>
<noscript>You need JavaScript to view the comments.</noscript>
<script src="/js/purify.min.js"></script>
<script type="text/javascript">
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function emojify(input, emojis) {
let output = input;
emojis.forEach(emoji => {
let picture = document.createElement("picture");
let source = document.createElement("source");
source.setAttribute("srcset", escapeHtml(emoji.url));
source.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let img = document.createElement("img");
img.className = "emoji";
img.setAttribute("src", escapeHtml(emoji.static_url));
img.setAttribute("alt", `:${ emoji.shortcode }:`);
img.setAttribute("title", `:${ emoji.shortcode }:`);
img.setAttribute("width", "20");
img.setAttribute("height", "20");
picture.appendChild(source);
picture.appendChild(img);
output = output.replace(`:${ emoji.shortcode }:`, picture.outerHTML);
});
return output;
}
function loadComments() {
let commentsWrapper = document.getElementById("comments-wrapper");
document.getElementById("load-comment").innerHTML = "Loading";
fetch('https://{{ .host }}/api/v1/statuses/{{ .id }}/context')
.then(function(response) {
return response.json();
})
.then(function(data) {
let descendants = data['descendants'];
if(
descendants &&
Array.isArray(descendants) &&
descendants.length > 0
) {
commentsWrapper.innerHTML = "";
descendants.forEach(function(status) {
console.log(descendants)
if( status.account.display_name.length > 0 ) {
status.account.display_name = escapeHtml(status.account.display_name);
status.account.display_name = emojify(status.account.display_name, status.account.emojis);
} else {
status.account.display_name = status.account.username;
};
let instance = "";
if( status.account.acct.includes("@") ) {
instance = status.account.acct.split("@")[1];
} else {
instance = "{{ .host }}";
}
const isReply = status.in_reply_to_id !== "{{ .id }}";
let op = false;
if( status.account.acct == "{{ .username }}" ) {
op = true;
}
status.content = emojify(status.content, status.emojis);
let avatarSource = document.createElement("source");
avatarSource.setAttribute("srcset", escapeHtml(status.account.avatar));
avatarSource.setAttribute("media", "(prefers-reduced-motion: no-preference)");
let avatarImg = document.createElement("img");
avatarImg.className = "avatar";
avatarImg.setAttribute("src", escapeHtml(status.account.avatar_static));
avatarImg.setAttribute("alt", `@${ status.account.username }@${ instance } avatar`);
let avatarPicture = document.createElement("picture");
avatarPicture.appendChild(avatarSource);
avatarPicture.appendChild(avatarImg);
let avatar = document.createElement("a");
avatar.className = "avatar-link";
avatar.setAttribute("href", status.account.url);
avatar.setAttribute("rel", "external nofollow");
avatar.setAttribute("title", `View profile at @${ status.account.username }@${ instance }`);
avatar.appendChild(avatarPicture);
let instanceBadge = document.createElement("a");
instanceBadge.className = "instance";
instanceBadge.setAttribute("href", status.account.url);
instanceBadge.setAttribute("title", `@${ status.account.username }@${ instance }`);
instanceBadge.setAttribute("rel", "external nofollow");
instanceBadge.textContent = instance;
let display = document.createElement("span");
display.className = "display";
display.setAttribute("itemprop", "author");
display.setAttribute("itemtype", "http://schema.org/Person");
display.innerHTML = status.account.display_name;
let header = document.createElement("header");
header.className = "author";
header.appendChild(display);
header.appendChild(instanceBadge);
let permalink = document.createElement("a");
permalink.setAttribute("href", status.url);
permalink.setAttribute("itemprop", "url");
permalink.setAttribute("title", `View comment at ${ instance }`);
permalink.setAttribute("rel", "external nofollow");
permalink.textContent = new Date( status.created_at ).toLocaleString('en-US', {
dateStyle: "long",
timeStyle: "short",
});
let timestamp = document.createElement("time");
timestamp.setAttribute("datetime", status.created_at);
timestamp.appendChild(permalink);
let main = document.createElement("main");
main.setAttribute("itemprop", "text");
main.innerHTML = status.content;
let interactions = document.createElement("footer");
if(status.favourites_count > 0) {
let faves = document.createElement("a");
faves.className = "faves";
faves.setAttribute("href", `${ status.url }/favourites`);
faves.setAttribute("title", `Favorites from ${ instance }`);
faves.textContent = status.favourites_count;
interactions.appendChild(faves);
}
let comment = document.createElement("article");
comment.id = `comment-${ status.id }`;
comment.className = isReply ? "comment comment-reply" : "comment";
comment.setAttribute("itemprop", "comment");
comment.setAttribute("itemtype", "http://schema.org/Comment");
comment.appendChild(avatar);
comment.appendChild(header);
comment.appendChild(timestamp);
comment.appendChild(main);
comment.appendChild(interactions);
if(op === true) {
comment.classList.add("op");
avatar.classList.add("op");
avatar.setAttribute(
"title",
"Blog post author; " + avatar.getAttribute("title")
);
instanceBadge.classList.add("op");
instanceBadge.setAttribute(
"title",
"Blog post author: " + instanceBadge.getAttribute("title")
);
}
commentsWrapper.innerHTML += DOMPurify.sanitize(comment.outerHTML);
});
}
});
}
document.getElementById("load-comment").addEventListener("click", loadComments);
</script>
</section>
{{ end }}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Jörg Lohrer</title>
<meta name="keywords" content="">
<meta name="description" content=" - Jörg Lohrer">
<meta name="author" content="">
<link rel="canonical" href="http://localhost:1313/1/01/01/.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/1/01/01/.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
</h1>
<div class="post-meta">
</div>
</header>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

View File

@ -0,0 +1,241 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Premium, Freemium, Mium mium mium | Jörg Lohrer</title>
<meta name="keywords" content="OER, Freemium, MOOC">
<meta name="description" content="Zur Produktion von sattmachenden Bildungsinhalten">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2013/02/07/premium-freemium-mium-mium-mium.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2013/02/07/premium-freemium-mium-mium-mium.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Premium, Freemium, Mium mium mium
</h1>
<div class="post-description">
Zur Produktion von sattmachenden Bildungsinhalten
</div>
<div class="post-meta"><span title='2013-02-07 00:00:00 +0000 UTC'>Februar 7, 2013</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2013/02/07/premium-freemium-mium-mium-mium.html/my-very-hungry-caterpillar.jpg" alt="">
</figure>
<div class="post-content"><h1 id="premium-freemium-mium-mium-mium">Premium, Freemium, Mium mium mium<a hidden class="anchor" aria-hidden="true" href="#premium-freemium-mium-mium-mium">#</a></h1>
<p>Wer leckere Bildungsangebote macht, findet immer reichlich Abnehmer. Und im digitalen Zeitalter kann ein einzelnes Produkt dank der vorhandenen Replikatormaschinen unendlich vervielfacht konsumiert werden. Das ist kein Traum mehr, sondern längst cloudgewordene Wirklichkeit.</p>
<p>Nun wird versucht über ein komplexes Rechtemanagement den Produzenten ein lukratives Einkommen zu sichern. Doch während im Dienstleitungssektor der Bildung die einen noch an Vertriebswegen tüfteln, sind die Teilnehmenden in den offenen und massiven Online-Kursen (MOOCs) schon einen Schritt weiter. Hier wird allmählich begriffen, dass nicht die Her- und Bereitstellung, Aufbereitung und didaktische Darbietung von Bildung sich in barer Münze auszahlt, sondern das vernetzende Lernen selbst, also der Prozess, die eigentliche Qualität darstellt, die ihr Geld wirklich wert wäre.</p>
<p>![](my-very-hungry-caterpillar.jpg =240x)
Foto: <a href="https://www.flickr.com/photos/fizzkitten/4454153264/">365.79 My very hungry caterpillar - Relly Annett-Baker</a> <a href="https://creativecommons.org/licenses/by-nc-sa/3.0/">CC BY-NC-SA</a></p>
<p>Wo früher noch für ein Produkt bezahlt wurde, wird morgen ein Vorgang monetarisiert werden müssen. Übergangslösungen sind bezahlte Dienstleitungen, Werbeeinblendungen, Freischaltcodes, digitales Rechtemanagement oder Crowdfunding. Insgesamt zeigt sich zugleich, dass Geld immer weniger einen materialisierbaren Gegenwert erhält und sich somit eventuell die Substanz der Währung allmählich aufzulösen beginnt. Doch genausowenig wie man Geld essen kann, wird man von Bildung satt. Die Frage, die sich stellt, ist also keine geringere als die nach einer Kalibrierung der Geldfunktionen.
Gegenseitige Anerkennung, Verlässlichkeit und Loyalität, Kooperation und Ideenreichtum, Unterstützung und Feedback sind im Rahmen eines offenen Online-Kurses ohnehin unbezahlbar und treten verblüffenderweise da am stärksten auf, wo kein Geldtransfer im Spiel ist. Das zeigt einmal mehr, dass man sich die besten Dinge im Leben mit Geld eben gerade nicht kaufen kann.
Doch satt bin ich noch immer nicht.</p>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Erlebnispädagogik im Handbuch Jugend Evangelische Perspektiven | Jörg Lohrer</title>
<meta name="keywords" content="Erlebnispädagogik, Jugendliche">
<meta name="description" content="Artikel unter CC-BY Lizenz">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2013/05/29/erlebnispadagogik-im-handbuch-jugend-evangelische-perspektive.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2013/05/29/erlebnispadagogik-im-handbuch-jugend-evangelische-perspektive.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Erlebnispädagogik im Handbuch Jugend Evangelische Perspektiven
</h1>
<div class="post-description">
Artikel unter CC-BY Lizenz
</div>
<div class="post-meta"><span title='2013-05-29 00:00:00 +0000 UTC'>Mai 29, 2013</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<div class="post-content"><h1 id="erlebnispädagogik-im-handbuch-jugend--evangelische-perspektiven">Erlebnispädagogik im Handbuch Jugend Evangelische Perspektiven<a hidden class="anchor" aria-hidden="true" href="#erlebnispädagogik-im-handbuch-jugend--evangelische-perspektiven">#</a></h1>
<p>Das
<a href="http://ci-muenster.de/bookshop/artikel/buecher/Bildung-im-Kindes-und-Jugendalter/A40097_Handbuch_Jugend_2013.php"><img loading="lazy" src="http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&amp;ASIN=3847400746&amp;Format=_SL160_&amp;ID=AsinImage&amp;MarketPlace=DE&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=httpwwwjoergl-21" alt="" />
</a>
<strong>Handbuch Jugend Evangelische Perspektiven</strong>, welches 2013 erschienen ist, hat auf Seite 343-345 folgenden Artikel von mir zur Erlebnispädagogik abgedruckt.
<a href="http://creativecommons.org/licenses/by/2.0/de/">CC BY</a> Jörg Lohrer</p>
<h2 id="erlebnispädagogik">Erlebnispädagogik<a hidden class="anchor" aria-hidden="true" href="#erlebnispädagogik">#</a></h2>
<h3 id="ziele-und-arbeitsformen">Ziele und Arbeitsformen<a hidden class="anchor" aria-hidden="true" href="#ziele-und-arbeitsformen">#</a></h3>
<p>Erlebnispädagogik arbeitet mit einem pädagogischen Konzept zielorientiert und bevorzugt in der Natur oder dem naturnahen Raum vorrangig an der Förderung von Selbst- und Sozialkompetenzen. Dabei grenzt sich Erlebnispädagogik bewusst von Nervenkitzel-Aktionismus und der eskalierenden Suche nach dem Kick nach immer mehr und phantastischeren Erlebnissen ab. Die Förderung von Zutrauen in die eigenen Fähigkeiten, der Umgang mit Ängsten, das Erfahren und Überwinden von Grenzen, die Vemittlung von sozialen Kompetenzen können Zielsetzungen für erlebnispädagogische Maßnahmen in der evangelischen Jugendbildung sein. Dabei geht es auch immer um eine Erweiterung der eigenen Handlungskompetenzen zur Lebensbewältigung durch ein angstfreies Lernen in der Gruppe.</p>
<p>Erlebnispädagogik arbeitet mit Wahrnehmungs- und Vertrauensübungen, Kooperations- und Problemlösungsaufgaben, abenteuerlichen Aktionen, persönlichen Herausforderungen und Grenzerfahrungen. Angebote und Maßnahmen mit erlebnispädagogischem Charakter sind zum Beispiel Kanu- und Fahrrad-Touren, Kletteraktionen, kooperative Abenteuerspiele, Natursensibilisierung, Trekkingtouren, Nieder- und Hochseilparcours oder Geocaching.</p>
<p>Erlebnispädagogische Maßnahmen finden u.a. Anwendung im Bereich der Kooperation von Jugendverbandsarbeit und Schule, bei Klassenfahrten, der Präventionsarbeit, bei gruppendynamischen Prozessen, Mitarbeiterschulungen, der Entwicklung von Teamarbeit und in etlichen weiteren Praxisfeldern evangelischer Jugendbildung.</p>
<p>Mit den Arbeitsprinzipien Respekt, Selbstbestimmung, Vertrauen, Kooperation, Verantwortung und Ganzheitlichkeit korrespondiert die Erlebnispädagogik mit dem Profil der evangelischen Jugendarbeit. Ebenso liegen Chancen in einer Verknüpfung von erlebnispädagogischen Aktionen mit einer erfahrungsbezogenen Verkündigung. Erlebnispädagogische Maßnahmen werden als Jugendbildungsveranstaltungen gefördert, wenn sozial-ökologische Inhalte und/oder politisch-kulturelle Schwerpunktthemen mit gesellschaftlichem Bezug im Mittelpunkt der Maßnahme stehen.</p>
<h3 id="entstehung-prinzipien-methoden">Entstehung, Prinzipien, Methoden<a hidden class="anchor" aria-hidden="true" href="#entstehung-prinzipien-methoden">#</a></h3>
<p>Der Bundesverband Individual- und Erlebnispädagogik e.V. (BE) nennt weit in die Vergangenheit zurück reichende Wurzeln der Erlebnispädagogik. Als wichtige Autoren, auf die das Konzept vom handlungsorientierten Lernen zurückgeführt wird, werden Platon (427347 v.Chr.), Rousseau (17121778), Pestalozzi (17461827) und schließ- lich Kurt Hahn (18861974) als Urvater der Erlebnispädagogik genannt. Ihre heutige Vielfalt und weite Verbreitung hat die Erlebnispädagogik allerdings erst im 20. Jahrhundert und schließlich in den letzten Jahrzehnten entwickelt. Erlebnispädagogik wird nicht nur im Bereich der Erziehung von Kindern und Jugendlichen angewandt, sondern findet inzwischen auch vielfältige Beachtung in der Arbeit mit nahezu allen Altersgruppen zu unterschiedlichen Problem- und Zielstellungen in der präventiven Kurzzeitpädagogik, der intensiven sozialpädagogischen Einzelbetreuung, im Rahmen der beruflichen Fort und Weiterbildung oder in Führungskräftetrainings (vgl. <a href="https://www.bundesverband-erlebnispaedagogik.de/">www.bundesverband- erlebnispaedagogik.de</a>).</p>
<p>Als Grundprinzipien können unter anderem benannt werden: Handlungsorientierung, Ganzheitlichkeit, Eigenverantwortung, Freiwilligkeit, Sicherheit und Nachhaltig- keit. Dabei sind die pädagogischen, kompetenz- und ressourcenorientierten Angebote der Erlebnispädagogik stets eingebettet in die aktuelle Rechtsgrundlage und (Bildungs-) Politik.</p>
<p>Methodische Aspekte sind z.B.: der hohe Stellenwert des Erlebnisses und das Arbeiten mit erlebnispädagogischen Lernszenarien, nicht-alltäglichen Herausforderungen und Wagnissen, Einsatz verschiedener Medien und der Natur als bevorzugtem Lern- und Erfahrungsraum.
Neben einer zunehmende Gruppenselbststeuerung baut die Erlebnispädagogik auf Lern- und Wirkungsmodelle wie das Komfortzonenmodell, das metaphorische Modell, Aktions- und Reflexionswelle oder das FlowModell.
Erlebnispädagogik hat als Lernszenariotechnik zwar eine eigene Strukturlogik, dient jedoch ebenso als Methode im nicht genuin erlebnispädagogischen Kontext und entwickelt somit Reichweite, Relevanz und pädagogische Innovationskraft in Konfirmandenarbeit, Religionsunterricht und Gemeindepädagogik.</p>
<h3 id="aktuelle-fragestellungen-und-entwicklungsperspektiven-qualität-und-wirkung">Aktuelle Fragestellungen und Entwicklungsperspektiven: Qualität und Wirkung<a hidden class="anchor" aria-hidden="true" href="#aktuelle-fragestellungen-und-entwicklungsperspektiven-qualität-und-wirkung">#</a></h3>
<p>Als partizipatorisches Angebot setzt die Erlebnispädagogik auf das Prinzip der Freiwilligkeit und der selbstbestimmten Teilnahme. Unter dem Motto challenge by choice hat eine subjektorientierte Erlebnispädagogik stets zu gewährleisten, dass die Teilnehmenden den Grad ihrer Beteiligung und Verantwortungsübernahme selbst bestimmen können. Reiserechtliche Bestimmungen, Versicherung und Haftung sind daher aktuell handlungsleitendend bei der Erstellung von erlebnispädagogischen Angeboten und Konzeptionen. Hier gab es in den letzten Jahren zahlreiche Entwicklungen, die eine Qualitätssicherung der Mitarbeitendenbildung in Pädagogik und Sicherheitstechnik hinsichtlich geltender Kriterien voraussetzen und notwendig machen. Entsprechende Zusatzqualifikationen sollten daher immer mindestens durch die jeweiligen Fachsportverbände zertifizierbar sein. Mit dem <a href="https://www.bundesverband-erlebnispaedagogik.de/qualitaet/beq-qualitaetssiegel.html">Gütesiegel „Qualität erlebnispädagogischer Programme und Angebote Mit Sicherheit pädagogisch!“ (beQ)</a> hat der Bundesverband Individual- und Erlebnispädagogik e.V. (BE) ein Zertifizierungsverfahren entwickelt, das ein bundeseinheitliches Qualitätsmanagement fördern will.</p>
<p>Die Wirkungsforschung erlebnispädagogischer Lernarrangements untersucht deren Substanz, Akzeptanz und Nachhaltigkeit hinsichtlich ihrer Qualität von Konzept, Struktur, Prozess und Ergebnis. Studien konnten bislang Indizien für Impulse zur Kompetenzentwicklung nachweisen, die wissenschaftliche Evaluation gestaltet sich jedoch genauso komplex wie das Spektrum der erlebnispädagogischen Handlungsfelder und so fehlt es bislang an validen Studien, die das Wirkungspotenzial eindeutig beschreiben können.</p>
<p>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.</p>
<h3 id="literatur">Literatur<a hidden class="anchor" aria-hidden="true" href="#literatur">#</a></h3>
<p><img loading="lazy" src="http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&amp;ASIN=3866870493&amp;Format=_SL160_&amp;ID=AsinImage&amp;MarketPlace=DE&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=httpwwwjoergl-21" alt="" />
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.
<img loading="lazy" src="http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&amp;ASIN=3497022934&amp;Format=_SL160_&amp;ID=AsinImage&amp;MarketPlace=DE&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=httpwwwjoergl-21" alt="" />
Heckmair, Bernd/Michl, Werner (2008): Erleben und Lernen. Einführung in die Erlebnispädagogik. München: Reinhardt.
<img loading="lazy" src="http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&amp;ASIN=3940562866&amp;Format=_SL160_&amp;ID=AsinImage&amp;MarketPlace=DE&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=httpwwwjoergl-21" alt="" />
Reiners, Annette (2007): Praktische Erlebnispädagogik. Augsburg: ZIEL.
<img loading="lazy" src="http://ws.assoc-amazon.de/widgets/q?_encoding=UTF8&amp;ASIN=3936369348&amp;Format=_SL160_&amp;ID=AsinImage&amp;MarketPlace=DE&amp;ServiceVersion=20070822&amp;WS=1&amp;tag=httpwwwjoergl-21" alt="" />
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.</p>
<h4 id="links">Links<a hidden class="anchor" aria-hidden="true" href="#links">#</a></h4>
<ul>
<li>Bundesverband Individual- und Erlebnispädagogik e.V. (BE): <a href="https://www.bundesverband-erlebnispaedagogik.de/">https://www.bundesverband-erlebnispaedagogik.de/</a></li>
<li>Fachportal für Erlebnispädagogik im christlichen Kontext des Evangelischen Jugendwerks in Württemberg: <a href="https://www.ejwue.de/arbeitsbereiche/erlebnispaedagogik/">https://www.ejwue.de/arbeitsbereiche/erlebnispaedagogik/</a></li>
<li>Informationsdienst Erlebnispädagogik mit Literaturliste: <a href="http://erlebnispaedagogik.de/">http://erlebnispaedagogik.de/</a></li>
<li>Bundesweite Klassenfahrten, Aus- und Weiterbildungsangebote der Gesellschaft zur Förderung der Erlebnispädagogik (GFE): <a href="https://www.erlebnistage.de/">https://www.erlebnistage.de/</a></li>
</ul>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

View File

@ -0,0 +1,247 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Telegram Bot für Octopi | Jörg Lohrer</title>
<meta name="keywords" content="Telegram, Octopi, Raspberry, 3DDruck">
<meta name="description" content="Schnittstelle zwischen Telegram und OctoPrint">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2017/10/23/telegram-octopi.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2017/10/23/telegram-octopi.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Telegram Bot für Octopi
</h1>
<div class="post-description">
Schnittstelle zwischen Telegram und OctoPrint
</div>
<div class="post-meta"><span title='2017-10-23 00:00:00 +0000 UTC'>Oktober 23, 2017</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2017/10/23/telegram-octopi.html/octopi1.png" alt="">
</figure>
<div class="post-content"><p>Das <a href="http://plugins.octoprint.org/plugins/telegram/">OctoPrint-Telegram-Plugin</a> schafft eine Schnittstelle zwischen Telegram und OctoPrint.
Hier die Anleitung auf Englisch: <a href="https://github.com/fabianonline/OctoPrint-Telegram/blob/stable/README.md">https://github.com/fabianonline/OctoPrint-Telegram/blob/stable/README.md</a></p>
<p>Das dauert eine Weile:
<img loading="lazy" src="octopi1.png" alt="" />
</p>
<p>Token eingeben:
<img loading="lazy" src="octopi2.png" alt="" />
</p>
<p>Heisst aber nicht, dass jetzt alles gleich klappt:
<img loading="lazy" src="octopi3.png" alt="" />
</p>
<p>Es müssen dem Benutzer noch die Rechte “Command” und “Notify” gegeben werden:
<img loading="lazy" src="octopi4.png" alt="" />
</p>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@ -0,0 +1,261 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Lutherkürbis - Reformation an Halloween | Jörg Lohrer</title>
<meta name="keywords" content="Lutherrose, Reformation, Halloween, Luther">
<meta name="description" content="Schablone und Bastelanleitung für einen Kürbis zur Reformation">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2017/10/31/lutherkuerbis.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2017/10/31/lutherkuerbis.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Lutherkürbis - Reformation an Halloween
</h1>
<div class="post-description">
Schablone und Bastelanleitung für einen Kürbis zur Reformation
</div>
<div class="post-meta"><span title='2017-10-31 00:00:00 +0000 UTC'>Oktober 31, 2017</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2017/10/31/lutherkuerbis.html/kuerbis-titelbild.jpg" alt="">
</figure>
<div class="post-content"><h1 id="lutherkürbis---reformation-an-halloween">Lutherkürbis - Reformation an Halloween<a hidden class="anchor" aria-hidden="true" href="#lutherkürbis---reformation-an-halloween">#</a></h1>
<p>Das Symbol der Lutherrose ist auch heute noch in vielen Wappen enthalten und wird als Dekoelement genutzt (<a href="https://www.t-online.de/leben/familie/id_65982142/lutherrose-entstehung-und-bedeutung.html">Quelle: epd/imago</a>)</p>
<p>Aus einer <a href="https://duckduckgo.com/?q=lutherrose&amp;t=h_&amp;iax=images&amp;ia=images">Fotovorlage der Lutherrose</a> wird mit einem <a href="https://image.online-convert.com/convert-to-svg">Online-Konverter eine Schablone als verlustfrei skalierbare Vektorgrafik</a> erzeugt:</p>
<p><a href="https://material.rpi-virtuell.de/wp-content/uploads/2018/10/Lutherrose.pdf"><img loading="lazy" src="lutherrose.png" alt="" />
</a>
<a href="https://material.rpi-virtuell.de/wp-content/uploads/2018/10/Lutherrose.pdf">Lutherrose PDF-Vorlage</a></p>
<h2 id="bastel-anleitung">Bastel-Anleitung<a hidden class="anchor" aria-hidden="true" href="#bastel-anleitung">#</a></h2>
<p>Einen Kürbis aufschneiden:
<img loading="lazy" src="kuerbis-aufschneiden.jpg" alt="" />
</p>
<p>entkernen und aushöhlen:
<img loading="lazy" src="kuerbis-entkernen.jpg" alt="" />
</p>
<p>Schablone aufbringen:
<img loading="lazy" src="schablone-aufbringen.jpg" alt="" />
</p>
<p>Ausschneiden:
<img loading="lazy" src="kuerbis-ausschneiden.jpg" alt="" />
</p>
<p>Mit Kerze oder elektrischem Licht ausstatten:
<img loading="lazy" src="kuerbis-titelbild.jpg" alt="" />
</p>
<p>Fertig!</p>
<p>Diese Idee inklusive der Schablone steht unter <a href="https://creativecommons.org/publicdomain/zero/1.0/deed.de">CC0-Lizenz</a>. Du darfst das Werk kopieren, verändern, verbreiten und aufführen, sogar zu kommerziellen Zwecken, ohne um weitere Erlaubnis bitten zu müssen.</p>
<h4 id="weitere-quellen">Weitere Quellen<a hidden class="anchor" aria-hidden="true" href="#weitere-quellen">#</a></h4>
<ul>
<li>How to Make a Paper Cut-Out Luther Rose <a href="https://www.youtube.com/watch?v=b5FCaNZPU98">YouTube</a> | <a href="http://www.kellyklages.com/lutherrose.pdf">PDF</a></li>
</ul>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Pflanzenschild mit QR-Code | Jörg Lohrer</title>
<meta name="keywords" content="QR-Code, 3DDruck">
<meta name="description" content="Mit einem QR-Code-Generator lassen sich verlustfrei skalierbare Vektorgrafiken (SVG) erstellen">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2019/03/26/pflanzenschild-qr-code.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2019/03/26/pflanzenschild-qr-code.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Pflanzenschild mit QR-Code
</h1>
<div class="post-description">
Mit einem QR-Code-Generator lassen sich verlustfrei skalierbare Vektorgrafiken (SVG) erstellen
</div>
<div class="post-meta"><span title='2019-03-26 00:00:00 +0000 UTC'>März 26, 2019</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2019/03/26/pflanzenschild-qr-code.html/cura-plugin-change-filment-at-z.png" alt="">
</figure>
<div class="post-content"><h1 id="pflanzenschild-mit-qr-code">Pflanzenschild mit QR-Code<a hidden class="anchor" aria-hidden="true" href="#pflanzenschild-mit-qr-code">#</a></h1>
<p>Mit einem QR-Code-Generator lassen sich verlustfrei skalierbare Vektorgrafiken (SVG) erstellen:
<a href="https://keremerkan.net/qr-code-and-2d-code-generator/"></a></p>
<p>Dann noch mit Thingiverse ein Dreieck designen (80x40x1.4mm) und den QR-Code obendrauf:
<a href="https://www.tinkercad.com/embed/dcnCyqLFX1P?editbtn=1"></a></p>
<p>Mit dem Cura-Plugin <a href="https://www.thingiverse.com/thing:2077884/#files">Change Filament at Z</a> den 3D-Druck im Layer unterbrechen, wo ein Filament(Farb)-Wechsel stattfinden soll:
<img loading="lazy" src="cura-plugin-change-filment-at-z.png" alt="" />
</p>
<p>Et voilà:
<a href="https://twitter.com/joerglohrer/status/1110580168035176449"></a>
<img loading="lazy" src="qr-code-pflanzenschild.jpg" alt="" />
</p>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

View File

@ -0,0 +1,471 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>VR - Virtual Reality | Jörg Lohrer</title>
<meta name="keywords" content="Immersion, Präsenz, Virtuelle Realität, Sidequest, Oculus, Meta">
<meta name="description" content="Tutorials der Oculus Quest 2">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2021/08/15/virtual-reality.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2021/08/15/virtual-reality.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
VR - Virtual Reality
</h1>
<div class="post-description">
Tutorials der Oculus Quest 2
</div>
<div class="post-meta"><span title='2021-08-15 00:00:00 +0000 UTC'>August 15, 2021</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2021/08/15/virtual-reality.html/04-aframe.jpg" alt="">
</figure>
<div class="post-content"><h1 id="vr---virtual-reality">VR - Virtual Reality<a hidden class="anchor" aria-hidden="true" href="#vr---virtual-reality">#</a></h1>
<h1 id="ausschreibung-torsten--jörg">Ausschreibung Torsten &amp; Jörg<a hidden class="anchor" aria-hidden="true" href="#ausschreibung-torsten--jörg">#</a></h1>
<p>Ausgehend vom Lernbaustein <a href="https://relilab.org/vr/">https://relilab.org/vr/</a> beschäftigen wir uns in diesem Workshop mit Immersion und praktischen Beispielen im Themenbereich VR/AR. Neben der Vermittlung von theoretischen Grundlagen besprechen wir die Einsatzmöglichkeit im Religionsunterricht am Beispiel einer Reihe zu Schöpfungsmythen. Darüber hinnaus bieten wir die Gelegenheit zu Erfahrungsaustausch, Diskussion und exemplarischen Erlebnismöglichkeiten in der virtuellen Realität.</p>
<h1 id="theorie">Theorie<a hidden class="anchor" aria-hidden="true" href="#theorie">#</a></h1>
<h2 id="immersion--präsenz">Immersion &amp; Präsenz<a hidden class="anchor" aria-hidden="true" href="#immersion--präsenz">#</a></h2>
<p><a href="https://www.immersivelearning.institute/">https://www.immersivelearning.institute/</a>
<a href="https://omnia360.de/blog/was-ist-immersion/">https://omnia360.de/blog/was-ist-immersion/</a>
<a href="https://en.wikipedia.org/wiki/Immersive_learning">https://en.wikipedia.org/wiki/Immersive_learning</a>
<img loading="lazy" src="01-immersion-wikipedia.jpg" alt="" />
<a href="https://www.sciencedirect.com/science/article/pii/S0360131519303276">https://www.sciencedirect.com/science/article/pii/S0360131519303276</a></p>
<h2 id="forschung">Forschung<a hidden class="anchor" aria-hidden="true" href="#forschung">#</a></h2>
<p>A Public Database of 360 Videos with Corresponding Ratings of Arousal and Valence
<a href="https://vhil.stanford.edu/360-video-database/">https://vhil.stanford.edu/360-video-database/</a></p>
<p><a href="https://www.researchgate.net/publication/335733502_Getting_your_game_on_Using_virtual_reality_to_improve_real_table_tennis_skills">https://www.researchgate.net/publication/335733502_Getting_your_game_on_Using_virtual_reality_to_improve_real_table_tennis_skills</a></p>
<blockquote>
<p>&ldquo;Diese Studie ergänzt die spärliche, aber wachsende Literatur, indem sie den Transfer von Fertigkeiten aus der realen Welt durch Virtual Reality in einer sportlichen Aufgabe demonstriert.&rdquo;</p>
</blockquote>
<p><a href="https://mixed.de/vr-und-philosophie-youtuber-zeigt-ungewoehnliche-vr-apps/amp/">https://mixed.de/vr-und-philosophie-youtuber-zeigt-ungewoehnliche-vr-apps/amp/</a>
Der norwegische Forscher Joakim Vindenes unterhält einen Blog, Podcast und Youtube-Kanal, in denen er außergewöhnliche VR-Apps vorstellt und sich mit der philosophischen Dimension des Mediums auseinandersetzt.</p>
<p><a href="https://www.youtube.com/watch?v=VY5HaEhUm2Q">https://www.youtube.com/watch?v=VY5HaEhUm2Q</a>
A look at what it would be like to plug into an infinite VR experience machine.</p>
<h2 id="religionen">Religionen<a hidden class="anchor" aria-hidden="true" href="#religionen">#</a></h2>
<p><a href="https://www.matrise.no/2019/06/hinduism-and-virtual-reality/">https://www.matrise.no/2019/06/hinduism-and-virtual-reality/</a></p>
<p><a href="https://www.matrise.no/2020/01/the-existential-problem-of-vr-existentialism/">https://www.matrise.no/2020/01/the-existential-problem-of-vr-existentialism/</a></p>
<h2 id="empathie">Empathie<a hidden class="anchor" aria-hidden="true" href="#empathie">#</a></h2>
<p><a href="https://www.youtube.com/watch?v=L6m79wqNiMA&amp;feature=youtu.be&amp;t=150">https://www.youtube.com/watch?v=L6m79wqNiMA&amp;feature=youtu.be&amp;t=150</a></p>
<blockquote>
<p>&ldquo;VR is a storytelling vehicle, but it&rsquo;s an empathy vehicle.&rdquo;</p>
</blockquote>
<h2 id="therapie">Therapie<a hidden class="anchor" aria-hidden="true" href="#therapie">#</a></h2>
<p>Feeling Good during the COVID19 Epidemic
Virtual Reality Can Help Us to Overcome the Psychological Burden of Coronavirus - <a href="https://www.covidfeelgood.com/home">https://www.covidfeelgood.com/home</a></p>
<p>Die Verkörperung von Mitgefühl in der virtuellen Realität und ihre Auswirkungen auf Patienten mit Depressionen
<a href="https://scitec-media.ch/2016/03/04/game-technologien-helfen-gegen-depressionen/">https://scitec-media.ch/2016/03/04/game-technologien-helfen-gegen-depressionen/</a>
<a href="https://www.youtube.com/watch?v=GwxJVCESc-E">https://www.youtube.com/watch?v=GwxJVCESc-E</a>
<a href="https://www.cambridge.org/core/journals/bjpsych-open/article/embodying-selfcompassion-within-virtual-reality-and-its-effects-on-patients-with-depression/1A1217651159D085145A7999CFFFF772">https://www.cambridge.org/core/journals/bjpsych-open/article/embodying-selfcompassion-within-virtual-reality-and-its-effects-on-patients-with-depression/</a></p>
<h2 id="theater">Theater<a hidden class="anchor" aria-hidden="true" href="#theater">#</a></h2>
<p><a href="https://www.brendanabradley.com/futurestages">https://www.brendanabradley.com/futurestages</a></p>
<h2 id="körperlichkeit">Körperlichkeit<a hidden class="anchor" aria-hidden="true" href="#körperlichkeit">#</a></h2>
<p><a href="https://www.matrise.no/2018/07/virtual-embodiment/">https://www.matrise.no/2018/07/virtual-embodiment/</a>
<a href="https://www.matrise.no/2020/10/virtual-reality-depersonalization-derealization/">https://www.matrise.no/2020/10/virtual-reality-depersonalization-derealization/</a></p>
<h1 id="praxis">Praxis<a hidden class="anchor" aria-hidden="true" href="#praxis">#</a></h1>
<h2 id="padlets">Padlets<a hidden class="anchor" aria-hidden="true" href="#padlets">#</a></h2>
<ul>
<li>[https://padlet.com/petiteprof79/xrinfo(https://padlet.com/petiteprof79/xrinfo)] XR-Info - Stephanie Wössner | CC BY-SA @petiteprof79</li>
<li><a href="https://padlet.com/strsa/ar">https://padlet.com/strsa/ar</a> Augmented Reality (AR) &amp; 360°, (VR) | Gestartet von Tobias Erles (@Mr_Airless) und Sabine Strauss (@Sallythechin) beim Barcamp #wildcampen18, erweitert bei #wildcampen19 zusammen mit Jan Hartwig (@hartifical)</li>
</ul>
<h2 id="artikel">Artikel<a hidden class="anchor" aria-hidden="true" href="#artikel">#</a></h2>
<ul>
<li><a href="https://www.bpb.de/lernen/digitale-bildung/werkstatt/298516/virtual-und-augmented-reality-im-klassenraum-ein-ueberblick-bildungsrelevanter-angebote">Virtual und Augmented Reality im Klassenraum? Ein Überblick bildungsrelevanter Angebote - Steffen Jauch am 16.10.2029 bei bpb</a></li>
</ul>
<h2 id="unterricht">Unterricht<a hidden class="anchor" aria-hidden="true" href="#unterricht">#</a></h2>
<ul>
<li>Eine <a href="https://www.lmz-bw.de/medien-und-bildung/medienwissen/virtual-und-augmented-reality/virtual-reality-unterrichtsbeispiele/">Auswahl an Unterrichtsbeispielen mit Virtual Reality Szenarien</a> von Stephanie Wössner <a href="https://twitter.com/petiteprof79">@petiteprof79</a></li>
</ul>
<h2 id="communities">Communities<a hidden class="anchor" aria-hidden="true" href="#communities">#</a></h2>
<p><a href="https://educatorsinvr.com/">https://educatorsinvr.com/</a></p>
<h1 id="religiöse-erfahrungsräume">Religiöse Erfahrungsräume<a hidden class="anchor" aria-hidden="true" href="#religiöse-erfahrungsräume">#</a></h1>
<h2 id="im-oculus-store">im Oculus Store<a hidden class="anchor" aria-hidden="true" href="#im-oculus-store">#</a></h2>
<h3 id="wander">Wander<a hidden class="anchor" aria-hidden="true" href="#wander">#</a></h3>
<p><a href="https://www.oculus.com/experiences/quest/2078376005587859/">https://www.oculus.com/experiences/quest/2078376005587859/</a></p>
<h2 id="filme">Filme<a hidden class="anchor" aria-hidden="true" href="#filme">#</a></h2>
<p>[https://mixed.de/beruehrender-oculus-film-zeigt-worauf-es-im-leben-ankommt/(https://mixed.de/beruehrender-oculus-film-zeigt-worauf-es-im-leben-ankommt/)]</p>
<h2 id="in-sidequest">In Sidequest<a hidden class="anchor" aria-hidden="true" href="#in-sidequest">#</a></h2>
<h3 id="vanishing-grace">Vanishing Grace<a hidden class="anchor" aria-hidden="true" href="#vanishing-grace">#</a></h3>
<p>[https://sidequestvr.com/app/772/vanishing-grace-pre-alpha-demo(https://sidequestvr.com/app/772/vanishing-grace-pre-alpha-demo)]</p>
<blockquote>
<p>Vanishing Grace ist eine emotionale Reise über die Opfer, die wir bringen müssen, um unseren Platz in der Welt einzunehmen.</p>
</blockquote>
<h3 id="cubism">Cubism<a hidden class="anchor" aria-hidden="true" href="#cubism">#</a></h3>
<p>[https://sidequestvr.com/app/403/cubism-demo(https://sidequestvr.com/app/403/cubism-demo)]</p>
<h3 id="deism">Deism<a hidden class="anchor" aria-hidden="true" href="#deism">#</a></h3>
<blockquote>
<p>The virtual reality god simulator
[https://sidequestvr.com/app/85/deisim(https://sidequestvr.com/app/85/deisim)]</p>
</blockquote>
<h3 id="liminal">Liminal<a hidden class="anchor" aria-hidden="true" href="#liminal">#</a></h3>
<p><a href="https://sidequestvr.com/app/1042/liminal">https://sidequestvr.com/app/1042/liminal</a>
Wählen Sie aus, wie Sie sich fühlen und was Sie leisten wollen: Ruhe, Energie, Schmerzlinderung und Ehrfurcht.</p>
<h1 id="3d-modelle">3D-Modelle<a hidden class="anchor" aria-hidden="true" href="#3d-modelle">#</a></h1>
<p><a href="https://sketchfab.com/3d-models/abandoned-warehouse-interior-scene-1d5285f2e0fd4211a27c8042496d5959#">https://sketchfab.com/3d-models/abandoned-warehouse-interior-scene-1d5285f2e0fd4211a27c8042496d5959#</a></p>
<p><img loading="lazy" src="02-mittelalterliche-kirche.jpg" alt="" />
<a href="https://creativecommons.org/licenses/by-nc/4.0/">CC BY-NC</a> Medieval Church, Calatrava la Nueva, Spain
Processed in Reality Capture from 76 Faro laser scans and 4100 photographs
<a href="https://sketchfab.com/3d-models/medieval-church-calatrava-la-nueva-spain-171a047c08bc4dd588cca5ac744e8065">https://sketchfab.com/3d-models/medieval-church-calatrava-la-nueva-spain-171a047c08bc4dd588cca5ac744e8065</a></p>
<h2 id="avatare">Avatare<a hidden class="anchor" aria-hidden="true" href="#avatare">#</a></h2>
<h2 id="erfahrungswelten">Erfahrungswelten:<a hidden class="anchor" aria-hidden="true" href="#erfahrungswelten">#</a></h2>
<h3 id="360-grad-filme">360 Grad Filme<a hidden class="anchor" aria-hidden="true" href="#360-grad-filme">#</a></h3>
<ul>
<li><a href="https://www.youtube.com/watch?v=dMZfIUr0gEs">Kölner Dom in 360°: Privatkonzert | WDR</a>
Der Domchor gibt ein nächtliches Konzert und der einzige Zuschauer bist du. Unter Leitung des Domkapellmeisters Professor Eberhard Metternich singt der Chor das Ave Maria von Franz Biebl in einer eher ungewöhnlichen Aufstellung. Und du stehst mittendrin. <a href="https://dom360.wdr.de/privatkonzert-bei-nacht/">https://dom360.wdr.de/privatkonzert-bei-nacht/</a>]</li>
<li><a href="https://www.youtube.com/watch?v=QwC5d75iTcA">INSIDE AUSCHWITZ - Das ehemalige Konzentrationslager in 360° | WDR
</a></li>
</ul>
<h3 id="anne-frank-vr">Anne Frank VR<a hidden class="anchor" aria-hidden="true" href="#anne-frank-vr">#</a></h3>
<p><a href="https://www.annefrank.org/de/uber-uns/was-wir-tun/unsere-publikationen/das-anne-frank-haus-virtual-reality/">https://www.annefrank.org/</a></p>
<h3 id="within">Within<a hidden class="anchor" aria-hidden="true" href="#within">#</a></h3>
<p>[https://www.with.in/watch/the-ellen-fund-presents-gorillas-in-vr(https://www.with.in/watch/the-ellen-fund-presents-gorillas-in-vr)]
[https://account.altvr.com/channels/VRChurch(https://account.altvr.com/channels/VRChurch)]</p>
<h1 id="programmierung">Programmierung<a hidden class="anchor" aria-hidden="true" href="#programmierung">#</a></h1>
<ul>
<li><a href="https://circuitstream.com/blog/programming-development-guides/">Learn VR Development: Tips, tricks, and guides to develop VR and AR applications</a></li>
<li><a href="https://www.reddit.com/r/learnVRdev/">Reddit - Learn Virtual Reality Development</a></li>
</ul>
<h2 id="mozilla-hubs">Mozilla Hubs<a hidden class="anchor" aria-hidden="true" href="#mozilla-hubs">#</a></h2>
<h3 id="dokumentation-httpshubsmozillacomdocswelcomehtmlhttpshubsmozillacomdocswelcomehtml">Dokumentation [https://hubs.mozilla.com/docs/welcome.html(https://hubs.mozilla.com/docs/welcome.html)]<a hidden class="anchor" aria-hidden="true" href="#dokumentation-httpshubsmozillacomdocswelcomehtmlhttpshubsmozillacomdocswelcomehtml">#</a></h3>
<h3 id="vorlagen">Vorlagen<a hidden class="anchor" aria-hidden="true" href="#vorlagen">#</a></h3>
<p><a href="https://v3l.de/relilabvr">https://v3l.de/relilabvr</a> (relilab-VR-Hub mit Videos, Bildern, &hellip;)</p>
<p>[https://hubs.mozilla.com/Av38AUU/detailed-tinted-room(https://hubs.mozilla.com/Av38AUU/detailed-tinted-room)]
[https://hubs.mozilla.com/scenes/juMbRem/dramainvrhome(https://hubs.mozilla.com/scenes/juMbRem/dramainvrhome)]</p>
<h3 id="avatare-erstellen">Avatare erstellen<a hidden class="anchor" aria-hidden="true" href="#avatare-erstellen">#</a></h3>
<h3 id="mozilla-hubs---glb">Mozilla Hubs - GLB<a hidden class="anchor" aria-hidden="true" href="#mozilla-hubs---glb">#</a></h3>
<h4 id="online-dienst---readyplayer">Online Dienst - Readyplayer<a hidden class="anchor" aria-hidden="true" href="#online-dienst---readyplayer">#</a></h4>
<p><a href="https://twitter.com/joerglohrer/status/1388023150626099201">https://twitter.com/joerglohrer/status/1388023150626099201</a>
<img loading="lazy" src="03-avatare-erstellen.jpg" alt="" />
</p>
<p>[https://readyplayer.me/(https://readyplayer.me/)]</p>
<p>Beispiel: (<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA - Wolfprint 3D</a>)
<a href="https://d1a370nemizbjq.cloudfront.net/10b05c49-593a-4616-82e9-adcea09aa66c.glb">https://d1a370nemizbjq.cloudfront.net/10b05c49-593a-4616-82e9-adcea09aa66c.glb</a></p>
<p>Hubs by Mozilla</p>
<ul>
<li><a href="https://hubs.mozilla.com/docs/intro-avatars.html">Creating Custom Avatars</a></li>
<li><a href="https://hubs.mozilla.com/docs/creators-advanced-avatar-customization.html">Advanced Avatar Customization</a></li>
</ul>
<p><a href="http://tryquilt.io/">http://tryquilt.io/</a>]</p>
<p><a href="https://github.com/MozillaReality/hubs-avatar-pipelines/tree/master/Blender/AvatarBot">Blender files for AvatarBot</a>
<a href="https://youtube.com/playlist?list=PLCxaiaRxTL6-3pxUsfSa7lzKruGu5UEat">Creating an Avatar using Hubs components in Blender</a></p>
<h4 id="online-editoren">Online Editoren<a hidden class="anchor" aria-hidden="true" href="#online-editoren">#</a></h4>
<p>[https://modelviewer.dev/editor/(https://modelviewer.dev/editor/)]</p>
<h4 id="software">Software<a hidden class="anchor" aria-hidden="true" href="#software">#</a></h4>
<h5 id="makehuman">MakeHuman<a hidden class="anchor" aria-hidden="true" href="#makehuman">#</a></h5>
<p><a href="http://makehumancommunity.org/">http://makehumancommunity.org/</a></p>
<p><a href="https://www.youtube.com/watch?v=kQjp5DYsR_c&amp;t=86s">https://www.youtube.com/watch?v=kQjp5DYsR_c&amp;t=86s</a></p>
<h3 id="tutorials-altspace--blender--unity">Tutorials Altspace / Blender / Unity<a hidden class="anchor" aria-hidden="true" href="#tutorials-altspace--blender--unity">#</a></h3>
<p><a href="http://edvschwab.de/Altspace/Kurz%20Doku%20Blender.pdf">http://edvschwab.de/Altspace/Kurz%20Doku%20Blender.pdf</a>
[http://edvschwab.de/Altspace/Kurz%20Doku%20Unity.pdf(http://edvschwab.de/Altspace/Kurz%20Doku%20Unity.pdf)]</p>
<h2 id="sidequest">Sidequest<a hidden class="anchor" aria-hidden="true" href="#sidequest">#</a></h2>
<p><a href="https://sidequestvr.com/">https://sidequestvr.com/</a>
Installation:
<a href="https://sidequestvr.com/setup-howto">https://sidequestvr.com/setup-howto</a></p>
<p><a href="https://www.google.com/amp/s/www.androidcentral.com/how-put-custom-songs-beat-saber-oculus-quest%3famp">How to put custom songs onto Beat Saber on Oculus Quest</a></p>
<h2 id="aframe">Aframe<a hidden class="anchor" aria-hidden="true" href="#aframe">#</a></h2>
<p><img loading="lazy" src="04-aframe.jpg" alt="" />
<a href="https://codepen.io/joerglohrer/full/dyXQqWG">https://codepen.io/joerglohrer/full/dyXQqWG</a>
<a href="https://aframe.io/">https://aframe.io/</a>
<a href="https://www.codecademy.com/learn/learn-a-frame">https://www.codecademy.com/learn/learn-a-frame</a>
VR development within VR with Oculus Quest + Firefox Reality + Glitch +
<a href="https://rocketvirtual.com/index.html">https://rocketvirtual.com/index.html</a>
<a href="https://michael-mcanally.medium.com/where-to-begin-with-vr-in-a-browser-d818f713a8a8">Where to begin with VR in a browser?</a>
<a href="https://www.reddit.com/r/WebVR/comments/equgdt/vr_development_within_vr_with_oculus_quest/">https://www.reddit.com/r/WebVR/comments/equgdt/vr_development_within_vr_with_oculus_quest/</a></p>
<p><a href="https://aframe-model-viewer.glitch.me/">https://aframe-model-viewer.glitch.me/</a></p>
<p><a href="https://klausw.github.io/a-frame-car-sample/index.html">https://klausw.github.io/a-frame-car-sample/index.html</a></p>
<h2 id="threejs">ThreeJS<a hidden class="anchor" aria-hidden="true" href="#threejs">#</a></h2>
<p><a href="https://threejs.org/">https://threejs.org/</a>
<a href="https://www.jesuisundev.com/en/understand-threejs/">https://www.jesuisundev.com/en/understand-threejs/</a></p>
<h2 id="tour-creator-google"><del>Tour Creator Google</del><a hidden class="anchor" aria-hidden="true" href="#tour-creator-google">#</a></h2>
<p>&ldquo;Starting June 30, 2021, the Google Expeditions and Tour Creator <a href="https://support.google.com/tourcreator/?hl=en">will no longer be accessible</a>.&rdquo;</p>
<h2 id="unity">Unity<a hidden class="anchor" aria-hidden="true" href="#unity">#</a></h2>
<p><a href="https://unity.com/de/learn/get-started">https://unity.com/de/learn/get-started</a></p>
<h1 id="optik">Optik<a hidden class="anchor" aria-hidden="true" href="#optik">#</a></h1>
<h2 id="pupillendistanz-pd-messen---ipd-interpupillary-distance">Pupillendistanz (PD) messen - IPD (interpupillary distance)<a hidden class="anchor" aria-hidden="true" href="#pupillendistanz-pd-messen---ipd-interpupillary-distance">#</a></h2>
<p><a href="https://apps.apple.com/de/app/eyemeasure/id1417435049">https://apps.apple.com/de/app/eyemeasure/id1417435049</a>
Die kostenfreie App misst den Augenabstand auf 0,5 mm genau ab iPhoneX oder iPadPro
<img loading="lazy" src="05-pupillendistanz.jpg" alt="" />
Oder einfach mit Lineal:
<a href="https://imgur.com/a/gyYKB">https://imgur.com/a/gyYKB</a></p>
<h1 id="oculus-quest">Oculus Quest<a hidden class="anchor" aria-hidden="true" href="#oculus-quest">#</a></h1>
<h2 id="tutorials">Tutorials<a hidden class="anchor" aria-hidden="true" href="#tutorials">#</a></h2>
<h3 id="deutsch">Deutsch<a hidden class="anchor" aria-hidden="true" href="#deutsch">#</a></h3>
<p><a href="https://twitter.com/StubeDie">https://twitter.com/StubeDie</a></p>
<h3 id="englisch">Englisch<a hidden class="anchor" aria-hidden="true" href="#englisch">#</a></h3>
<h4 id="foren">Foren<a hidden class="anchor" aria-hidden="true" href="#foren">#</a></h4>
<p><a href="https://www.reddit.com/r/OculusQuest/">https://www.reddit.com/r/OculusQuest/</a>
<a href="https://www.reddit.com/r/OculusQuest2/">https://www.reddit.com/r/OculusQuest2/</a></p>
<h2 id="streaming--playthrough--livecasting--obs">Streaming / Playthrough / Livecasting / OBS<a hidden class="anchor" aria-hidden="true" href="#streaming--playthrough--livecasting--obs">#</a></h2>
<p><a href="https://bsaber.com/queststreamingguide/">Oculus Quest Recording / Live Streaming Guide</a>
<a href="https://restream.io/blog/ultimate-guide-to-twitch/">How to stream on Twitch: the ultimate guide</a></p>
<h2 id="file-transfer">File Transfer<a hidden class="anchor" aria-hidden="true" href="#file-transfer">#</a></h2>
<p>3 different ways
<a href="https://www.youtube.com/watch?v=APbbuJF4Ma4">https://www.youtube.com/watch?v=APbbuJF4Ma4</a>
<a href="https://www.android.com/filetransfer/">https://www.android.com/filetransfer/</a>
<a href="https://support.oculus.com/2255729571307786/">Wie übertrage ich Bilder oder Videos von meinem Computer auf mein Oculus Quest 2 oder Quest?</a></p>
<p>Facebook video downloader here - <a href="https://fbdown.net/">https://fbdown.net/</a></p>
<h2 id="brillenträger">Brillenträger<a hidden class="anchor" aria-hidden="true" href="#brillenträger">#</a></h2>
<p>Sehstärke-Linsen Einsätze
<a href="https://mixed.de/vr-optiker-sehstaerke-linsen-test/">https://mixed.de/vr-optiker-sehstaerke-linsen-test/</a></p>
<ul>
<li><a href="https://vroptiker.de/sehstaerke-linsen-einsaetze/oculus-quest2/">https://vroptiker.de/sehstaerke-linsen-einsaetze/oculus-quest2/</a></li>
<li><a href="https://widmovr.com/product/oculus-quest-2-prescription-lens-adapters/">https://widmovr.com/product/oculus-quest-2-prescription-lens-adapters/</a></li>
</ul>
<h2 id="not-to-do">Not to do<a hidden class="anchor" aria-hidden="true" href="#not-to-do">#</a></h2>
<h3 id="sonnenlicht-in-die-linse">Sonnenlicht in die Linse)<a hidden class="anchor" aria-hidden="true" href="#sonnenlicht-in-die-linse">#</a></h3>
<p>Sonnenlicht kann durch die Linsen gebündelt werden und das Display beschödigen.</p>
<h3 id="linsen-reinigen">Linsen reinigen<a hidden class="anchor" aria-hidden="true" href="#linsen-reinigen">#</a></h3>
<h3 id="grips---handhalterungen">Grips - Handhalterungen<a hidden class="anchor" aria-hidden="true" href="#grips---handhalterungen">#</a></h3>
<p><a href="https://www.youtube.com/watch?v=_52xWr_R4uM&amp;feature=youtu.be&amp;t=388">https://www.youtube.com/watch?v=_52xWr_R4uM&amp;feature=youtu.be&amp;t=388</a></p>
<h3 id="headstrap">Headstrap<a hidden class="anchor" aria-hidden="true" href="#headstrap">#</a></h3>
<h4 id="vive-deluxe-audio-strap">VIVE DELUXE AUDIO STRAP<a hidden class="anchor" aria-hidden="true" href="#vive-deluxe-audio-strap">#</a></h4>
<p><a href="https://www.vive.com/de/accessory/vive-deluxe-audio-strap/">https://www.vive.com/de/accessory/vive-deluxe-audio-strap/</a></p>
<p>FrankenQuest 2
<a href="https://uploadvr.com/frankenquest-2-quest-2/">https://uploadvr.com/frankenquest-2-quest-2/</a>
Adapter zum 3D-Druck:
<a href="https://www.thingiverse.com/thing:4622970">https://www.thingiverse.com/thing:4622970</a>
Variante mit Kabelklemme (oben oder unten):
<a href="https://www.thingiverse.com/thing:4628600/files">https://www.thingiverse.com/thing:4628600/files</a>
<img loading="lazy" src="06-vr-adapter-3ddruck.jpg" alt="" />
:::success
Druck skaliert auf 101%
:::
Install guide
Remove the Quest 2 straps following this guide: <a href="https://www.youtube.com/watch?v=PejiYjR7_44&amp;feature=youtu.be&amp;t=110">https://www.youtube.com/watch?v=PejiYjR7_44&amp;feature=youtu.be&amp;t=110</a>
Snap the DAS onto the adaptor (pushing the round front section in first like in the picture).
Snap the adaptor onto Quest 2.
Use the velcro loop to attach the head strap to the headset.
<img loading="lazy" src="07-vive-straps-3ddruck.jpg" alt="" />
</p>
<p>Oculus Quest 2 Comfort Head Strap Mods!</p>
<h4 id="3ddruck">3DDruck<a hidden class="anchor" aria-hidden="true" href="#3ddruck">#</a></h4>
<p>Oculus Quest 2 Elite Strap_V2_NAVIDA DESIGN
[https://www.thingiverse.com/thing:4630780/files(https://www.thingiverse.com/thing:4630780/files)]
Oculus Link Cable Clip for Deluxe Audio Strap &ldquo;DAS&rdquo;
<a href="https://www.thingiverse.com/thing:4666749">https://www.thingiverse.com/thing:4666749</a></p>
<p>Überblick:
<a href="https://www.youtube.com/watch?v=sUhMk19PVq4">https://www.youtube.com/watch?v=sUhMk19PVq4</a>
<a href="https://www.youtube.com/watch?v=CN9fYlUGVtk">https://www.youtube.com/watch?v=CN9fYlUGVtk</a></p>
<h3 id="lens-adaptor">Lens Adaptor<a hidden class="anchor" aria-hidden="true" href="#lens-adaptor">#</a></h3>
<h4 id="für-brillen">Für Brillen<a hidden class="anchor" aria-hidden="true" href="#für-brillen">#</a></h4>
<p>[https://www.thingiverse.com/thing:3653631(https://www.thingiverse.com/thing:3653631)]
Für Linsen
[https://www.thingiverse.com/thing:3642004(https://www.thingiverse.com/thing:3642004)]</p>
<p>Esimen Upgrade K3</p>
<h3 id="batterie">Batterie<a hidden class="anchor" aria-hidden="true" href="#batterie">#</a></h3>
<p><a href="https://www.androidcentral.com/best-oculus-quest-battery-pack">https://www.androidcentral.com/best-oculus-quest-battery-pack</a></p>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

View File

@ -0,0 +1,374 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>WordPress Werkstatt PHP | Jörg Lohrer</title>
<meta name="keywords" content="ACF, WordPress, Formulare, JSON, Plugin">
<meta name="description" content="Advanced Custom Fields und Formulareingaben">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2021/11/17/wordpress-werkstatt.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2021/11/17/wordpress-werkstatt.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
WordPress Werkstatt PHP
</h1>
<div class="post-description">
Advanced Custom Fields und Formulareingaben
</div>
<div class="post-meta"><span title='2021-11-17 00:00:00 +0000 UTC'>November 17, 2021</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2021/11/17/wordpress-werkstatt.html/04-termine-neu.png" alt="">
</figure>
<div class="post-content"><h1 id="wordpress-werkstatt-php">WordPress Werkstatt PHP<a hidden class="anchor" aria-hidden="true" href="#wordpress-werkstatt-php">#</a></h1>
<p>Zunächst wird auf relilab.org das kostenfreie <a href="https://de.wordpress.org/plugins/advanced-custom-fields/">Plugin ACF - Advanced Custom Fields</a> installiert und aktiviert.
Dies ermöglicht weitere individuelle Beitragsfelder für die Beiträge.
Nun kann manuell aktiviert oder eine Feldgruppe importiert werden - hier mittels <a href="#ACF-JSON-Export">dieser JSON-Datei</a>, die das abkürzt:
<img loading="lazy" src="h01-json-import.png" alt="" />
mit dem Ergebnis, dass unter allen WordPress-Beiträgen jetzt zwei Terminfelder erscheinen, die ausgefüllt werden können:
<img loading="lazy" src="02-terminfelder.png" alt="" />
Zudem gibt es neu eine Kategorie &ldquo;Termine&rdquo;, die aktiviert werden kann mit Unterkategorien, die später Übersichtsseiten ermöglichen:
<img loading="lazy" src="03-kategorien.png" alt="" />
Jetzt wird das <a href="https://github.com/rpi-virtuell/relilab-termine">Plugin relilab-termine</a> installiert und aktiviert
Nun kann mittels Shortcode <code>[relilab_termine]</code> eine Terminübersicht als WordPress-Block erzeugt werden:
<img loading="lazy" src="04-termine-neu.png" alt="" />
</p>
<h2 id="json">JSON<a hidden class="anchor" aria-hidden="true" href="#json">#</a></h2>
<h4 id="acf-json-export">ACF-JSON-Export:<a hidden class="anchor" aria-hidden="true" href="#acf-json-export">#</a></h4>
<pre tabindex="0"><code class="language-json=" data-lang="json=">[
{
&#34;key&#34;: &#34;group_6193936e4f12c&#34;,
&#34;title&#34;: &#34;Termin&#34;,
&#34;fields&#34;: [
{
&#34;key&#34;: &#34;field_619393f8e62e0&#34;,
&#34;label&#34;: &#34;Startet am&#34;,
&#34;name&#34;: &#34;relilab_startdate&#34;,
&#34;type&#34;: &#34;date_time_picker&#34;,
&#34;instructions&#34;: &#34;&#34;,
&#34;required&#34;: 0,
&#34;conditional_logic&#34;: 0,
&#34;wrapper&#34;: {
&#34;width&#34;: &#34;&#34;,
&#34;class&#34;: &#34;&#34;,
&#34;id&#34;: &#34;&#34;
},
&#34;display_format&#34;: &#34;d.m.Y H:i&#34;,
&#34;return_format&#34;: &#34;Y-m-d H:i&#34;,
&#34;first_day&#34;: 1
},
{
&#34;key&#34;: &#34;field_619394a3e62e1&#34;,
&#34;label&#34;: &#34;Endet am&#34;,
&#34;name&#34;: &#34;relilab_enddate&#34;,
&#34;type&#34;: &#34;date_time_picker&#34;,
&#34;instructions&#34;: &#34;&#34;,
&#34;required&#34;: 0,
&#34;conditional_logic&#34;: 0,
&#34;wrapper&#34;: {
&#34;width&#34;: &#34;&#34;,
&#34;class&#34;: &#34;&#34;,
&#34;id&#34;: &#34;&#34;
},
&#34;display_format&#34;: &#34;d.m.Y H:i&#34;,
&#34;return_format&#34;: &#34;Y-m-d H:i&#34;,
&#34;first_day&#34;: 1
}
],
&#34;location&#34;: [
[
{
&#34;param&#34;: &#34;post_type&#34;,
&#34;operator&#34;: &#34;==&#34;,
&#34;value&#34;: &#34;post&#34;
}
]
],
&#34;menu_order&#34;: 0,
&#34;position&#34;: &#34;normal&#34;,
&#34;style&#34;: &#34;default&#34;,
&#34;label_placement&#34;: &#34;left&#34;,
&#34;instruction_placement&#34;: &#34;label&#34;,
&#34;hide_on_screen&#34;: &#34;&#34;,
&#34;active&#34;: true,
&#34;description&#34;: &#34;&#34;,
&#34;show_in_rest&#34;: 0,
&#34;acfe_display_title&#34;: &#34;&#34;,
&#34;acfe_autosync&#34;: &#34;&#34;,
&#34;acfe_form&#34;: 0,
&#34;acfe_meta&#34;: &#34;&#34;,
&#34;acfe_note&#34;: &#34;&#34;
}
]
</code></pre><h2 id="php">PHP<a hidden class="anchor" aria-hidden="true" href="#php">#</a></h2>
<h3 id="software">Software<a hidden class="anchor" aria-hidden="true" href="#software">#</a></h3>
<h4 id="php-storm">PHP-Storm<a hidden class="anchor" aria-hidden="true" href="#php-storm">#</a></h4>
<p><a href="https://www.jetbrains.com/de-de/phpstorm/">https://www.jetbrains.com/de-de/phpstorm/</a></p>
<h5 id="shortcode-zum-sprechen-bringen">Shortcode zum Sprechen bringen<a hidden class="anchor" aria-hidden="true" href="#shortcode-zum-sprechen-bringen">#</a></h5>
<p><a href="https://developer.wordpress.org/reference/functions/add_shortcode/">https://developer.wordpress.org/reference/functions/add_shortcode/</a>
In PhpStorm
<img loading="lazy" src="05-php-storm.png" alt="" />
<code>add_shortcode( string $tag, callable $callback )</code></p>
<p>alle Termine listen, die
<a href="https://www.advancedcustomfields.com/resources/orde-posts-by-custom-fields/">https://www.advancedcustomfields.com/resources/orde-posts-by-custom-fields/</a></p>
<p><img loading="lazy" src="06-termine-listen.png" alt="" />
</p>
<p>PHP-Storm nutzt als External Library dann WordPress
<img loading="lazy" src="07-external-library.png" alt="" />
</p>
<h3 id="plugin">Plugin<a hidden class="anchor" aria-hidden="true" href="#plugin">#</a></h3>
<p>Unsere Funktion:</p>
<pre tabindex="0"><code>/**
*Plugin Name: relilab Termine
*/
add_shortcode(&#39;termine&#39;,&#39;termineAusgeben&#39;);
function termineAusgeben( $atts ) {
$posts = get_posts(array(
&#39;post_type&#39; =&gt; &#39;post&#39;,
&#39;posts_per_page&#39; =&gt; -1,
&#39;category&#39; =&gt; &#39;termine&#39;,
&#39;meta_key&#39; =&gt; &#39;relilab_startdate&#39;,
&#39;orderby&#39; =&gt; &#39;meta_value&#39;,
&#39;order&#39; =&gt; &#39;DESC&#39;
));
// ob_start();
global $post;
?&gt;
&lt;ul&gt;
&lt;?php
foreach ($posts as $post) {
setup_postdata( $post )
?&gt;
&lt;li&gt;
&lt;a href=&#34;&lt;?php the_permalink(); ?&gt;&#34;&gt;&lt;?php the_title(); ?&gt; (date: &lt;?php the_field(&#39;relilab_startdate&#39;); ?&gt;)&lt;/a&gt;
&lt;/li&gt;
&lt;?php
}
?&gt;
&lt;/ul&gt;
&lt;?php
wp_reset_postdata();
// return ob_get_clean();
}
</code></pre>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,349 @@
<!DOCTYPE html>
<html lang="de" dir="auto">
<head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="robots" content="noindex, nofollow">
<title>Moodle Server mit Ubuntu 20 LTS und Iomad | Jörg Lohrer</title>
<meta name="keywords" content="Moodle, Ubuntu, Linux, Iomad, Server, MySql, Datenbank">
<meta name="description" content="Installation von Iomad zur Moodle-Instanz-Verwaltung">
<meta name="author" content="Jörg Lohrer">
<link rel="canonical" href="http://localhost:1313/2022/02/16/moodle-iomad-linux.html/">
<link crossorigin="anonymous" href="/assets/css/stylesheet.fcb38834b6dee4645dbe7c77d6c5278e12448b758b3f769e48a2d86d35709cb2.css" integrity="sha256-/LOINLbe5GRdvnx31sUnjhJEi3WLP3aeSKLYbTVwnLI=" rel="preload stylesheet" as="style">
<link rel="icon" href="http://localhost:1313/favicon.ico">
<link rel="icon" type="image/png" sizes="16x16" href="http://localhost:1313/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="http://localhost:1313/favicon-32x32.png">
<link rel="apple-touch-icon" href="http://localhost:1313/apple-touch-icon.png">
<link rel="mask-icon" href="http://localhost:1313/safari-pinned-tab.svg">
<meta name="theme-color" content="#2e2e33">
<meta name="msapplication-TileColor" content="#2e2e33">
<link rel="alternate" hreflang="de" href="http://localhost:1313/2022/02/16/moodle-iomad-linux.html/">
<noscript>
<style>
#theme-toggle,
.top-link {
display: none;
}
</style>
<style>
@media (prefers-color-scheme: dark) {
:root {
--theme: rgb(29, 30, 32);
--entry: rgb(46, 46, 51);
--primary: rgb(218, 218, 219);
--secondary: rgb(155, 156, 157);
--tertiary: rgb(65, 66, 68);
--content: rgb(196, 196, 197);
--code-block-bg: rgb(46, 46, 51);
--code-bg: rgb(55, 56, 62);
--border: rgb(51, 51, 51);
}
.list {
background: var(--theme);
}
.list:not(.dark)::-webkit-scrollbar-track {
background: 0 0;
}
.list:not(.dark)::-webkit-scrollbar-thumb {
border-color: var(--theme);
}
}
</style>
</noscript>
</head>
<body class="" id="top">
<script>
if (localStorage.getItem("pref-theme") === "dark") {
document.body.classList.add('dark');
} else if (localStorage.getItem("pref-theme") === "light") {
document.body.classList.remove('dark')
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
<header class="header">
<nav class="nav">
<div class="logo">
<a href="http://localhost:1313/" accesskey="h" title="Jörg Lohrer (Alt + H)">Jörg Lohrer</a>
<div class="logo-switches">
<button id="theme-toggle" accesskey="t" title="(Alt + T)">
<svg id="moon" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
<svg id="sun" xmlns="http://www.w3.org/2000/svg" width="24" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
</button>
<ul class="lang-switch"><li>|</li>
</ul>
</div>
</div>
<ul id="menu">
<li>
<a href="http://localhost:1313/" title="Jörg Lohrer">
<span>Home</span>
</a>
</li>
<li>
<a href="http://localhost:1313/archives/" title="Archive">
<span>Blog</span>
</a>
</li>
<li>
<a href="http://localhost:1313/impressum/" title="Impressum">
<span>Impressum</span>
</a>
</li>
<li>
<a href="https://reliverse.social/@joerglohrer" title="Mastodon">
<span><i class="fa fa-heart"></i>Mastodon</span>&nbsp;
<svg fill="none" shape-rendering="geometricPrecision" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round" stroke-width="2.5" viewBox="0 0 24 24" height="12" width="12">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"></path>
<path d="M15 3h6v6"></path>
<path d="M10 14L21 3"></path>
</svg>
</a>
</li>
</ul>
</nav>
</header>
<main class="main">
<article class="post-single">
<header class="post-header">
<h1 class="post-title entry-hint-parent">
Moodle Server mit Ubuntu 20 LTS und Iomad
</h1>
<div class="post-description">
Installation von Iomad zur Moodle-Instanz-Verwaltung
</div>
<div class="post-meta"><span title='2022-02-16 00:00:00 +0000 UTC'>Februar 16, 2022</span>&nbsp;·&nbsp;Jörg Lohrer
</div>
</header>
<figure class="entry-cover">
<img loading="eager" src="http://localhost:1313/2022/02/16/moodle-iomad-linux.html/title-gif.gif" alt="">
</figure>
<div class="post-content"><h1 id="moodle-server-mit-ubuntu-20-lts-und-iomad">Moodle Server mit Ubuntu 20 LTS und Iomad<a hidden class="anchor" aria-hidden="true" href="#moodle-server-mit-ubuntu-20-lts-und-iomad">#</a></h1>
<h2 id="ubuntu-server-image-herunterladen">Ubuntu Server-Image herunterladen<a hidden class="anchor" aria-hidden="true" href="#ubuntu-server-image-herunterladen">#</a></h2>
<p><a href="https://releases.ubuntu.com/20.04/">https://releases.ubuntu.com/20.04/</a></p>
<h2 id="virtualbox-mit-dem-ubuntu-image-einrichten">Virtualbox mit dem Ubuntu Image einrichten<a hidden class="anchor" aria-hidden="true" href="#virtualbox-mit-dem-ubuntu-image-einrichten">#</a></h2>
<h3 id="netzwerkbrücke-aktivieren">Netzwerkbrücke aktivieren<a hidden class="anchor" aria-hidden="true" href="#netzwerkbrücke-aktivieren">#</a></h3>
<p><img loading="lazy" src="01-netzwerkbruecke.png" alt="" />
</p>
<h3 id="ip-adresse-ermitteln">IP-Adresse ermitteln<a hidden class="anchor" aria-hidden="true" href="#ip-adresse-ermitteln">#</a></h3>
<p><code>ifconfig </code> -&gt; 192.168.178.132</p>
<h3 id="auf-dem-mac-oder-pc-die-auflösung-des-hosts-verknüpfen">Auf dem Mac (oder PC) die Auflösung des Hosts verknüpfen<a hidden class="anchor" aria-hidden="true" href="#auf-dem-mac-oder-pc-die-auflösung-des-hosts-verknüpfen">#</a></h3>
<p>auf dem Mac <code>sudo nano /etc/hosts</code> die IP eintragen und moodle.local zuweisen:
<img loading="lazy" src="02-hosts-eintragen.png" alt="" />
</p>
<h2 id="moodle-server-auf-virtualbox-vorbereiten">Moodle Server auf Virtualbox vorbereiten<a hidden class="anchor" aria-hidden="true" href="#moodle-server-auf-virtualbox-vorbereiten">#</a></h2>
<p><code>sudo -i</code>wechselt auf root</p>
<h3 id="ssh-zugriff-ermöglichen">SSH Zugriff ermöglichen<a hidden class="anchor" aria-hidden="true" href="#ssh-zugriff-ermöglichen">#</a></h3>
<p><a href="https://linuxconfig.org/allow-ssh-root-login-on-ubuntu-20-04-focal-fossa-linux">Allow SSH root login on Ubuntu 20.04 Focal Fossa Linux</a></p>
<h3 id="shellbefehle-zur-installation">Shellbefehle zur Installation:<a hidden class="anchor" aria-hidden="true" href="#shellbefehle-zur-installation">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">sudo apt update &amp;&amp; apt upgrade -y
apt install mariadb-server
sudo apt install apache2 libapache2-mod-fcgid
sudo apt install php php-cli php-fpm php-json php-common php-mysql php-zip php-gd php-mbstring php-curl php-xml php-pear php-bcmath php-intl php-xmlrpc php-soap
a2enconf php7.4-fpm
sudo a2enmod actions fcgid alias proxy_fcgi setenvif
a2dismod php7.4
a2dismod mpm_prefork
a2dismod mpm_worker
a2enmod mpm_event
systemctl restart php7.4-fpm apache2
</code></pre><h3 id="anlegen-etcapache2sites-availablemoodleconf">anlegen: /etc/apache2/sites-available/moodle.conf<a hidden class="anchor" aria-hidden="true" href="#anlegen-etcapache2sites-availablemoodleconf">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">############################
&lt;VirtualHost *:80&gt;
ServerName moodle.local
ServerAdmin webmaster@localhost
DocumentRoot /var/www/moodle
&lt;FilesMatch \.php$&gt;
SetHandler &#34;proxy:unix:/var/run/php/php7.4-fpm.sock|fcgi://localhost/&#34;
&lt;/FilesMatch&gt;
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
&lt;/VirtualHost&gt;
####################################################################
</code></pre><h3 id="weitere-shellbefehle-zur-installation">Weitere Shellbefehle zur Installation:<a hidden class="anchor" aria-hidden="true" href="#weitere-shellbefehle-zur-installation">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">a2ensite moodle.conf
systemctl reload apache2
mkdir /var/www/moodle
echo &#39;&lt;?php phpinfo(); ?&gt;&#39; &gt; /var/www/moodle/info.php
</code></pre><h3 id="host-eintrag-hinzufügen-192168178xxx-moodlelocal">host eintrag hinzufügen: &ldquo;192.168.178.xxx moodle.local&rdquo;<a hidden class="anchor" aria-hidden="true" href="#host-eintrag-hinzufügen-192168178xxx-moodlelocal">#</a></h3>
<p>192.168.178.xxx moodle.local
192.168.178.xxx <a href="https://www.moodle.local">www.moodle.local</a></p>
<h3 id="im-browser-öffnen-httpmoodlelocalinfophp">im Browser öffnen: <a href="http://moodle.local/info.php">http://moodle.local/info.php</a><a hidden class="anchor" aria-hidden="true" href="#im-browser-öffnen-httpmoodlelocalinfophp">#</a></h3>
<h3 id="maschine-speichern-und-klonen">maschine speichern und klonen<a hidden class="anchor" aria-hidden="true" href="#maschine-speichern-und-klonen">#</a></h3>
<h2 id="anschließend-iomad-moodle-installieren">Anschließend IOMAD moodle installieren:<a hidden class="anchor" aria-hidden="true" href="#anschließend-iomad-moodle-installieren">#</a></h2>
<p><a href="https://www.iomad.org/wp-content/uploads/2021/03/Iomad-Installation-Guide.pdf">https://www.iomad.org/wp-content/uploads/2021/03/Iomad-Installation-Guide.pdf</a></p>
<h3 id="datenbank-für-moodle-erzeugen-via-ssh">Datenbank für moodle erzeugen via SSH:<a hidden class="anchor" aria-hidden="true" href="#datenbank-für-moodle-erzeugen-via-ssh">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">
mysql
CREATE DATABASE moodledb;
CREATE USER &#39;moodleowner&#39;@&#39;localhost&#39; IDENTIFIED BY &#39;$mdb2passwd&#39;;
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, INDEX, DROP, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON moodledb.* TO &#39;moodleowner&#39;@&#39;localhost&#39;;
GRANT FILE ON *.* TO &#39;moodleowner&#39;@&#39;localhost&#39;;
quit
</code></pre><h3 id="installation-iomad">Installation iomad<a hidden class="anchor" aria-hidden="true" href="#installation-iomad">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">cd /var/www/moodle
git clone https://github.com/iomad/iomad.git
cd iomad
git checkout -b myiomad origin/IOMAD_310_STABLE
mkdir /var/www/moodledata &amp;&amp; chmod 777 /var/www/moodledata
</code></pre><p><strong>ändern!!!</strong>: <code>/etc/apache2/sites-available/moodle.conf -&gt; DocumentRoot /var/www/moodle/iomad</code></p>
<p><code>systemctl restart php7.4-fpm apache2</code></p>
<h3 id="httpmoodlelocal-aufrufen-und-configphp-datei-mit-hilfe-des-assistenten-generieren-lassen"><a href="http://moodle.local">http://moodle.local</a> aufrufen und config.php Datei mit Hilfe des Assistenten generieren lassen<a hidden class="anchor" aria-hidden="true" href="#httpmoodlelocal-aufrufen-und-configphp-datei-mit-hilfe-des-assistenten-generieren-lassen">#</a></h3>
<p>![](03-config generieren.png)</p>
<p><strong>ändern!!!</strong>: <code>/moodle</code> entfernen</p>
<h3 id="configphp">config.php<a hidden class="anchor" aria-hidden="true" href="#configphp">#</a></h3>
<pre tabindex="0"><code class="language-shell=" data-lang="shell=">&lt;?php // Moodle configuration file
unset($CFG);
global $CFG;
$CFG = new stdClass();
$CFG-&gt;dbtype = &#39;mariadb&#39;;
$CFG-&gt;dblibrary = &#39;native&#39;;
$CFG-&gt;dbhost = &#39;localhost&#39;;
$CFG-&gt;dbname = &#39;moodledb&#39;;
$CFG-&gt;dbuser = &#39;moodleowner&#39;;
$CFG-&gt;dbpass = &#39;$mdb2passwd&#39;;
$CFG-&gt;prefix = &#39;mdl_&#39;;
$CFG-&gt;dboptions = array (
&#39;dbpersist&#39; =&gt; 0,
&#39;dbport&#39; =&gt; &#39;&#39;,
&#39;dbsocket&#39; =&gt; &#39;&#39;,
&#39;dbcollation&#39; =&gt; &#39;utf8mb4_general_ci&#39;,
);
$CFG-&gt;wwwroot = &#39;http://moodle.local&#39;;
$CFG-&gt;dataroot = &#39;/var/www/moodledata&#39;;
$CFG-&gt;admin = &#39;admin&#39;;
$CFG-&gt;directorypermissions = 0777;
require_once(__DIR__ . &#39;/lib/setup.php&#39;);
// There is no php closing tag in this file,
// it is intentional because it prevents trailing whitespace problems!
</code></pre>
</div>
<footer class="post-footer">
<ul class="post-tags">
</ul>
</footer>
</article>
</main>
<footer class="footer">
<span>&copy; 2025 <a href="http://localhost:1313/">Jörg Lohrer</a></span>
<span>
Powered by
<a href="https://gohugo.io/" rel="noopener noreferrer" target="_blank">Hugo</a> &
<a href="https://github.com/adityatelange/hugo-PaperMod/" rel="noopener" target="_blank">PaperMod</a>
<a rel="me" href="https://reliverse.social/@joerglohrer">Mastodon</a>
</span>
</footer>
<a href="#top" aria-label="go to top" title="Go to Top (Alt + G)" class="top-link" id="top-link" accesskey="g">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 6" fill="currentColor">
<path d="M12 6H0l6-6z" />
</svg>
</a>
<script>
let menu = document.getElementById('menu')
if (menu) {
menu.scrollLeft = localStorage.getItem("menu-scroll-position");
menu.onscroll = function () {
localStorage.setItem("menu-scroll-position", menu.scrollLeft);
}
}
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function (e) {
e.preventDefault();
var id = this.getAttribute("href").substr(1);
if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView({
behavior: "smooth"
});
} else {
document.querySelector(`[id='${decodeURIComponent(id)}']`).scrollIntoView();
}
if (id === "top") {
history.replaceState(null, null, " ");
} else {
history.pushState(null, null, `#${id}`);
}
});
});
</script>
<script>
var mybutton = document.getElementById("top-link");
window.onscroll = function () {
if (document.body.scrollTop > 800 || document.documentElement.scrollTop > 800) {
mybutton.style.visibility = "visible";
mybutton.style.opacity = "1";
} else {
mybutton.style.visibility = "hidden";
mybutton.style.opacity = "0";
}
};
</script>
<script>
document.getElementById("theme-toggle").addEventListener("click", () => {
if (document.body.className.includes("dark")) {
document.body.classList.remove('dark');
localStorage.setItem("pref-theme", 'light');
} else {
document.body.classList.add('dark');
localStorage.setItem("pref-theme", 'dark');
}
})
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

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