Gedruckte Adressen aus club.website statt base_url
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
28
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 |
|
| 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 |
|
| Seiten-Slugs & Routing | `app/routes.php` | URLs woanders hart verdrahten |
|
||||||
| Navigation (Labels/Reihenfolge) | `data/navigation.json` | Menüpunkte in Templates |
|
| 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` |
|
| 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 |
|
||||||
| 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` |
|
| 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` |
|
||||||
@@ -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
|
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()` (interne
|
- **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,
|
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`
|
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),
|
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).
|
`exit` statt `return` (sonst hängt `layout.php` einen HTML-Body an die Weiterleitung).
|
||||||
Nicht in der Sitemap: es ist eine Weiterleitung, keine Seite.
|
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 <pfad|url> [ziel.png] [--scale=24]`** (Default nach
|
**QR-Codes für Drucksachen: `php bin/qr.php <pfad|url> [ziel.png] [--scale=24]`** (Default nach
|
||||||
`storage/qr/`, gitignored). Getrennt von den Stadionzeitungs-Codes, die
|
`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
|
`bin/stadionzeitung-add.php` selbst klein für die A5-Seiten erzeugt; interne Pfade laufen über
|
||||||
Skripts:** ein QR-Code hat die URL fest eingebacken, und gegen die Dev-Config erzeugt steht
|
`club_url()`, sind also unabhängig von der Umgebung schon richtig.
|
||||||
`http://localhost:8000/…` darin — das gedruckte Plakat ist dann Altpapier. Interne Pfade laufen
|
**Jeden Druck-QR vor der Freigabe einmal mit dem Handy scannen.** Das ist nicht Vorsicht, sondern die
|
||||||
über `abs_url()` (Single Source `base_url`), und ist die nicht https, sagt das Skript es laut. Solange
|
einzige Prüfmöglichkeit: die vendorte Bibliothek arbeitet **nicht reproduzierbar** (dieselbe URL
|
||||||
die Seite nicht live ist, die vollständige Produktions-URL direkt übergeben. **Jeden Druck-QR vor der
|
ergibt unterschiedliche Pixel — anderes Maskenmuster, beides gültig), ein Vergleich zweier PNGs
|
||||||
Freigabe einmal mit dem Handy scannen** — im Repo gibt es keinen Decoder, der das prüfen könnte.
|
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`)
|
**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
|
sind die einzige Merge-Logik für Termine bzw. News — `/termine`, `/news` UND die
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ if ($match !== null) {
|
|||||||
$opponentCrest = $match['is_home'] ? $match['away_crest'] : $match['home_crest'];
|
$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) : ''));
|
$kicker = trim($season . ($date !== '' ? ' · ' . german_date($date) : ''));
|
||||||
|
|
||||||
// Zweite Coverzeile (Magazin-Praxis: Teaser auf die Geschichte im Heft) —
|
// Zweite Coverzeile (Magazin-Praxis: Teaser auf die Geschichte im Heft) —
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ $groups = $groups ?? [];
|
|||||||
$matchcenterQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-matchcenter.png')
|
$matchcenterQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-matchcenter.png')
|
||||||
? 'img/stadionzeitung/qr-matchcenter.png'
|
? 'img/stadionzeitung/qr-matchcenter.png'
|
||||||
: null;
|
: null;
|
||||||
$matchcenterLink = preg_replace('~^https?://(www\.)?~', '', abs_url('matchcenter'));
|
$matchcenterLink = club_domain('matchcenter');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-ergebnisse">
|
<div class="stadionzeitung-ergebnisse">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ $emailHistorie = (string) (json_load('club')['email_historie'] ?? '');
|
|||||||
$historieQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-historie.png')
|
$historieQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-historie.png')
|
||||||
? 'img/stadionzeitung/qr-historie.png'
|
? 'img/stadionzeitung/qr-historie.png'
|
||||||
: null;
|
: null;
|
||||||
$historieLink = preg_replace('~^https?://(www\.)?~', '', abs_url('historie'));
|
$historieLink = club_domain('historie');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-historie">
|
<div class="stadionzeitung-historie">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ $club = json_load('club');
|
|||||||
$legalName = (string) ($club['legal_name'] ?? $club['name'] ?? '');
|
$legalName = (string) ($club['legal_name'] ?? $club['name'] ?? '');
|
||||||
$address = $club['address'] ?? [];
|
$address = $club['address'] ?? [];
|
||||||
$email = (string) ($club['email'] ?? '');
|
$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'] ?? [];
|
$board = $club['board']['members'] ?? [];
|
||||||
$boardNote = (string) ($club['board']['note'] ?? '');
|
$boardNote = (string) ($club['board']['note'] ?? '');
|
||||||
?>
|
?>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ $persons = array_values(array_filter(
|
|||||||
$mitmachenQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitmachen.png')
|
$mitmachenQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitmachen.png')
|
||||||
? 'img/stadionzeitung/qr-mitmachen.png'
|
? 'img/stadionzeitung/qr-mitmachen.png'
|
||||||
: null;
|
: null;
|
||||||
$mitmachenLink = preg_replace('~^https?://(www\.)?~', '', abs_url('mitmachen'));
|
$mitmachenLink = club_domain('mitmachen');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-kontakt">
|
<div class="stadionzeitung-kontakt">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ declare(strict_types=1);
|
|||||||
$mitgliedQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitglied-werden.png')
|
$mitgliedQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-mitglied-werden.png')
|
||||||
? 'img/stadionzeitung/qr-mitglied-werden.png'
|
? 'img/stadionzeitung/qr-mitglied-werden.png'
|
||||||
: null;
|
: null;
|
||||||
$mitgliedLink = preg_replace('~^https?://(www\.)?~', '', abs_url('mitglied-werden'));
|
$mitgliedLink = club_domain('mitglied-werden');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-mitglied">
|
<div class="stadionzeitung-mitglied">
|
||||||
<div class="stadionzeitung-mitglied__poster">
|
<div class="stadionzeitung-mitglied__poster">
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ $sourceLabel = static fn (string $source): string => match ($source) {
|
|||||||
$newsQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-news.png')
|
$newsQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-news.png')
|
||||||
? 'img/stadionzeitung/qr-news.png'
|
? 'img/stadionzeitung/qr-news.png'
|
||||||
: null;
|
: null;
|
||||||
$newsLink = preg_replace('~^https?://(www\.)?~', '', abs_url('news'));
|
$newsLink = club_domain('news');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-news">
|
<div class="stadionzeitung-news">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ foreach (json_load('club')['board']['members'] ?? [] as $member) {
|
|||||||
$partnerQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-partner-werden.png')
|
$partnerQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-partner-werden.png')
|
||||||
? 'img/stadionzeitung/qr-partner-werden.png'
|
? 'img/stadionzeitung/qr-partner-werden.png'
|
||||||
: null;
|
: null;
|
||||||
$partnerLink = preg_replace('~^https?://(www\.)?~', '', abs_url('partner-werden'));
|
$partnerLink = club_domain('partner-werden');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-partner">
|
<div class="stadionzeitung-partner">
|
||||||
<header class="stadionzeitung-page__head">
|
<header class="stadionzeitung-page__head">
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ $heroText = (string) (json_load('sportheimbuchung')['hero']['text'] ?? '');
|
|||||||
$sportheimQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-sportheimbuchung.png')
|
$sportheimQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-sportheimbuchung.png')
|
||||||
? 'img/stadionzeitung/qr-sportheimbuchung.png'
|
? 'img/stadionzeitung/qr-sportheimbuchung.png'
|
||||||
: null;
|
: null;
|
||||||
$sportheimLink = preg_replace('~^https?://(www\.)?~', '', abs_url('sportheimbuchung'));
|
$sportheimLink = club_domain('sportheimbuchung');
|
||||||
?>
|
?>
|
||||||
<?php /* Pinselstrich-Ecken bewusst entfernt (Felix, 29.07.2026) — kommen
|
<?php /* Pinselstrich-Ecken bewusst entfernt (Felix, 29.07.2026) — kommen
|
||||||
ggf. später als saubere Bild-Assets zurück. */ ?>
|
ggf. später als saubere Bild-Assets zurück. */ ?>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ $heroImage = json_load('home')['hero']['image'] ?? null;
|
|||||||
$termineQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-termine.png')
|
$termineQr = is_file(PUBLIC_PATH . '/assets/img/stadionzeitung/qr-termine.png')
|
||||||
? 'img/stadionzeitung/qr-termine.png'
|
? 'img/stadionzeitung/qr-termine.png'
|
||||||
: null;
|
: null;
|
||||||
$termineLink = preg_replace('~^https?://(www\.)?~', '', abs_url('termine'));
|
$termineLink = club_domain('termine');
|
||||||
?>
|
?>
|
||||||
<div class="stadionzeitung-termine">
|
<div class="stadionzeitung-termine">
|
||||||
<?php if ($heroImage): ?>
|
<?php if ($heroImage): ?>
|
||||||
|
|||||||
@@ -77,6 +77,40 @@ function abs_url(string $slug = ''): string
|
|||||||
return rtrim((string) config('base_url'), '/') . url($slug);
|
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').
|
* Asset-Pfad mit Cache-Busting über filemtime: asset('css/tokens.css').
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ $meta = [
|
|||||||
<?= e($addr['zip']) ?> <?= e($addr['city']) ?><br>
|
<?= e($addr['zip']) ?> <?= e($addr['city']) ?><br>
|
||||||
<br>
|
<br>
|
||||||
E-Mail: <a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a><br>
|
E-Mail: <a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a><br>
|
||||||
Web: <a href="<?= e(config('base_url')) ?>"><?= e(preg_replace('#^https?://#', '', (string) config('base_url'))) ?></a>
|
<?php /* club_url() statt base_url: das Impressum nennt die Adresse des VEREINS,
|
||||||
|
nicht die des Servers, auf dem der Code gerade läuft. Lokal stand hier
|
||||||
|
sonst „localhost:8000" als Vereins-Webadresse. */ ?>
|
||||||
|
Web: <a href="<?= e(club_url()) ?>"><?= e(club_domain()) ?></a>
|
||||||
</address>
|
</address>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
22
bin/qr.php
@@ -18,11 +18,11 @@ declare(strict_types=1);
|
|||||||
* Plakate und Aushänge, die in Canva zusammengebaut werden — dort braucht es
|
* Plakate und Aushänge, die in Canva zusammengebaut werden — dort braucht es
|
||||||
* ein Vielfaches an Pixeln, sonst franst der Code im Druck aus.
|
* ein Vielfaches an Pixeln, sonst franst der Code im Druck aus.
|
||||||
*
|
*
|
||||||
* DIE EIGENTLICHE AUFGABE DIESES SKRIPTS IST DIE WARNUNG: ein QR-Code enthält
|
* Ein QR-Code hat die URL fest eingebacken — gegen die Entwicklungs-Config erzeugt
|
||||||
* die URL fest eingebacken. Wird er gegen die Entwicklungs-Config erzeugt, steht
|
* stünde `http://localhost:8000/…` darin und das gedruckte Plakat wäre Altpapier.
|
||||||
* `http://localhost:8000/…` darin und das gedruckte Plakat ist Altpapier. Ein
|
* Deshalb läuft ein interner Pfad durch club_url(): die öffentliche Vereinsadresse
|
||||||
* interner Pfad läuft deshalb durch abs_url() (Single Source: config base_url),
|
* aus data/club.json, unabhängig von der Umgebung. Der Code stimmt damit schon vor
|
||||||
* und wenn die nicht auf https zeigt, sagt das Skript es laut.
|
* dem Produktivgang und muss danach nicht neu erzeugt werden.
|
||||||
*/
|
*/
|
||||||
if (PHP_SAPI !== 'cli') {
|
if (PHP_SAPI !== 'cli') {
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -45,7 +45,7 @@ if ($args === []) {
|
|||||||
|
|
||||||
$target = $args[0];
|
$target = $args[0];
|
||||||
$isAbsolute = str_starts_with($target, 'http://') || str_starts_with($target, 'https://');
|
$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 —
|
// Zielpfad: ohne Angabe neben das Skript-Verzeichnis in storage/qr/ ablegen —
|
||||||
// Druckmaterial ist kein Website-Asset und hat unter public/ nichts zu suchen.
|
// 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';
|
$dest = $dir . '/qr-' . trim($name, '-') . '.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$isAbsolute && !str_starts_with((string) config('base_url'), 'https://')) {
|
// Der Riegel bleibt, nur zeigt er jetzt auf das richtige Feld: ohne saubere
|
||||||
fwrite(STDERR, "WARNUNG base_url ist '" . (string) config('base_url') . "' — dieser QR-Code führt NICHT auf die echte Seite\n");
|
// club.website wäre der gedruckte Code wieder unbrauchbar.
|
||||||
fwrite(STDERR, " und ist als Druckvorlage unbrauchbar. Entweder die Produktions-Config verwenden\n");
|
if (!$isAbsolute && !str_starts_with($url, 'https://')) {
|
||||||
fwrite(STDERR, " oder die vollständige URL direkt übergeben:\n");
|
fwrite(STDERR, "WARNUNG club.website in data/club.json ist nicht https ('{$url}') — dieser QR-Code\n");
|
||||||
fwrite(STDERR, " php bin/qr.php https://tsv08kulmbach.de/{$target}\n\n");
|
fwrite(STDERR, " führt nicht auf die echte Seite und ist als Druckvorlage unbrauchbar.\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!generate_qr_png($url, $dest, $scale)) {
|
if (!generate_qr_png($url, $dest, $scale)) {
|
||||||
|
|||||||
@@ -19,18 +19,20 @@ declare(strict_types=1);
|
|||||||
* <ordner>/spieler.* optional, freigestelltes Spielerfoto (PNG mit Transparenz)
|
* <ordner>/spieler.* optional, freigestelltes Spielerfoto (PNG mit Transparenz)
|
||||||
* fürs Cover — wird unverändert kopiert, nie über GD neu kodiert
|
* fürs Cover — wird unverändert kopiert, nie über GD neu kodiert
|
||||||
* <ordner>/verfasser.* optional, Porträtfoto für die Vorwort-Signatur
|
* <ordner>/verfasser.* optional, Porträtfoto für die Vorwort-Signatur
|
||||||
* <ordner>/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 →
|
* KEINE Anzeigen im Ordner (seit 31.07.2026): die kommen aus dem dauerhaften
|
||||||
* Ergebnis-Rückblick (live) → Anzeigen A → je Mannschaft eine Tabelle (live,
|
* Bestand data/anzeigen.json, gepflegt mit bin/anzeige-add.php. Sponsorenmotive
|
||||||
* Reihenfolge = data/matchcenter.json) → Vorschau (live) → Anzeigen B →
|
* ändern sich selten, das Heft erscheint oft — vorher lag jede Anzeige als Kopie
|
||||||
* Termine (live) → Anzeigen C → News (live) → Ansprechpartner (live) →
|
* unter jeder Ausgabe und ihr Alt-Text musste jedes Mal neu geschrieben werden.
|
||||||
* Anzeigen D → Rückseite. Das Cover wird automatisch generiert (siehe
|
*
|
||||||
* app/components/stadionzeitung-cover.php) — kein Canva-Bild mehr nötig. Der
|
* Feste Vorlage (Interview-Beschluss 29.07.2026, Anzeigen 31.07.2026 überarbeitet):
|
||||||
* gesamte Anzeigen-Pool (in gelieferter Reihenfolge) wird zu möglichst
|
* Der Inhalt ist eine Liste von BLÖCKEN, die Anzeigen werden gleichmäßig in die
|
||||||
* gleichen Teilen auf die vier Gruppen verteilt.
|
* 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
|
* Neue Ausgaben entstehen als ENTWURF (status: entwurf): unsichtbar in
|
||||||
* Archiv/Sitemap, Live-Seiten rechnen bei jedem Aufruf frisch. Erst
|
* 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.
|
// --- 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. ---
|
// QR ist "nice to have" — schlägt die Erzeugung fehl, läuft die Ausgabe ohne weiter. ---
|
||||||
$qrImage = null;
|
$qrImage = null;
|
||||||
$qrDest = "{$destDir}/qr.png";
|
$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";
|
$qrImage = "img/stadionzeitung/{$slug}/qr.png";
|
||||||
$log('QR-Code erzeugt.');
|
$log('QR-Code erzeugt.');
|
||||||
} else {
|
} 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 →
|
// Geteilte QRs der CTA-Füße (Kontakt → /mitmachen, News → /news, Termine →
|
||||||
// /termine, Vorschau → /matchcenter, für alle Ausgaben gleich) — werden bei
|
// /termine, Vorschau → /matchcenter, für alle Ausgaben gleich) — ebenfalls über
|
||||||
// jedem Lauf mit der aktuellen base_url neu geschrieben, damit sie nach dem
|
// club_url(), damit sie unabhängig von der Umgebung stimmen und nach dem
|
||||||
// Produktivgang automatisch auf die echte Domain zeigen.
|
// Produktivgang nicht neu erzeugt werden müssen.
|
||||||
foreach (['mitmachen', 'news', 'termine', 'matchcenter', 'mitglied-werden', 'historie', 'sportheimbuchung', 'partner-werden'] as $ctaTarget) {
|
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.");
|
$log("QR-Code für /{$ctaTarget} erzeugt.");
|
||||||
} else {
|
} else {
|
||||||
$log("QR-Code für /{$ctaTarget} konnte nicht erzeugt werden — die Seite rendert ohne QR.", 'WARN');
|
$log("QR-Code für /{$ctaTarget} konnte nicht erzeugt werden — die Seite rendert ohne QR.", 'WARN');
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"founded": "1908",
|
"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.",
|
"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.",
|
"slogan": "Gemeinsam. Immer weiter.",
|
||||||
|
"website": "https://www.tsv08kulmbach.de",
|
||||||
"departments": [
|
"departments": [
|
||||||
"Fußball",
|
"Fußball",
|
||||||
"Turnen",
|
"Turnen",
|
||||||
|
|||||||
@@ -1134,7 +1134,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"published_at": "2026-07-31T12:40:07+00:00"
|
"published_at": "2026-07-31T13:34:54+00:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 579 B |
|
Before Width: | Height: | Size: 431 B After Width: | Height: | Size: 425 B |
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 430 B |
|
Before Width: | Height: | Size: 428 B After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 432 B After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 421 B |
|
Before Width: | Height: | Size: 417 B After Width: | Height: | Size: 427 B |
|
Before Width: | Height: | Size: 429 B After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 434 B After Width: | Height: | Size: 505 B |
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 426 B |