From 2f8619e78d06297932887b9aa29062b10a6e4455 Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 31 Jul 2026 15:40:59 +0200 Subject: [PATCH] Gedruckte Adressen aus club.website statt base_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QR-Codes und Adresszeilen trugen `http://localhost:8000` ins Druckmaterial, weil sie über abs_url() liefen. Nach dem Produktivgang hätten alle zehn Codes neu erzeugt werden müssen — und ein gedruckter Code mit localhost darin ist Altpapier. Die öffentliche Domain ist Vereinsidentität, keine Umgebungseinstellung: neues Feld `club.website` in data/club.json plus club_url()/club_domain(). base_url bleibt umgebungsabhängig, weil Canonical, OG, Sitemap und JSON-LD zur ausliefernden Adresse passen müssen. Umgestellt: Cover-QR und die acht geteilten CTA-QRs, die Internet-Zeile im Heft-Impressum, die Cover-Zeile neben dem QR, die acht Linkzeilen unter den CTA-QRs der Live-Seiten (die hatte ich beim ersten Durchgang übersehen), das Web-Feld im Website-Impressum und bin/qr.php. Nebenbei: club_domain() ohne Pfad hängte einen Wurzel-Slash an („tsv08kulmbach.de/"), und der Kopf-Kommentar von stadionzeitung-add.php beschrieb noch die alten vier Anzeigen-Gruppen. Nicht verifizierbar und deshalb dokumentiert: die vendorte QR-Bibliothek arbeitet nicht reproduzierbar (gleiche URL, andere Pixel — anderes Maskenmuster, beides gültig). Ein PNG-Vergleich beweist nichts, nachweisbar ist nur der Code-Pfad. Jeden Druck-QR einmal scannen. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7 --- CLAUDE.md | 28 +++- app/components/stadionzeitung-cover.php | 3 +- app/components/stadionzeitung-ergebnisse.php | 2 +- app/components/stadionzeitung-historie.php | 2 +- app/components/stadionzeitung-impressum.php | 4 +- app/components/stadionzeitung-kontakt.php | 2 +- app/components/stadionzeitung-mitglied.php | 2 +- app/components/stadionzeitung-news.php | 2 +- app/components/stadionzeitung-partner.php | 2 +- app/components/stadionzeitung-sportheim.php | 2 +- app/components/stadionzeitung-termine.php | 2 +- app/helpers.php | 34 +++++ app/pages/impressum.php | 5 +- bin/qr.php | 22 +-- bin/stadionzeitung-add.php | 37 +++-- data/club.json | 137 +++++++++--------- data/stadionzeitung.json | 2 +- .../2026-08-01-sv-hutschdorf/qr.png | Bin 504 -> 579 bytes .../assets/img/stadionzeitung/qr-historie.png | Bin 431 -> 425 bytes .../img/stadionzeitung/qr-instagram.png | Bin 419 -> 430 bytes .../img/stadionzeitung/qr-matchcenter.png | Bin 428 -> 431 bytes .../img/stadionzeitung/qr-mitglied-werden.png | Bin 432 -> 503 bytes .../img/stadionzeitung/qr-mitmachen.png | Bin 434 -> 421 bytes public/assets/img/stadionzeitung/qr-news.png | Bin 417 -> 427 bytes .../img/stadionzeitung/qr-partner-werden.png | Bin 429 -> 503 bytes .../stadionzeitung/qr-sportheimbuchung.png | Bin 434 -> 505 bytes .../assets/img/stadionzeitung/qr-termine.png | Bin 430 -> 426 bytes 27 files changed, 173 insertions(+), 115 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a4e1724..8b4eb64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,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, Slogan) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren | +| Vereinsdaten (Name, Adresse, Kontakt, Social, Geo, Slogan, **Web-Adresse**) | `data/club.json` | Adresse/E-Mail irgendwo als Text duplizieren. `club.website` ist die öffentliche Domain für alles Gedruckte (`club_url()`/`club_domain()`) — **nicht** `config('base_url')`, die ist umgebungsabhängig | | 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` | @@ -100,7 +100,10 @@ Funktion, sie ist die einzige Stelle, an der aus JSON HTML 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), **`club_url()`/`club_domain()`** (absolute + URL bzw. Domain ohne Schema auf der ÖFFENTLICHEN Vereinsadresse `club.website` — für alles, was + gedruckt wird oder als Adresse dasteht; **nie `abs_url()` dafür**, siehe „Gedruckte Adressen" unten), + `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), @@ -611,14 +614,23 @@ unreparierbar auf jedem Gerät, das ihn einmal gesehen hat) · `Cache-Control: n `exit` statt `return` (sonst hängt `layout.php` einen HTML-Body an die Weiterleitung). Nicht in der Sitemap: es ist eine Weiterleitung, keine Seite. +**Gedruckte Adressen: `club.website` statt `base_url`** (seit 31.07.2026). `base_url` ist bewusst +umgebungsabhängig, weil Canonical, OG, Sitemap und JSON-LD zur ausliefernden Adresse passen müssen. +**Gedrucktes kennt kein „lokal":** QR-Codes, die Internet-Zeile im Heft-Impressum, die Cover-Zeile +neben dem QR und die Linkzeilen unter den CTA-QRs der Live-Seiten laufen deshalb über +`club_url()`/`club_domain()` (Quelle: `club.website` in `data/club.json`). Vorher trugen sie +`localhost:8000` ins Druckmaterial und hätten nach dem Produktivgang alle neu erzeugt werden müssen. +Die Domain ist Vereinsidentität, keine Umgebungseinstellung — deshalb `club.json` und nicht `config`. +Fehlt das Feld, fällt `club_url()` auf `abs_url()` zurück (nie ein kaputter Link). + **QR-Codes für Drucksachen: `php bin/qr.php [ziel.png] [--scale=24]`** (Default nach `storage/qr/`, gitignored). Getrennt von den Stadionzeitungs-Codes, die -`bin/stadionzeitung-add.php` selbst klein für die A5-Seiten erzeugt. **Die Warnung ist der Zweck des -Skripts:** ein QR-Code hat die URL fest eingebacken, und gegen die Dev-Config erzeugt steht -`http://localhost:8000/…` darin — das gedruckte Plakat ist dann Altpapier. Interne Pfade laufen -über `abs_url()` (Single Source `base_url`), und ist die nicht https, sagt das Skript es laut. Solange -die Seite nicht live ist, die vollständige Produktions-URL direkt übergeben. **Jeden Druck-QR vor der -Freigabe einmal mit dem Handy scannen** — im Repo gibt es keinen Decoder, der das prüfen könnte. +`bin/stadionzeitung-add.php` selbst klein für die A5-Seiten erzeugt; interne Pfade laufen über +`club_url()`, sind also unabhängig von der Umgebung schon richtig. +**Jeden Druck-QR vor der Freigabe einmal mit dem Handy scannen.** Das ist nicht Vorsicht, sondern die +einzige Prüfmöglichkeit: die vendorte Bibliothek arbeitet **nicht reproduzierbar** (dieselbe URL +ergibt unterschiedliche Pixel — anderes Maskenmuster, beides gültig), ein Vergleich zweier PNGs +beweist deshalb nichts. Nachweisbar ist nur der Code-Pfad, nicht der Bildinhalt. **Live-Seiten wiederverwendbar:** `termine_build_lists()` und `news_build_feed()` (`app/helpers.php`) sind die einzige Merge-Logik für Termine bzw. News — `/termine`, `/news` UND die diff --git a/app/components/stadionzeitung-cover.php b/app/components/stadionzeitung-cover.php index 13b9bca..500c744 100644 --- a/app/components/stadionzeitung-cover.php +++ b/app/components/stadionzeitung-cover.php @@ -67,7 +67,8 @@ if ($match !== null) { $opponentCrest = $match['is_home'] ? $match['away_crest'] : $match['home_crest']; } -$linkText = preg_replace('~^https?://(www\.)?~', '', rtrim((string) config('base_url'), '/')); +// Gedruckte Zeile neben dem QR-Code — immer die echte Domain (club_domain()). +$linkText = club_domain(); $kicker = trim($season . ($date !== '' ? ' · ' . german_date($date) : '')); // Zweite Coverzeile (Magazin-Praxis: Teaser auf die Geschichte im Heft) — diff --git a/app/components/stadionzeitung-ergebnisse.php b/app/components/stadionzeitung-ergebnisse.php index e51ec19..174f325 100644 --- a/app/components/stadionzeitung-ergebnisse.php +++ b/app/components/stadionzeitung-ergebnisse.php @@ -25,7 +25,7 @@ $groups = $groups ?? []; $matchcenterQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-matchcenter.png') ? 'img/stadionzeitung/qr-matchcenter.png' : null; -$matchcenterLink = preg_replace('~^https?://(www\.)?~', '', abs_url('matchcenter')); +$matchcenterLink = club_domain('matchcenter'); ?>
diff --git a/app/components/stadionzeitung-historie.php b/app/components/stadionzeitung-historie.php index 6298d70..6e06ab4 100644 --- a/app/components/stadionzeitung-historie.php +++ b/app/components/stadionzeitung-historie.php @@ -17,7 +17,7 @@ $emailHistorie = (string) (json_load('club')['email_historie'] ?? ''); $historieQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-historie.png') ? 'img/stadionzeitung/qr-historie.png' : null; -$historieLink = preg_replace('~^https?://(www\.)?~', '', abs_url('historie')); +$historieLink = club_domain('historie'); ?>
diff --git a/app/components/stadionzeitung-impressum.php b/app/components/stadionzeitung-impressum.php index 05c53a5..bd0b1e3 100644 --- a/app/components/stadionzeitung-impressum.php +++ b/app/components/stadionzeitung-impressum.php @@ -15,7 +15,9 @@ $club = json_load('club'); $legalName = (string) ($club['legal_name'] ?? $club['name'] ?? ''); $address = $club['address'] ?? []; $email = (string) ($club['email'] ?? ''); -$domain = preg_replace('~^https?://(www\.)?~', '', rtrim((string) config('base_url'), '/')); +// club_domain() statt base_url: das Heft wird gedruckt, da darf nie +// „localhost:8000" stehen. +$domain = club_domain(); $board = $club['board']['members'] ?? []; $boardNote = (string) ($club['board']['note'] ?? ''); ?> diff --git a/app/components/stadionzeitung-kontakt.php b/app/components/stadionzeitung-kontakt.php index 2c02c52..985c2f1 100644 --- a/app/components/stadionzeitung-kontakt.php +++ b/app/components/stadionzeitung-kontakt.php @@ -23,7 +23,7 @@ $persons = array_values(array_filter( $mitmachenQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitmachen.png') ? 'img/stadionzeitung/qr-mitmachen.png' : null; -$mitmachenLink = preg_replace('~^https?://(www\.)?~', '', abs_url('mitmachen')); +$mitmachenLink = club_domain('mitmachen'); ?>
diff --git a/app/components/stadionzeitung-mitglied.php b/app/components/stadionzeitung-mitglied.php index 401d40c..ba17f92 100644 --- a/app/components/stadionzeitung-mitglied.php +++ b/app/components/stadionzeitung-mitglied.php @@ -18,7 +18,7 @@ declare(strict_types=1); $mitgliedQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitglied-werden.png') ? 'img/stadionzeitung/qr-mitglied-werden.png' : null; -$mitgliedLink = preg_replace('~^https?://(www\.)?~', '', abs_url('mitglied-werden')); +$mitgliedLink = club_domain('mitglied-werden'); ?>
diff --git a/app/components/stadionzeitung-news.php b/app/components/stadionzeitung-news.php index 5965953..8beddcf 100644 --- a/app/components/stadionzeitung-news.php +++ b/app/components/stadionzeitung-news.php @@ -27,7 +27,7 @@ $sourceLabel = static fn (string $source): string => match ($source) { $newsQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-news.png') ? 'img/stadionzeitung/qr-news.png' : null; -$newsLink = preg_replace('~^https?://(www\.)?~', '', abs_url('news')); +$newsLink = club_domain('news'); ?>
diff --git a/app/components/stadionzeitung-partner.php b/app/components/stadionzeitung-partner.php index 7ba0f24..3bb5a79 100644 --- a/app/components/stadionzeitung-partner.php +++ b/app/components/stadionzeitung-partner.php @@ -24,7 +24,7 @@ foreach (json_load('club')['board']['members'] ?? [] as $member) { $partnerQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-partner-werden.png') ? 'img/stadionzeitung/qr-partner-werden.png' : null; -$partnerLink = preg_replace('~^https?://(www\.)?~', '', abs_url('partner-werden')); +$partnerLink = club_domain('partner-werden'); ?>
diff --git a/app/components/stadionzeitung-sportheim.php b/app/components/stadionzeitung-sportheim.php index b02262f..eb2494c 100644 --- a/app/components/stadionzeitung-sportheim.php +++ b/app/components/stadionzeitung-sportheim.php @@ -18,7 +18,7 @@ $heroText = (string) (json_load('sportheimbuchung')['hero']['text'] ?? ''); $sportheimQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-sportheimbuchung.png') ? 'img/stadionzeitung/qr-sportheimbuchung.png' : null; -$sportheimLink = preg_replace('~^https?://(www\.)?~', '', abs_url('sportheimbuchung')); +$sportheimLink = club_domain('sportheimbuchung'); ?> diff --git a/app/components/stadionzeitung-termine.php b/app/components/stadionzeitung-termine.php index b59adcc..84a7997 100644 --- a/app/components/stadionzeitung-termine.php +++ b/app/components/stadionzeitung-termine.php @@ -27,7 +27,7 @@ $heroImage = json_load('home')['hero']['image'] ?? null; $termineQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-termine.png') ? 'img/stadionzeitung/qr-termine.png' : null; -$termineLink = preg_replace('~^https?://(www\.)?~', '', abs_url('termine')); +$termineLink = club_domain('termine'); ?>
diff --git a/app/helpers.php b/app/helpers.php index 38cc288..ca68cc7 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -77,6 +77,40 @@ function abs_url(string $slug = ''): string return rtrim((string) config('base_url'), '/') . url($slug); } +/** + * Absolute URL auf der ÖFFENTLICHEN Vereinsadresse (`club.website`), unabhängig + * davon, wo dieser Code gerade läuft. + * + * Nicht dasselbe wie abs_url(): `base_url` ist bewusst umgebungsabhängig, weil + * Canonical, OG und Sitemap zur ausliefernden Adresse passen müssen. Gedruckte + * Dinge kennen dagegen kein „lokal" — ein QR-Code auf Papier und die + * Internet-Zeile im Heft-Impressum zeigen immer auf die echte Domain. Vorher + * liefen sie über base_url und trugen deshalb `http://localhost:8000` ins + * Druckmaterial. + * + * Fällt `club.website` weg, greift abs_url() — ein fehlendes Feld darf keinen + * kaputten Link erzeugen. + */ +function club_url(string $slug = ''): string +{ + $website = (string) (json_load('club')['website'] ?? ''); + + return $website !== '' ? rtrim($website, '/') . url($slug) : abs_url($slug); +} + +/** + * Die öffentliche Adresse ohne Schema und ohne „www", zum Hinschreiben + * (Cover, Impressum, Plakate): `tsv08kulmbach.de/stadionzeitung`. + */ +function club_domain(string $slug = ''): string +{ + $ohneSchema = (string) preg_replace('~^https?://(www\.)?~', '', club_url($slug)); + + // Ohne Pfad soll „tsv08kulmbach.de" stehen, nicht „tsv08kulmbach.de/" — + // url('') liefert den Wurzel-Slash, der hier nur Lärm wäre. + return $slug === '' ? rtrim($ohneSchema, '/') : $ohneSchema; +} + /** * Asset-Pfad mit Cache-Busting über filemtime: asset('css/tokens.css'). * diff --git a/app/pages/impressum.php b/app/pages/impressum.php index fcec53f..ca39664 100644 --- a/app/pages/impressum.php +++ b/app/pages/impressum.php @@ -38,7 +38,10 @@ $meta = [

E-Mail:
- Web: + + Web:
diff --git a/bin/qr.php b/bin/qr.php index 07caa35..6b9efc5 100644 --- a/bin/qr.php +++ b/bin/qr.php @@ -18,11 +18,11 @@ declare(strict_types=1); * Plakate und Aushänge, die in Canva zusammengebaut werden — dort braucht es * ein Vielfaches an Pixeln, sonst franst der Code im Druck aus. * - * DIE EIGENTLICHE AUFGABE DIESES SKRIPTS IST DIE WARNUNG: ein QR-Code enthält - * die URL fest eingebacken. Wird er gegen die Entwicklungs-Config erzeugt, steht - * `http://localhost:8000/…` darin und das gedruckte Plakat ist Altpapier. Ein - * interner Pfad läuft deshalb durch abs_url() (Single Source: config base_url), - * und wenn die nicht auf https zeigt, sagt das Skript es laut. + * Ein QR-Code hat die URL fest eingebacken — gegen die Entwicklungs-Config erzeugt + * stünde `http://localhost:8000/…` darin und das gedruckte Plakat wäre Altpapier. + * Deshalb läuft ein interner Pfad durch club_url(): die öffentliche Vereinsadresse + * aus data/club.json, unabhängig von der Umgebung. Der Code stimmt damit schon vor + * dem Produktivgang und muss danach nicht neu erzeugt werden. */ if (PHP_SAPI !== 'cli') { exit(1); @@ -45,7 +45,7 @@ if ($args === []) { $target = $args[0]; $isAbsolute = str_starts_with($target, 'http://') || str_starts_with($target, 'https://'); -$url = $isAbsolute ? $target : abs_url(trim($target, '/')); +$url = $isAbsolute ? $target : club_url(trim($target, '/')); // Zielpfad: ohne Angabe neben das Skript-Verzeichnis in storage/qr/ ablegen — // Druckmaterial ist kein Website-Asset und hat unter public/ nichts zu suchen. @@ -59,11 +59,11 @@ if ($dest === null) { $dest = $dir . '/qr-' . trim($name, '-') . '.png'; } -if (!$isAbsolute && !str_starts_with((string) config('base_url'), 'https://')) { - fwrite(STDERR, "WARNUNG base_url ist '" . (string) config('base_url') . "' — dieser QR-Code führt NICHT auf die echte Seite\n"); - fwrite(STDERR, " und ist als Druckvorlage unbrauchbar. Entweder die Produktions-Config verwenden\n"); - fwrite(STDERR, " oder die vollständige URL direkt übergeben:\n"); - fwrite(STDERR, " php bin/qr.php https://tsv08kulmbach.de/{$target}\n\n"); +// Der Riegel bleibt, nur zeigt er jetzt auf das richtige Feld: ohne saubere +// club.website wäre der gedruckte Code wieder unbrauchbar. +if (!$isAbsolute && !str_starts_with($url, 'https://')) { + fwrite(STDERR, "WARNUNG club.website in data/club.json ist nicht https ('{$url}') — dieser QR-Code\n"); + fwrite(STDERR, " führt nicht auf die echte Seite und ist als Druckvorlage unbrauchbar.\n\n"); } if (!generate_qr_png($url, $dest, $scale)) { diff --git a/bin/stadionzeitung-add.php b/bin/stadionzeitung-add.php index 4ccd4c2..9311efb 100644 --- a/bin/stadionzeitung-add.php +++ b/bin/stadionzeitung-add.php @@ -19,18 +19,20 @@ declare(strict_types=1); * /spieler.* optional, freigestelltes Spielerfoto (PNG mit Transparenz) * fürs Cover — wird unverändert kopiert, nie über GD neu kodiert * /verfasser.* optional, Porträtfoto für die Vorwort-Signatur - * /anzeigen/* Sponsoren-Seiten (fertige Vollseiten — ob im Sponsoring - * halbe oder ganze Seite verkauft wurde, ist hier egal, - * geliefert wird immer eine fertige Seite), natürlich sortiert * - * Feste Vorlage (Interview-Beschluss 29.07.2026): Cover (live) → Vorwort → - * Ergebnis-Rückblick (live) → Anzeigen A → je Mannschaft eine Tabelle (live, - * Reihenfolge = data/matchcenter.json) → Vorschau (live) → Anzeigen B → - * Termine (live) → Anzeigen C → News (live) → Ansprechpartner (live) → - * Anzeigen D → Rückseite. Das Cover wird automatisch generiert (siehe - * app/components/stadionzeitung-cover.php) — kein Canva-Bild mehr nötig. Der - * gesamte Anzeigen-Pool (in gelieferter Reihenfolge) wird zu möglichst - * gleichen Teilen auf die vier Gruppen verteilt. + * KEINE Anzeigen im Ordner (seit 31.07.2026): die kommen aus dem dauerhaften + * Bestand data/anzeigen.json, gepflegt mit bin/anzeige-add.php. Sponsorenmotive + * ändern sich selten, das Heft erscheint oft — vorher lag jede Anzeige als Kopie + * unter jeder Ausgabe und ihr Alt-Text musste jedes Mal neu geschrieben werden. + * + * Feste Vorlage (Interview-Beschluss 29.07.2026, Anzeigen 31.07.2026 überarbeitet): + * Der Inhalt ist eine Liste von BLÖCKEN, die Anzeigen werden gleichmäßig in die + * Lücken dazwischen verteilt — nie mehr als zwei hintereinander (vorher vier + * Blöcke à fünf, das las sich wie „Heft zu Ende"). Anzeigenfrei bleiben: nach dem + * Cover, im Aufschlag Vorwort+Interview, zwischen den Tabellenseiten und zwischen + * Impressum und Rückseite. Die Doppelseiten-Regel (erste Tabelle auf gerader + * Seite) rechnet das Skript selbst und verschiebt dafür eine Anzeige. + * Das Cover wird automatisch generiert (app/components/stadionzeitung-cover.php). * * Neue Ausgaben entstehen als ENTWURF (status: entwurf): unsichtbar in * Archiv/Sitemap, Live-Seiten rechnen bei jedem Aufruf frisch. Erst @@ -500,10 +502,13 @@ if ($spieler !== null) { } // --- QR-Code zur digitalen Ausgabe fürs Cover: einmalig, Ziel-URL ändert sich nie. +// club_url() statt abs_url(): der Code landet auf Papier und muss auf die echte +// Domain zeigen, auch wenn er hier lokal erzeugt wird (vorher stand +// http://localhost:8000 darin). // QR ist "nice to have" — schlägt die Erzeugung fehl, läuft die Ausgabe ohne weiter. --- $qrImage = null; $qrDest = "{$destDir}/qr.png"; -if (generate_qr_png(abs_url('stadionzeitung/' . $slug), $qrDest)) { +if (generate_qr_png(club_url('stadionzeitung/' . $slug), $qrDest)) { $qrImage = "img/stadionzeitung/{$slug}/qr.png"; $log('QR-Code erzeugt.'); } else { @@ -511,11 +516,11 @@ if (generate_qr_png(abs_url('stadionzeitung/' . $slug), $qrDest)) { } // Geteilte QRs der CTA-Füße (Kontakt → /mitmachen, News → /news, Termine → -// /termine, Vorschau → /matchcenter, für alle Ausgaben gleich) — werden bei -// jedem Lauf mit der aktuellen base_url neu geschrieben, damit sie nach dem -// Produktivgang automatisch auf die echte Domain zeigen. +// /termine, Vorschau → /matchcenter, für alle Ausgaben gleich) — ebenfalls über +// club_url(), damit sie unabhängig von der Umgebung stimmen und nach dem +// Produktivgang nicht neu erzeugt werden müssen. foreach (['mitmachen', 'news', 'termine', 'matchcenter', 'mitglied-werden', 'historie', 'sportheimbuchung', 'partner-werden'] as $ctaTarget) { - if (generate_qr_png(abs_url($ctaTarget), PUBLIC_PATH . "/assets/img/stadionzeitung/qr-{$ctaTarget}.png")) { + if (generate_qr_png(club_url($ctaTarget), PUBLIC_PATH . "/assets/img/stadionzeitung/qr-{$ctaTarget}.png")) { $log("QR-Code für /{$ctaTarget} erzeugt."); } else { $log("QR-Code für /{$ctaTarget} konnte nicht erzeugt werden — die Seite rendert ohne QR.", 'WARN'); diff --git a/data/club.json b/data/club.json index 2955a11..80cebda 100644 --- a/data/club.json +++ b/data/club.json @@ -1,71 +1,72 @@ { - "name": "TSV 08 Kulmbach", - "legal_name": "TSV 1908 Kulmbach e. V.", - "founded": "1908", - "description": "Wir sind dein Sportverein für Fußball und Turnen in Kulmbach: gegründet 1908, heute zuhause im Katzbachtal, mit Jugend-, Herren- und Frauenfußball, Turnen und Trampolin.", - "slogan": "Gemeinsam. Immer weiter.", - "departments": [ - "Fußball", - "Turnen", - "Trampolin" - ], - "address": { - "venue": "Hans-Rausch-Sportanlage", - "street": "Thurnauer Str. 51", - "zip": "95326", - "city": "Kulmbach", - "country": "DE" - }, - "geo": { - "lat": 50.095, - "lng": 11.4372 - }, - "email": "info@tsv08kulmbach.de", - "email_turnen": "turnen@tsv08kulmbach.de", - "email_historie": "historie@tsv08kulmbach.de", - "board": { - "note": "Vertretungsberechtigter Vorstand gemäß § 26 BGB", - "members": [ - { - "name": "Kai van Avondt", - "role": "1. Vorsitzender" - }, - { - "name": "Christian Ohnemüller", - "role": "2. Vorsitzender" - }, - { - "name": "Matthias Korzonek", - "role": "" - }, - { - "name": "Michael Senar", - "role": "" - } - ] - }, - "banking": [ - { - "bank": "Sparkasse Kulmbach-Kronach", - "iban": "DE11 7715 0000 0000 1542 52", - "bic": "BYLADEM1KUB" + "name": "TSV 08 Kulmbach", + "legal_name": "TSV 1908 Kulmbach e. V.", + "founded": "1908", + "description": "Wir sind dein Sportverein für Fußball und Turnen in Kulmbach: gegründet 1908, heute zuhause im Katzbachtal, mit Jugend-, Herren- und Frauenfußball, Turnen und Trampolin.", + "slogan": "Gemeinsam. Immer weiter.", + "website": "https://www.tsv08kulmbach.de", + "departments": [ + "Fußball", + "Turnen", + "Trampolin" + ], + "address": { + "venue": "Hans-Rausch-Sportanlage", + "street": "Thurnauer Str. 51", + "zip": "95326", + "city": "Kulmbach", + "country": "DE" }, - { - "bank": "VR Bank Oberfranken Mitte", - "iban": "DE90 7719 0000 0000 0362 18", - "bic": "GENODEF1KU1" + "geo": { + "lat": 50.095, + "lng": 11.4372 + }, + "email": "info@tsv08kulmbach.de", + "email_turnen": "turnen@tsv08kulmbach.de", + "email_historie": "historie@tsv08kulmbach.de", + "board": { + "note": "Vertretungsberechtigter Vorstand gemäß § 26 BGB", + "members": [ + { + "name": "Kai van Avondt", + "role": "1. Vorsitzender" + }, + { + "name": "Christian Ohnemüller", + "role": "2. Vorsitzender" + }, + { + "name": "Matthias Korzonek", + "role": "" + }, + { + "name": "Michael Senar", + "role": "" + } + ] + }, + "banking": [ + { + "bank": "Sparkasse Kulmbach-Kronach", + "iban": "DE11 7715 0000 0000 1542 52", + "bic": "BYLADEM1KUB" + }, + { + "bank": "VR Bank Oberfranken Mitte", + "iban": "DE90 7719 0000 0000 0362 18", + "bic": "GENODEF1KU1" + } + ], + "data_protection_contact": "Fabian Früh", + "webdesign": "Felix Schneider", + "memberships": [ + "Bayerischer Landes-Sportverband", + "Bayerischer Fußball-Verband", + "Bayerischer Turnverband" + ], + "social": { + "instagram": "https://www.instagram.com/tsv08kulmbach/", + "facebook": "https://www.facebook.com/tsv1908kulmbach/", + "youtube": null } - ], - "data_protection_contact": "Fabian Früh", - "webdesign": "Felix Schneider", - "memberships": [ - "Bayerischer Landes-Sportverband", - "Bayerischer Fußball-Verband", - "Bayerischer Turnverband" - ], - "social": { - "instagram": "https://www.instagram.com/tsv08kulmbach/", - "facebook": "https://www.facebook.com/tsv1908kulmbach/", - "youtube": null - } -} \ No newline at end of file +} diff --git a/data/stadionzeitung.json b/data/stadionzeitung.json index 0fb7a75..49b334e 100644 --- a/data/stadionzeitung.json +++ b/data/stadionzeitung.json @@ -1134,7 +1134,7 @@ } ] }, - "published_at": "2026-07-31T12:40:07+00:00" + "published_at": "2026-07-31T13:34:54+00:00" } ] } \ No newline at end of file diff --git a/public/assets/img/stadionzeitung/2026-08-01-sv-hutschdorf/qr.png b/public/assets/img/stadionzeitung/2026-08-01-sv-hutschdorf/qr.png index 063b4af04719987b50854d9be6cab4a662c54067..496f0456d41d28fb0bb9213c51cd00d653dff11d 100644 GIT binary patch delta 549 zcmV+=0^0rf1H%L%iBL{Q4GJ0x0000DNk~Le0002?0002?0RsR40N>o`i;*EJf8$9+ zK~z|U?V0P1#2^fWEl~g^(K;oi03@G1V<)+7U~Op;%4PYu9~K@zh{$hzcTBZ$;>Y5j zafz?$?!f(Vu+g4p>d1=2Y`U9^rT;&}&!Yah!#SxSdg_6Xm&C$vXo=Nx`GG6w|)j9Go zi#zV3i#=6pb)FJOPB=3%GZLLN;I0X|(^}dm!W7YgD?%wwX+>L{LT|xC%_vcAix(H( z;;)Ra)j2~c9ZL0e%N71rd!4hIW{H8zIjZE2ueJFt@hfv3Guv#!<9>Hh-{+S5-0%CB z!rUJ$MW3GjZC`l~vm&O_v*>Yp_2Nsjzj-weU3eyh^e-g3-GU3gZ)kocIPrZT`?KmC n`i@(QmEjg$rgq@J@y+oE@rGX)kS-;P00000NkvXXu0mjfl*2=lEuam*{*`4~qVnjw_rnR|Iy%3#+2hoS@_lPfBe z+xj2)(2x?WE!YwC^=E$yK@KL{f6^_SDSsbAuwEQm4rIL)%^6V$fta7?s%^Y0Lf9~B zg)!^ayHFL@Tem}aUt<_@unhRLhD+C#eiMbP8VzQ+`LO|(R;zfFpYBZ*f>+l= zhg(wxC1D(H!D)V5>w3`UF!LCX?}%f*H0Z(Ff>(cZ@UAb_mIo%>amX4;NPbP!E*KK3oEXiLj@8#UR(pVMfJr<+A@CS2)%u72@#`* zn==xrzNrvqNuT%kl|PkaSCZI{2*cF(DV5}8aO&Gu6qpg|B7Z{StT{ekaW=ToAb^O$ zgx8m$s|mDT?=llcRUz-1w{yi$Jk}Fh)L30o()kjE3i=7dxWz;<4H7D+dk04VgyilR z<%F)zV9ZW?Yh6#+F;G#mz_49BdPy}&HMzm`6b9}hWMa!!=K0;pBS8!)0*iWkTC|*O z4ROYCcxJJYAYl8I6BbRyId!TK7>{o+)ELy`Y;GEa(>j9D_Q>fcOH>H!F>Hy-r$z8C z#B7p^KP_5H=xsZr6SDd91y)G*RKmny3G|QrsU&~-MP3pR%i`}s`Dp+E002ovPDHLk FV1l6Asn7rb delta 365 zcmV-z0h0cy1Fr*+OMg#EL_t(oh3%J14#OY}L@jZE9Eq!Qq#PiU>zRRsw!3cZMJ*Ho zFAK;2nEmB9IXvV#yq!3?51f6}Ab4EXLE^15_Vcm=2_56)XK(Itj4!xKk;A!xUF(rh zyo;A2@HYJ1WAT-@I7lVRl5wxk{7EE|uqAWFz9f@`@#t`>FMpN&wskg%%;?ovFQxL+=dy`UO&Nyr_Ox;h zBJ~g7J8c~jxiWj2*>2HXV(YJ(u2z{TLS`ji<1otev}ii{%P;Z*v;Mm{{qaDl00000 LNkvXXu0mjfw#>6K diff --git a/public/assets/img/stadionzeitung/qr-instagram.png b/public/assets/img/stadionzeitung/qr-instagram.png index f5385083d8d1cce64d3f477617204a2feb040f66..28388aba3afb3a3095cbf0dc044de52005379341 100644 GIT binary patch delta 364 zcmV-y0h9it1Fi#*OMgyDL_t(oh3%Kit;8SIn+&d!LJPkf8?MT2$qc~*t= zc_;K!W2Vy3>1E_NyurzK{$Q3M`<8w43QwfKrJF{c5t3rPAjl2LOapz9Jl+kHT{q66#8~zH z1)_{p073iA>Q63#{lmqm}S?6n^PlC}_0czt>Xk||qaoXWSPB#?yEnjk;&(a1u8 zhSW90)1f$Rt($nAl(U$nD#v{s&O;)Kl@@j83KgUvseBH{OY?9mg@d%k1+41Hw(t-u zX*rI2y=fez;#%sb5SVfj`aXHDo;sB~O9)6hn6txs2OOlT;LO@CQl6^JLEtqc*+0By zAObx;f5KRZKu+|m)qsO^mY^Ky>X?Q6?H79hYBu;}*pOTN00000NkvXXu0mjfb?d53 diff --git a/public/assets/img/stadionzeitung/qr-matchcenter.png b/public/assets/img/stadionzeitung/qr-matchcenter.png index 920015189f7ee333fe86b90b4a027037191cc60f..902e795a1be903d048f37ba1d040e2fb43eca729 100644 GIT binary patch delta 365 zcmV-z0h0c#1Fr*+OMg#EL_t(oh3%J34#Xe`g-JYsNAl_($pe@$@W9TrvzrZXF~s!8 z&jnTBQIWs>W?jv8S9{ztwury$k~VbNkO;<`6%xx2xM_2CvwtOV{uBg9-8=v%93M>) zl`KRe`mDg9%;1%VG=*XycQ-bbgA54;gJPUA!$I(9dugd`zPs4=OKGc#xon3lBLz*P zFtd;bt?=aV!t7bXL*$(BN!%gFii!D)jr3vFMfF<3K(e|Zk5O92QQCUqInm!@>%yBm zT2ER8Amk#Nf@W*4i^)eYX71vzf`Xjx3F410AR-=gJNsfMf>k6W=RAF*<<#i!Xvox% z3$%DQuggjNPou31=^!C;Iy-@~Df&AOl39`kFaBR|`BO*!_KUp$BB;z5JFU>c00000 LNkvXXu0mjf>|L`b delta 362 zcmV-w0hRu*1FQp(OMgsBL_t(oh3%G04g?_xL`gh=NAl_($pe@$6eT@9pVe$&F~oG# z*9!s`Ao920tgG4X(>C^1ojB{PLGal!Z0WLP`*T@=n2&a9{34COa#iD{) zl`9atfN=fG))8K(li}LMhT%LysX(HyiVsln9%l)Hu})$TU{J4r^9frS`iAHE_71X% zUAs6o%JY2fW@FhT9QO`3SO}|-Gd&&OF|%J&XFi#HjA*l*J$uya0z~y~Jkb;7BE+|w za(VXPA_O&$gQtkxBbrN?Z9YSepJm8VNmLiF%`rVClfV6955gG1jIX6LQUCw|07*qo IM6N<$g13{dlmGw# diff --git a/public/assets/img/stadionzeitung/qr-mitglied-werden.png b/public/assets/img/stadionzeitung/qr-mitglied-werden.png index 67d2c827c0cf26f28eed17f8f59b494c44e8bc6a..0184bb3421c7a7a78cc908eba434e24102bd4438 100644 GIT binary patch delta 473 zcmV;~0Ve*i1NQ?ViBL{Q4GJ0x0000DNk~Le0002q0002q0RsR40BET!^pPPdf0s!_ zK~z|U?U+%H!yphtE#&|?5?AL)IRFwnwjpWzU&$*KMQzICBSSrACctleZ=AEnIC<&5 z&kLNIJreSx$Z@s0A7}-~o)3ie)HQcpD1I=kuE>HRL#crJ|A0^s;NELhE~HGrUEfPS zHIxLMg8lXS%+DOSeM~#8EIgm-e>hkVe$gbR)G?WbR!z@@Lr+~=uE8DW!nQ>n2cZZc zOI8ZDTCqb(gA$1NU?}Bq{3~_}hCv=0&E(LQmuEoUoCswY<_rruK&0_Zc=({xmfJIK zZwrKrNE+OAw4s<1A$$n)mkUXrc(G8ab}UghtlH;slPm}tn`^YW>tA^We=?!V-btUZ zsBL&X5`xWyLa`Y;gj+*daOEFX*}J^?E+@mF6)j7C6Zp)8ZArs6tY%2Ifkf!?T+hC0 z2Q~3`Ihh5q)a!G@jpkEfq`}<=S;!1%KD|XAOt`51nyYh&JM(5M6j14a6|aJ6`5-=YW_DPBI8m)sXzBqNut8;8)lz_oDBbDvA~1uFlK-I&8mmZ+KYFq-M!;G-X#bgAR~TTe9k?oK*C3SS(}`m6^jre@fs81wIG4Z z$a2J0iojE|LSFn?2S>?L$u++8bB?4{4}X2MX-_*zDkfr-(SKNsmT{FJ%yS`RbWxa@ z?enDeEn+tghSnrVc)eq3JU8y^IZbk3p*|2-&ZN@waY~OM?koJ2e9wkd9Y}nIZ8S=MZ>;;8N(aJjd2~7Y1002ovPDHLkV1jv! BtU>?) delta 368 zcmV-$0gwKr1F{2)O^Z)$YruWfGIQ2I^zswSPS@^0Nj}N=AcWG9v5| z?R=v2r9pH)3ilRRfuJ~;kMt`hMySpw7H1U7ot9(Y*_li{4eb(+HG3VIm`={P@VTM< zM2iq;RL%|5ihtOalpsjD0kKeul(dH`M2`OuZPrSQ3eG2Rp?au;M3tly=3qNZ@$l(U zh77DrTvEL4-D#aBpB%q>|3g0Z?+qo9#P)PXF1OAWb1s`epgR@XeFb=)e^QB`a@t)e zGb*?Sp*#H?*7Wjd8Nzk=jNIqhER_h$S9k@K8pNway|pmlvkLj!FZKbh%(e3F)v_Z1 O0000W6Kyq5Gfg$r4GvjQ=V>(YkzFmCHri{NeEh46aBoT^j& z59$zC>c&qVd5}u>#(sY4CzH4tpDdK?yO&QW)4YgK6GpSCL4S_OMqjdAMhPOF_0e7F zy1O}_phVzT^gOn}867&GFbBP_2pbIlqXwZ_oDpjKgkA7fE2)I%SV&eCdUXF!I>DZo z2qTZLBHHOh_6ddsQu3(IC(J|ye8#C80WU#@_hX+r#RsT}c0QqAcod;#q3k@!CudxW zI*y_#KqRtGu40%|IJ49sam(o(?nc`cQ-eSt4m7*yXe6VQA!kCzEI{o#YY{&#cunj( zM~jf*dp?9dRHqW*Ko`e_-lnH~!YbjO31>b&>L-)@?Tvi^vc&wHdcDy600000NkvXX Hu0mjf6=kZG delta 351 zcmV-l0igb?1EB+uOMgL0L_t(oh3%F>jzl2{L`gh=NAl_($pe@$P{voS7EL(-fAe`C>`DOgBOPlEaGncO)D&SD~lm7@ic5GZwvK($^k1`EM7H{cj)*>@-i9txkK x&NwhhNZi4w^FHGM2}zekSH6s0PrmU<`~W%i8U3SB<8=T4002ovPDHLkV1me#rXK(R diff --git a/public/assets/img/stadionzeitung/qr-partner-werden.png b/public/assets/img/stadionzeitung/qr-partner-werden.png index acfff66b6404f0029ee81ca955ea4ab3e8b40f5c..941103bbf1163edede03eabc94fc6574fa74ed61 100644 GIT binary patch delta 473 zcmV;~0Ve*f1NQ?ViBL{Q4GJ0x0000DNk~Le0002q0002q0RsR40BET!^pPPdf0s!_ zK~z|U?U-Sz10f8A10KL5d3BGp2Uy6<%&6Pa--UV5uvqKok4-v>u@OJ}p7kud`^+ma zmR*4{rzOO2uQ6+EwQH{%!g}jFMiDwOYo+G5Gvu@^ukjDy)^JU@*B3${rH;&cB1V2_ z$O*0#e0@Ih(*~}PV_GZiiX$0Ee~7|1q-FRNqcJ1fDsOHuRn@>tBZ;b9^CfCO$m611 zNtFtC27#Nx#;8*k6>rG7R!T39kkz7ARObj$Ib7kOD-cU*xwgoUG==5LVDiCOk7Lzi zLr76;7hfNqp{MYKvAPDG6)YZuKY}HsK&WD-9c02SPwjJpi1A?Zsat3Nf6KWg4A!$a z@y4O?oXl=euITibx>YnB)RvIS)u{s^@Dpaf2%bNz|rkV>rT}earp;$5;WC1r$cI P00000NkvXXu0mjf@2%e3 delta 398 zcmV;90dfBK1FZugiBL{Q4GJ0x0000DNk~Le0002S0002S0RsR40JBwX9+4p`e@#h5 zK~z|U?Uz9ggfIw2Nj!i@>eW4x2WZ0Z|EJEF*|~v*q%qa!q5>b#?6qgMd)VBMX~R9P zhI>DH5L{;Pb4=~m!w!Up<&p6PxXxB&_wD5Vw049WhWhV&N{J#IPF#*?E333sZ;A|)fvh`Bc*mNhu;Gzd(*YfT8+ z7b^;L5GAk=kzUo%95v=ra5do+kcAmihG?sjJ>p`Rv=kfVpxH@e;dF_dP0JdYEQjFrxUH||907*qoM6N<$f|j1JtpET3 diff --git a/public/assets/img/stadionzeitung/qr-sportheimbuchung.png b/public/assets/img/stadionzeitung/qr-sportheimbuchung.png index 1bac55d27730f24e95cd8ac1878ce913fb8e690d..f44d290cd462a90fc18315e582160fd22a4a8366 100644 GIT binary patch delta 475 zcmV<10VMvi1Nj3XiBL{Q4GJ0x0000DNk~Le0002q0002q0RsR40BET!^pPPdf0;={ zK~z|U?U>Px#2^erEl~g^(K;nj0Fuu$1G&Au@0I3bQ52CpK4N3Wb|SvxbE9XC%}K=A zF=~oeIV~Y>yJxGXK5Me;e%~9y`sxjX>RudXX^AsztNg9`bcYd*B)G~MB1~eG=i3^& zM*NE*C&(k%=j-)$K8>ITKVM;ze=FXG5qtz=LE8}8oM?$1EDKgBKw7zbfnzs3;Zfhp z7>H|{1y#0%5{ZVxmsjVmVAw&0$)w9+knCvb2`eEZC+|+L#|fGAghz=nJy{NgPuz$% zgZ0WrT^gVumZ!r5cS_K3ic2RhaYqNsLq3qkBRne~pQ*;>hq| zWnl`X5JgZ(GKWY)93kH|_HCQ{(5@1DLg`sMmCj(V`73X3klHX<^G-?ylCvv3RYTm? zhsu%1@ow-57TA%erBE6%g!k8o5=fl>_$epdAe{}7m{5*m%3DKh1l7JPuAv;SF=Pr^ zr-%Bs(eke;b}&*kuX2cfF_T!rc&UF+_Rud$N5~_{GsnhV6%f*$Bm9m}jUPx?Jlhs{ R@vi^?002ovPDHLkV1g@|)wTct delta 403 zcmV;E0c`&H1F{1liBL{Q4GJ0x0000DNk~Le0002S0002S0RsR40JBwX9+4p`e^NdQg*p0=A2I!qdp8G$PT??e!(2&+#w*V2WsOV}U#zy7Ht zY5rzQ#*#l)My#oCY_#e`P z=-@6oaR){V*DVOP5K%K^iEO_nUp;a6QdS}k4TNt^2;~V+Gepfs$que2Y>qQv*74P7Fv5gu8-Rj0RHd$w~0@*$;d}_1NrmreR%qF6{hvkSUMrTzW zBA#KGdcvg&p-K?QXkYrtByMyT6aAUXCr7~zo@$%AGml+afq%Gg;jis!n;4cMaM>_Y z%H_6f(nok|##Q_9(=r5S9#!kd9>aMQoJ@Gfw&^(!H8-n5wwnfV$R@rYBj@R)d!8f1 zi=uh%l3XIO4gGZS=$`+{CfJGU_lDUx{U}0k471+c*+%t5-)j(5b7-8#t~}Rl1tL7~ z{QhruMAJ!l7-256zB9Qjlblx$vpt$uomB`UdV<=n0;&RuY8_f}1>5}st3ap^>Uip^ zMJVDKZ|hT!<`a2dInw;r{eA!B5UM0z)#m-JpG@+%H}(PfWw_=>haO%40000vm#(aK%>Zg)Okb{vf|CdTaH`QHYsg$9%Ab%O;we#_cFveWbt|vT5 znxSPrszjQQ?Y$a|p@pwLYAV55pasET zf@I|CYJMl$)g)67M;P!|*M-^XF4bgvjqF2rQ6t>i&rD3D5nvh%fnBpA2^Prxv zWFwS)sF-=C^<`>e+kok5Au9!2kp0SG&s%DH@pR>C0tY#sXdIr;7}bG1o^n)Sw7Zd^ z>B-*j;0^?4%>sH)G$E#e4kzk-AbJ}@XH~U)hh67Rt0!OiM1BC1Aj_;10Q=1V0000< KMNUMnLSTa9=CF?d