CLAUDE.md mit Code-Stand abgeglichen + Hero-Regel dokumentiert

- Hero-Regel festgeschrieben (Startseite groß, Unterseiten Überschrift +
  Teaser); gleiche Konvention im Docblock von hero.php
- Single-Source-Tabelle generalisiert (Seiteninhalte -> data/<slug>.json)
  + ansprechpartner.json als geteilte Personen-Quelle
- Spam-Schutz vollständig: Rate-Limiting, Link-Check, spam.log
- JSON-LD-Helper (sportsevent_nodes, job_posting_schema) und Helper-Liste
  (abs_url, config, form_token) ergänzt
- bin/img-resize.php und vollständige Logs-Liste dokumentiert

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
This commit is contained in:
2026-06-20 20:30:04 +02:00
parent 169878a4f7
commit 73583a896c
2 changed files with 31 additions and 10 deletions

View File

@@ -37,7 +37,8 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern.
| Vereinsdaten (Name, Adresse, Kontakt, Social, Geo) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren | | Vereinsdaten (Name, Adresse, Kontakt, Social, Geo) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren |
| Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` | | Startseiten-Inhalte | `data/home.json` | Texte in `pages/home.php` |
| Mannschaften (Kader, Trainer, Hero, FAQ) | `data/teams.json` | Spieler-/Trainernamen in Team-Templates | | Mannschaften (Kader, Trainer, Hero, FAQ) | `data/teams.json` | Spieler-/Trainernamen in Team-Templates |
| Fußball-/Jugend-/Vereinsseite (Inhalte) | `data/fussball.json` · `data/jugend.json` · `data/verein.json` | Texte in den jeweiligen `pages/*.php` | | Seiteninhalte (Texte, Hero, FAQ) | `data/<slug>.json` (z. B. `fussball`, `jugend`, `turnen`, `verein`, `mitmachen`, `historie`, `partner-werden`, `sportheimbuchung`) | Texte/Hero/FAQ hart in `pages/*.php` |
| Ansprechpartner/Personen (geteilte Quelle) | `data/ansprechpartner.json` (per Abteilung auf Team-/Jugend-/Verein-Seiten gezogen) | Namen/Funktionen/Kontakte in Templates duplizieren |
| Banner-Slider (Startseite) | `data/banners.json` | Slides in `pages/home.php` oder der Komponente | | Banner-Slider (Startseite) | `data/banners.json` | Slides in `pages/home.php` oder der Komponente |
| Partner/Sponsoren | `data/partners.json` | — | | Partner/Sponsoren | `data/partners.json` | — |
| Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** | | Instagram-Cache | `data/instagram.json` + `public/assets/img/instagram/` | **maschinenverwaltet von `bin/instagram-sync.php` — nie von Hand editieren** |
@@ -50,8 +51,10 @@ Per-Page-Meta (Title/Description/OG) lebt in der jeweiligen Page-Datei (`app/pag
`app/components/jsonld.php` hängt sie an den globalen `@graph` (Organization `#club`) an — nie ein `app/components/jsonld.php` hängt sie an den globalen `@graph` (Organization `#club`) an — nie ein
`<script type="application/ld+json">` von Hand ins Markup. Knoten **nur** über die Helper in `<script type="application/ld+json">` von Hand ins Markup. Knoten **nur** über die Helper in
`helpers.php` bauen: `breadcrumb_schema()`, `faq_schema()`, `page_schema()` (Breadcrumb + optional FAQ), `helpers.php` bauen: `breadcrumb_schema()`, `faq_schema()`, `page_schema()` (Breadcrumb + optional FAQ),
`team_schema()` (SportsTeam + Breadcrumb + FAQ, verweist via `#club` auf den Club). Sichtbare FAQ + `team_schema()` (SportsTeam + Breadcrumb + FAQ, verweist via `#club` auf den Club),
FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt und Markup nie auseinanderlaufen. `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.
## Architektur-Muster ## Architektur-Muster
@@ -61,14 +64,20 @@ FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt und Markup nie a
`component('name', ['key' => $val])`. **Component-first-Regel:** Bevor neues Markup/CSS entsteht, `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 prüfen ob eine Komponente existiert und erweitert werden kann. Wiederkehrende Inhalte auf neuen
Seiten → als Komponente extrahieren. Seiten → als Komponente extrahieren.
- **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()`, `asset()` - **Helpers** (`app/helpers.php`): `e()` (Escaping — IMMER für dynamische Ausgaben), `url()` (interne
(Versionierung via filemtime), `json_load('name')` (liest `data/name.json`, static cache), `component()`, Pfade), `abs_url()` (absolute URLs für Canonical/OG), `asset()` (Versionierung via filemtime),
`icon('name', 'klasse', 'label?')` (Inline-SVG aus `public/assets/icons/`, siehe Abschnitt Icons). `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).
- **CSS:** 6 Dateien als `<link>` in fester Reihenfolge: tokens → reset → base → layout → components → - **CSS:** 6 Dateien als `<link>` in fester Reihenfolge: tokens → reset → base → layout → components →
utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit utilities. Kein `@import`, kein Inline-Style. Komponenten-Styles in `components.css` mit
Banner-Kommentaren (`/* === hero === */`). Banner-Kommentaren (`/* === hero === */`).
- **JS:** Vanilla, `defer`, progressive enhancement — alles muss ohne JS funktionieren - **JS:** Vanilla, `defer`, progressive enhancement — alles muss ohne JS funktionieren
(Formulare = normales POST + Redirect). Ein Feature = eine Datei = eine selbst-initialisierende IIFE. (Formulare = normales POST + Redirect). Ein Feature = eine Datei = eine selbst-initialisierende IIFE.
- **Hero-Regel:** Die Startseite hat **einen** großen Hero (`display: true`, mit `kicker`/`tagline`/Video).
**Alle Unterseiten-Heros sind einheitlich „Überschrift + Teaser"** — nur `title` + `text`, dazu
`compact: true` und `align: 'bottom-left'`. **Kein `kicker`, keine `tagline`** auf Unterseiten (hält es
klar und einfach). `ctas` nur als begründete Ausnahme. Gilt für JSON- wie Inline-Heros.
- **Neue Seite anlegen** = genau 3 Schritte: `app/pages/<slug>.php` + Eintrag in `app/routes.php` - **Neue Seite anlegen** = genau 3 Schritte: `app/pages/<slug>.php` + Eintrag in `app/routes.php`
(+ optional `data/navigation.json`). Sitemap & Canonical folgen automatisch. (+ optional `data/navigation.json`). Sitemap & Canonical folgen automatisch.
@@ -86,9 +95,15 @@ FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt und Markup nie a
- A11y-Vorgaben: Feldränder mit `--clr-glass-border-strong` (≥3:1 Kontrast, WCAG 1.4.11), Checkbox - A11y-Vorgaben: Feldränder mit `--clr-glass-border-strong` (≥3:1 Kontrast, WCAG 1.4.11), Checkbox
min. 24px (WCAG 2.5.8), Status-Region nie per `display:none` verstecken (fliegt aus dem Accessibility-Tree). min. 24px (WCAG 2.5.8), Status-Region nie per `display:none` verstecken (fliegt aus dem Accessibility-Tree).
- Versand via PHPMailer über Brevo SMTP (`smtp-relay.brevo.com:587`, STARTTLS), Config aus `config/config.php`. - Versand via PHPMailer über Brevo SMTP (`smtp-relay.brevo.com:587`, STARTTLS), Config aus `config/config.php`.
- Spam-Schutz ohne externe Dienste: Honeypot-Feld + HMAC-signierter Timestamp (Time-Trap, `app_secret`) - Spam-Schutz ohne externe Dienste — mehrschichtig in jeder `app/actions/*`-Action:
+ serverseitige Validierung mit Whitelists. POST-Redirect-GET; `form.js` macht optional fetch. (1) Honeypot-Feld `website`, (2) HMAC-signierter Timestamp / Time-Trap (`ft`-Token via
- Fehler nach `storage/logs/mail.log`, nie an Besucher leaken. `form_token()`/`form_token_valid()`, `app_secret`), (3) serverseitige Whitelist-Validierung,
(4) **Rate-Limiting** via `rate_limit_ok()` mit Zählern unter `storage/ratelimit/`:
pro IP+Route (5/10 min), globaler Tages-Cap (100/Tag) und Token-Replay-Sperre (3×/Token),
(5) Link-Count-Check im Freitext. Abgewiesene Versuche → `log_spam()` nach `storage/logs/spam.log`.
Immer POST-Redirect-GET; `form.js` macht optional fetch.
- Logs unter `storage/logs/`: `mail.log` (Mailversand-Fehler), `spam.log` (abgewiesene Form-Versuche),
`instagram.log` / `matchcenter.log` (Sync-Läufe), `php-errors.log`. Fehler nie an Besucher leaken.
## Design (aus der alten Seite extrahiert, verifiziert) ## Design (aus der alten Seite extrahiert, verifiziert)
@@ -116,7 +131,8 @@ FAQPage-Schema aus derselben `data/*.json`-Quelle, damit Inhalt und Markup nie a
- [ ] Kontrast ≥ 4.5:1 (Fließtext) / 3:1 (große Schrift, UI) - [ ] Kontrast ≥ 4.5:1 (Fließtext) / 3:1 (große Schrift, UI)
- [ ] `prefers-reduced-motion` respektiert (globaler Kill-Switch in reset.css) - [ ] `prefers-reduced-motion` respektiert (globaler Kill-Switch in reset.css)
- [ ] `$meta` gesetzt: title, description (+ og_image falls abweichend) - [ ] `$meta` gesetzt: title, description (+ og_image falls abweichend)
- [ ] Bilder: `srcset`/`sizes`, `width`/`height`, `loading="lazy"` (above-the-fold: eager + fetchpriority) - [ ] Bilder: `srcset`/`sizes`, `width`/`height`, `loading="lazy"` (above-the-fold: eager + fetchpriority).
Responsive `widths`-Varianten mit `php bin/img-resize.php` erzeugen (nicht von Hand skalieren)
- [ ] Keine Duplikate: Inhalte/Werte aus den Single-Source-Dateien beziehen - [ ] Keine Duplikate: Inhalte/Werte aus den Single-Source-Dateien beziehen
- [ ] `php -l` sauber; Seite lokal geprüft - [ ] `php -l` sauber; Seite lokal geprüft

View File

@@ -9,6 +9,11 @@ declare(strict_types=1);
* align-Varianten: 'center' (mittig zentriert), 'bottom-left' (Default). * align-Varianten: 'center' (mittig zentriert), 'bottom-left' (Default).
* compact: true → niedrigere Variante für Unterseiten-Header. * compact: true → niedrigere Variante für Unterseiten-Header.
* display: true → großer Editorial-Look (Riesen-Titel, Outline-Tagline, Kicker, Scroll-Cue). * display: true → großer Editorial-Look (Riesen-Titel, Outline-Tagline, Kicker, Scroll-Cue).
*
* KONVENTION (siehe CLAUDE.md → Hero-Regel): display + kicker + tagline + video sind der
* Startseite vorbehalten. Unterseiten-Heros sind einheitlich „Überschrift + Teaser":
* nur title + text, dazu compact: true und align: 'bottom-left'. Kein kicker/tagline,
* ctas nur als Ausnahme.
*/ */
$align = in_array($hero['align'] ?? '', ['center', 'bottom-left'], true) ? $hero['align'] : 'bottom-left'; $align = in_array($hero['align'] ?? '', ['center', 'bottom-left'], true) ? $hero['align'] : 'bottom-left';
$compact = !empty($hero['compact']); $compact = !empty($hero['compact']);