diff --git a/CLAUDE.md b/CLAUDE.md index b8e8611..a4e1724 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,20 +8,29 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. - **Frontend:** Natives HTML, Vanilla CSS, Vanilla JS. Kein Framework, keine Build-Pipeline, kein Node. - **Backend:** Vanilla PHP ≥ 8.1, Composer nur für `phpmailer/phpmailer`. Keine Template-Engine. - **Hosting-Ziel:** Apache Shared Hosting, PHP 8.x, `.htaccess`, Cronjobs verfügbar. -- **Lokales Dev:** `php -S localhost:8000 -t public public/index.php` (index.php fungiert als Router-Script, weil `php -S` kein .htaccess kennt). +- **Lokales Dev:** `php -d upload_max_filesize=250M -d post_max_size=260M -S localhost:8000 -t public public/index.php` + (index.php fungiert als Router-Script, weil `php -S` kein .htaccess kennt). Die beiden `-d`-Schalter + brauchst du für die Dateiablage `/dateien`: der CLI-Server liest keine `.user.ini`, und mit den + PHP-Standardwerten (oft 2 MB) scheitert jeder Video-Upload. Für die Seite selbst sind sie egal. - **Docroot:** `public/`. Falls der Hoster nur das Repo-Root serviert, greift die Root-`.htaccess` (Rewrite → `public/`, Deny für alles andere). ## Harte Regeln (nicht verhandelbar) 1. **Keine externen Einbindungen.** Keine CDNs, keine externen Fonts/Scripts/iframes/Embeds, keine Tracking-/Analytics-Dienste, kein reCAPTCHA. Alles wird self-hosted. Einzige erlaubte ausgehende - Verbindungen: All-Inkl-SMTP (Mailversand, serverseitig) und der Instagram-Scraper (CLI/Cron, nie im Request-Pfad). + Verbindungen: All-Inkl-SMTP (Mailversand, serverseitig) und die CLI/Cron-Skripte unter `bin/` + (Instagram-Scraper, Matchcenter-Sync, Files-Gallery-Update — nie im Request-Pfad). Besucher-Browser kontaktieren ausschließlich unsere Domain. Die CSP (`default-src 'self'`) erzwingt das — nicht aufweichen. 2. **`.context/` und `.context_db/` sind reine Referenz** (alte, unsichere CMS-Seite). Von dort werden nur Assets (Bilder/Fonts/Videos), Texte und Styling-Werte extrahiert. **Niemals Code übernehmen, niemals Dateien von dort direkt verlinken.** Assets immer nach `public/assets/` kopieren/optimieren. 3. **Kein Admin-/Pflege-Backend bauen.** Inhalte werden per Chat gepflegt: Claude editiert `data/*.json` - bzw. die Seiten-Dateien. Kein Login, keine Schreib-Endpoints. + bzw. die Seiten-Dateien. Kein Login, keine Schreib-Endpoints — mit **zwei** benannten Ausnahmen: + (a) die Dateiablage `/dateien` (siehe unten), eine fremde, gekapselte App, die ausschließlich in + `storage/dateien/uploads/` schreibt und **keine** Website-Inhalte anfasst; (b) der Cron-Endpoint + `public/cron.php` (siehe unten), der nur zwei feste Sync-Skripte startet und keine Eingaben + verarbeitet. Nichts davon darf in den eigenen Code wachsen: kein zweiter Login, keine Schreib-Route + in `app/`, keine dritte Ausnahme ohne denselben Aufwand an Begründung. 4. **Sensible Altdaten nie übernehmen:** Aus dem DB-Dump keine Mail-Logs, Formulareinträge, Passwort-Hashes, Tokens oder personenbezogene Daten migrieren. 5. **Secrets nur in `config/config.php`** (gitignored, außerhalb des Webroots, per .htaccess denied). @@ -34,7 +43,7 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. | Farben, Fonts, Spacing, Radii, Schatten | `public/assets/css/tokens.css` | Hex-Werte/Magic Numbers in anderen CSS-Dateien | | Seiten-Slugs & Routing | `app/routes.php` | URLs woanders hart verdrahten | | Navigation (Labels/Reihenfolge) | `data/navigation.json` | Menüpunkte in Templates | -| Vereinsdaten (Name, Adresse, Kontakt, Social, Geo) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren | +| Vereinsdaten (Name, Adresse, Kontakt, Social, Geo, Slogan) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren | | Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` | | Mannschaften (Kader, Trainer, Hero, FAQ) | `data/teams.json` | Spieler-/Trainernamen in Team-Templates | | Seiteninhalte (Texte, Hero, FAQ) | `data/.json` (z. B. `fussball`, `jugend`, `turnen`, `verein`, `mitmachen`, `historie`, `partner-werden`, `sportheimbuchung`) | Texte/Hero/FAQ hart in `pages/*.php` | @@ -43,6 +52,12 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. | Partner/Sponsoren | `data/partners.json` | — | | Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** | | Matchcenter (Spiele/Ergebnisse/Tabellen) | `data/matchcenter.json` + `public/assets/img/crests/` | **maschinenverwaltet von `bin/matchcenter-sync.php` — nie von Hand editieren**. Team-/Wettbewerbs-IDs in `config.php` (`matchcenter.teams`) | +| Stadionzeitung (Ausgaben-Archiv) | `data/stadionzeitung.json` + `public/assets/img/stadionzeitung/` | **maschinenverwaltet von `bin/stadionzeitung-add.php` + `bin/stadionzeitung-publish.php` (Status/Snapshot) — nie von Hand editieren; einzige Chat-Pflege-Ausnahmen sind `vorwort` und `interview`.** Der Ausgaben-Ordner wird bei jedem Lauf komplett geleert und neu gefüllt | +| Sponsoren-Anzeigen (Stadionzeitung) | `data/anzeigen.json` + `public/assets/img/anzeigen/` | Dauerhafter Bestand, **nicht** pro Ausgabe: Motive ändern sich selten, das Heft erscheint oft. `sponsor`, `alt` (optional), `aktiv` und die **Reihenfolge** (= Reihenfolge im Heft) im Chat gepflegt; `base`/`widths` schreibt ausschließlich `bin/anzeige-add.php`. Anzeigen nie in den Ausgaben-Ordner legen | +| Termine/Events (`/termine`) | `data/events.json` (Vereins-Termine **ohne** eigene Detailseite) — Spieltermine kommen weiterhin nur aus `data/matchcenter.json`, Feste mit eigener Seite nur aus `data/veranstaltungen.json`; die Termine-Seite mergt alle drei | Von Claude im Chat gepflegt wie `teams.json`/`partners.json` — **keine** Automatisierung, kein Bin-Skript. Spieltermine nie hier duplizieren. Ein Fest, das eine Detailseite hat, steht **nur** in `veranstaltungen.json` (`bin/preflight.php` prüft das) | +| Veranstaltungen (`/veranstaltungen` + Detailseiten) | `data/veranstaltungen.json` + `public/assets/img/veranstaltungen/` | Termin, Texte, Programm, Abschnitte von Claude im Chat gepflegt — **außer `plakate` und `galerie`: die schreibt ausschließlich `bin/veranstaltung-bilder.php`**. Adresse nie in die Texte schreiben (steht in `club.json`) | +| News (`/news` + optionale Detailseiten) | `data/news.json` (redaktionelle Meldungen; `artikel[]` = Fließtext-Blöcke, `status` = Entwurf/veröffentlicht) — Instagram-Posts und Ergebnisse kommen weiterhin nur aus `data/instagram.json`/`data/matchcenter.json`, werden auf der News-Seite nur gemergt | Von Claude im Chat gepflegt wie `events.json`/`teams.json` — **keine** Automatisierung, kein Bin-Skript. Instagram/Ergebnisse nie hier duplizieren. `text` bleibt der Teaser (Feed, Meta-Description, Article-Schema), der Artikel steht in `artikel[]` | +| Dateiablage `/dateien` (Konfiguration) | `public/dateien/_filesconfig.php` (Rechte pro Konto: `storage/dateien/system/users/admin/config.php`) | **`storage/dateien/system/config/config.php` von Hand editieren** — die App generiert sie selbst und überschreibt sie bei jedem Update. Passwörter/Lizenz gehören in `config.php` (`dateien.*`) | | Icons | `public/assets/icons/` (Bootstrap Icons, lokal) via `icon()`-Helper | Inline-SVG-Pfade von Hand ins Markup, Emoji/Unicode-Glyphen, gezeichnete CSS-Icons, Icon-Font/CDN | Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pages/*.php`) im `$meta`-Array. @@ -55,33 +70,80 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag `sportsevent_nodes()` (SportsEvent-Knoten für kommende Spiele, Matchcenter) und `job_posting_schema()` (JobPosting `VOLUNTEER` für Ehrenamtsstellen, `/mitmachen`, verweist via `#club`). Sichtbare FAQ + FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt und Markup nie auseinanderlaufen. +Redaktioneller Text aus `data/*.json` darf interne Deeplinks als `[Label](slug)` tragen (auch +`[Label](#anker)` bzw. `slug#anker`) — `inline_links_html()` rendert sie als ``, +`inline_links_text()` hält HTML-freie Ausgaben (FAQPage-Schema) sauber. Genutzt von den +FAQ-Antworten (beide FAQ-Komponenten, `faq_schema()`) und vom Artikelkörper der News-Detailseite. +**Kein rohes HTML in der JSON, keine externen URLs** — die Escaping-Garantie ist der Wert dieser +Funktion, sie ist die einzige Stelle, an der aus JSON HTML entsteht. ## Architektur-Muster - **Front Controller:** `public/index.php` → Slug-Lookup in `app/routes.php` → Page-Datei setzt `$meta` - und emittiert Body via `component()` → `app/layout.php` rendert die Shell. + und emittiert Body via `component()` → `app/layout.php` rendert die Shell. **Genau drei bewusste + Ausnahmen** vom exakten Slug-Lookup, alle als benannte Prefix-Routen in `$prefixRoutes` + (`public/index.php`, vor dem `routes.php`-Lookup): `stadionzeitung/` → + `app/pages/stadionzeitung-ausgabe.php`, `veranstaltungen/` → `app/pages/veranstaltung.php` + und `news/` → `app/pages/news-artikel.php`. + Alle drei ermitteln ihren Detail-Slug selbst wieder aus der URL. **Keine generelle + Wildcard-Fähigkeit** — eine vierte Ausnahme braucht denselben Aufwand an Begründung. + `news/` ist kein neues Muster, sondern das dritte Vorkommen desselben (Detailseite zu einem + Eintrag in einer `data/*.json`); die Alternative, ein exakter `routes.php`-Eintrag pro Meldung, + würde redaktionelle Inhalte ins Routen-Register schreiben und die Single-Source-Regel brechen. + Die Übersichts-Slugs (`stadionzeitung`, `veranstaltungen`, `news`) beginnen nicht mit ihrem + eigenen Präfix und bleiben davon unberührt. + `$current` bleibt dabei der volle URL-Slug (sonst kanonisierte jede Detailseite auf die + Übersicht); dass der Elternpunkt in der Navigation deshalb kein `aria-current` bekommt, ist + bewusst in Kauf genommen. - **Komponenten:** `app/components/*.php` sind dumme Includes, bekommen Props via `component('name', ['key' => $val])`. **Component-first-Regel:** Bevor neues Markup/CSS entsteht, prüfen ob eine Komponente existiert und erweitert werden kann. Wiederkehrende Inhalte auf neuen Seiten → als Komponente extrahieren. - **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()` (interne - Pfade), `abs_url()` (absolute URLs für Canonical/OG), `asset()` (Versionierung via filemtime), + Pfade), `abs_url()` (absolute URLs für Canonical/OG), `asset()` (Versionierung via filemtime; + zweiter Parameter `false` lässt `?v=` weg — nur für Dateien, die auch statisches CSS anfordert, + also die Font-Preloads in `meta.php`, deren URL exakt der `@font-face`-URL aus `base.css` + entsprechen muss; geänderte Fonts bekommen einen neuen Dateinamen statt einer Version), `json_load('name')` (liest `data/name.json`, static cache), `component()`, `config('key')` (Werte aus `config/config.php`), `icon('name', 'klasse', 'label?')` (Inline-SVG aus `public/assets/icons/`, siehe Abschnitt Icons), `form_token()`/`form_token_valid()` (Time-Trap-Token, siehe Formulare), `img_intrinsic_attrs('pfad')` (width/height-Attribute aus der Bilddatei für CLS-freie ``, wenn die Maße nicht schon statisch bekannt sind), `match_when('2026-08-01T14:00')` (Anstoß aus `matchcenter.json` → `iso` für ``-Variante samt +Hover gab es für Instagram und Ergebnisse schon. + +**Zwei geteilte Bausteine, entstanden mit dieser Seite:** +`app/components/detail-head.php` — der schmale Kopf (Rückweg, `h1`, Entwurf-Badge, Datum/Ort) für +Veranstaltungs- **und** News-Detailseite, Klassen `.detail-head*`. Der Kopf der +Stadionzeitungs-Ausgabe bleibt bewusst eine eigene Fassung (Viewer-Chrome: Drei-Zonen-Grid, Titel als +`

`, Druck-Knopf, eigene `@media print`-Regeln). +`.prose` in `components.css` — die Lesespalte (72ch) samt Typografie für langen Fließtext, genutzt +vom Artikelkörper **und** von `.legal__body` (Impressum, Datenschutz tragen die Klasse zusätzlich). + +**Bilder** über `php bin/img-resize.php` wie überall, Zielordner `public/assets/img/news/`. Für neue +Artikel `[640, 1000, 1600]` erzeugen (das Bestandsbild `neue-website` hat nur 640/1000, weil das +Original nicht mehr vorliegt und `resize_image_variants()` nie hochskaliert). Kein +`bin/news-bilder.php`: News-Bilder sind Einzelstücke, kein Ordnerbestand wie Plakate und +Festgalerien. + +## Dateiablage /dateien (Files Gallery) + +Damit der Verein Fotos und Videos von Events selbst abliefern kann: `https://…/dateien/` ist eine +vendorte Fremd-App (**Files Gallery**, Vollversion-Lizenz, `public/dateien/index.php`) — die benannte +Ausnahme zu Regel 3. Zwei Zugänge: **`verein`** (hochladen, Ordner anlegen, herunterladen — bewusst +kein Löschen/Umbenennen, damit ein Fehlklick am gemeinsamen Konto nicht die Fotos anderer trifft) und +**`admin`** (alles außer `allow_settings`). Nicht in der Navigation, nicht in der Sitemap, +`Disallow` in `robots.txt`, `noindex` per Header und Meta. Das Einarbeiten der Bilder in die Seite +bleibt Chat-Arbeit (`bin/img-resize.php`). + +**Drei Entscheidungen tragen den Aufbau — keine davon anfassen, ohne den Grund zu kennen:** + +1. **`root` und `storage_path` liegen außerhalb des Docroots** (`storage/dateien/uploads` bzw. + `…/system`), dazu `load_files_proxy_php`. Die App lässt per Default **jede** Dateiendung zum Upload + zu (keine Blacklist, `index.php:4078-4092`); liegt das Verzeichnis nicht im Web-Baum, ist eine + hochgeladene `.php` prinzipiell nicht ausführbar und alles läuft durch PHP (`index.php:1607`). + `upload_allowed_file_types` ist die zweite Schicht, nicht die erste. +2. **`public/dateien/_filesconfig.php` ist der einzige Integrationspunkt.** Sie wird als erstes geladen + (`index.php:142`), gewinnt gegen die selbst generierte Storage-Config (`index.php:239`) und läuft + noch vor `session_start()` und Login — deshalb stehen dort auch Session-Härtung, das Login-Rate-Limit + (`rate_limit_ok()`, Log nach `spam.log`) und `display_errors = 0` (die App ist PHP-7-Code; eine + Deprecation-Notice landet sonst mitten im Bild-Stream und zerstört Thumbnails). + **Das Vereins-Konto muss das globale `username`/`password` der App bleiben:** ohne globales Passwort + sind Konten unter `system/users/` nur *zusätzliche* Logins und die Ablage wäre offen lesbar + (`index.php:328-337`). +3. **Assets self-hosted** unter `public/assets/filesgallery/` (Regel 1 — die App lädt sonst CSS und 14 + Scripts von jsDelivr). Die Pfade enthalten die Version, ein Update muss sie also mitziehen. + +**Uploadgrenze an zwei Stellen, es gilt die kleinere:** `upload_max_filesize` in +`_filesconfig.php` (250 MB, auf Videoclips gerechnet) und die PHP-Grenzen aus +`public/dateien/.user.ini`. `.user.ini` statt `.htaccess`, weil All-Inkl PHP als FastCGI/FPM fährt +(`php_value` wäre dort wirkungslos). Lokal übernehmen das die `-d`-Schalter im Dev-Kommando. +`bin/preflight.php` vergleicht beide Werte und warnt, wenn PHP niedriger steht. +Erlaubt sind Bilder (inkl. HEIC), `mp4/mov/m4v/webm`, PDF und `svg`. Video-Vorschaubilder brauchen +`ffmpeg` auf dem Server — fehlt es, funktionieren Upload und Wiedergabe trotzdem, es gibt nur ein Icon. +SVG hat keine Vorschau (GD kann sie nicht rastern), wird aber im Listing geführt. + +**SVG ist der einzige erlaubte Typ, der Code tragen kann** (`