Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
< ? php
declare ( strict_types = 1 );
/**
* Stadionzeitung - Ausgabe hinzufügen / aktualisieren : baut eine Ausgabe nach der
* festen Vorlage ( siehe CLAUDE . md → Stadionzeitung ) aus Canva - Bildern +
* drei automatischen Live - Seiten ( Tabelle / Termine / News ) . Manuell im Chat
* ausgelöst , kein Cron .
*
* Aufruf : php bin / stadionzeitung - add . php < ordner > " <Gegner> " < YYYY - MM - DD > < team - key > [ slug ]
* < team - key > Team - Key aus data / matchcenter . json ( z . B . erste - mannschaft , damen ) —
* bestimmt , wessen Tabelle auf der Live - Seite „Tabelle " erscheint.
*
* Erwarteter Ordnerinhalt :
* < ordner >/ vorwort .* optional , Fallback - Bild — nur genutzt , solange für diese
* Ausgabe noch kein Vorwort - Text hinterlegt ist ( siehe unten )
* < ordner >/ rueckseite .* optional
* < ordner >/ spieler .* optional , freigestelltes Spielerfoto ( PNG mit Transparenz )
* fürs Cover — wird unverändert kopiert , nie über GD neu kodiert
* < ordner >/ verfasser .* optional , Porträtfoto für die Vorwort - Signatur
*
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
2026-07-31 15:40:59 +02:00
* 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 ) .
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
*
* Neue Ausgaben entstehen als ENTWURF ( status : entwurf ) : unsichtbar in
* Archiv / Sitemap , Live - Seiten rechnen bei jedem Aufruf frisch . Erst
* bin / stadionzeitung - publish . php friert Tabellen / News / Ergebnisse / Vorschau als
* snapshot ein und schaltet die Ausgabe sichtbar . Fürs gefaltete A5 - Heft
* ( Druckfassung ? druck = 1 , Broschüren - Reihenfolge ) sollte die Seitenzahl ein
* Vielfaches von 4 sein — das Skript meldet die Lücke , aufgefüllt wird beim
* Erzeugen gemeinsam ( weiße Füllseiten in der Druckfassung sind der Notnagel ) .
*
* Vorwort - Text ist KEIN Skript - Input : er wird ( wie data / events . json / news . json ) im
* Chat gepflegt — Felix schickt den Text , Claude trägt ihn manuell ins `vorwort` - Feld
* der Ausgabe in data / stadionzeitung . json ein ( heading / paragraphs / closing / signee_name /
* signee_role ) . Damit ein erneuter Skript - Lauf ( z . B . um eine Anzeige zu korrigieren )
* dieses Feld nicht wegwirft , liest das Skript den bisherigen Wert vor dem Ersetzen
* aus und schreibt ihn unverändert zurück . Ist ein `vorwort` - Feld vorhanden , rendert
* die Ausgabe die Live - Seite app / components / stadionzeitung - vorwort . php statt des
* Fallback - Bilds `vorwort.*` .
*
Stadionzeitung Ausgabe 2 (TSV Ködnitz, 15.08.): Entwurf + neue Aufruf-Seite
- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
2026-08-13 17:27:41 +02:00
* Interview ( `interview` - Feld ) und die generische Aufruf - Seite ( `aufruf` - Feld ,
* z . B . Sportfest - Helferaufruf ) sind wie das Vorwort chat - gepflegt : das Skript
* trägt sie über erneute Läufe weiter und plant die Live - Seite nur ein , wenn
* das Feld existiert . Reihenfolge im anzeigenfreien Aufschlag : Vorwort →
* Interview → Aufruf . Für die Aufruf - Seite erzeugt das Skript zusätzlich den
* QR ( qr - aufruf . png im Ausgaben - Ordner ) aus `aufruf.link` .
*
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
* Erneuter Aufruf mit demselben Slug ersetzt die Ausgabe vollständig ( alte
* Seiten - Bilder werden vorher gelöscht ) — idempotent für Korrekturen .
*/
if ( PHP_SAPI !== 'cli' ) {
exit ( 1 );
}
require dirname ( __DIR__ ) . '/app/bootstrap.php' ;
$args = array_values ( array_filter ( $argv , static fn ( string $a ) : bool => $a !== '-v' ));
$verbose = in_array ( '-v' , $argv , true );
array_shift ( $args ); // Skriptname
if ( count ( $args ) < 4 ) {
fwrite ( STDERR , " Nutzung: php bin/stadionzeitung-add.php <ordner> \" <Gegner> \" <YYYY-MM-DD> <team-key> [slug] \n " );
exit ( 1 );
}
[ $sourceDir , $opponent , $date , $teamKey ] = $args ;
$slugArg = $args [ 4 ] ? ? '' ;
$log = static function ( string $msg , string $level = 'INFO' ) use ( $verbose ) : void {
log_write ( 'stadionzeitung' , $level , $msg );
if ( $verbose || $level === 'ERROR' || $level === 'WARN' ) {
echo '[' . date ( 'c' ) . " ] { $level } { $msg } \n " ;
}
};
$fail = static function ( string $msg ) use ( $log ) : never {
$log ( " { $msg } — bestehender Cache bleibt unangetastet. " , 'ERROR' );
exit ( 1 );
};
// --- Eingaben validieren ---
if ( ! is_dir ( $sourceDir )) {
$fail ( " Ordner nicht gefunden: { $sourceDir } " );
}
$parsedDate = DateTimeImmutable :: createFromFormat ( 'Y-m-d' , $date );
if ( $parsedDate === false || $parsedDate -> format ( 'Y-m-d' ) !== $date ) {
$fail ( " Ungültiges Datum (erwartet YYYY-MM-DD): { $date } " );
}
$opponent = trim ( $opponent );
if ( $opponent === '' ) {
$fail ( 'Gegner darf nicht leer sein.' );
}
$mc = json_load ( 'matchcenter' );
$knownTeamKeys = array_column ( $mc [ 'teams' ] ? ? [], 'key' );
if ( ! in_array ( $teamKey , $knownTeamKeys , true )) {
$fail ( " Unbekannter team-key ' { $teamKey } '. Bekannt: " . implode ( ', ' , $knownTeamKeys ));
}
/**
* Kleine , lokale Slugify - Funktion ( nur dieser Aufrufer ) — Umlaute
* transliterieren , Kleinschreibung , alles außer a - z0 - 9 zu Bindestrichen .
*/
$slugify = static function ( string $text ) : string {
$text = strtr ( $text , [ 'ä' => 'ae' , 'ö' => 'oe' , 'ü' => 'ue' , 'Ä' => 'Ae' , 'Ö' => 'Oe' , 'Ü' => 'Ue' , 'ß' => 'ss' ]);
$text = strtolower ( $text );
$text = preg_replace ( '/[^a-z0-9]+/' , '-' , $text ) ? ? '' ;
return trim ( $text , '-' );
};
$slug = $slugArg !== '' ? $slugify ( $slugArg ) : $date . '-' . $slugify ( $opponent );
if ( $slug === '' ) {
$fail ( 'Konnte keinen gültigen Slug bilden.' );
}
$exts = 'jpg,jpeg,png,webp,JPG,JPEG,PNG,WEBP' ;
$findOne = static function ( string $baseName ) use ( $sourceDir , $exts ) : ? string {
$matches = glob ( rtrim ( $sourceDir , '/' ) . " / { $baseName } . { { $exts } } " , GLOB_BRACE ) ? : [];
return $matches [ 0 ] ? ? null ;
};
$findAll = static function ( string $subdir ) use ( $sourceDir , $exts ) : array {
$dir = rtrim ( $sourceDir , '/' ) . " / { $subdir } " ;
if ( ! is_dir ( $dir )) {
return [];
}
$files = glob ( " { $dir } /*. { { $exts } } " , GLOB_BRACE ) ? : [];
natsort ( $files );
return array_values ( $files );
};
$vorwort = $findOne ( 'vorwort' );
$rueckseite = $findOne ( 'rueckseite' );
$spieler = $findOne ( 'spieler' );
$verfasser = $findOne ( 'verfasser' );
// Anzeigen kommen NICHT mehr aus dem Ablieferordner, sondern aus dem dauerhaften
// Bestand (data/anzeigen.json, gepflegt über bin/anzeige-add.php). Ein noch
// vorhandener anzeigen/-Unterordner wäre also wirkungslos — dann besser sagen,
// wo sie jetzt hingehören, als ihn stillschweigend zu ignorieren.
if ( $findAll ( 'anzeigen' ) !== []) {
$log ( 'Der Ordner enthält noch anzeigen/ — die werden NICHT mehr von hier gelesen. '
. 'Anzeigen liegen im Bestand: php bin/anzeige-add.php <ordner>' , 'WARN' );
}
if ( $rueckseite === null ) {
$log ( 'Keine rueckseite.* im Ordner — das Heft endet mit dem Impressum.' , 'WARN' );
}
// --- Lock gegen parallele Läufe (billige Absicherung, kein echtes Cron-Risiko hier) ---
if ( ! is_dir ( STORAGE_PATH . '/cache' )) {
mkdir ( STORAGE_PATH . '/cache' , 0755 , true );
}
$lock = fopen ( STORAGE_PATH . '/cache/stadionzeitung.lock' , 'c' );
if ( ! $lock || ! flock ( $lock , LOCK_EX | LOCK_NB )) {
$log ( 'Läuft bereits (Lock belegt) — Lauf übersprungen.' , 'WARN' );
exit ( 1 );
}
// --- Bisherigen Eintrag lesen (falls vorhanden) — trägt u. a. das chat-gepflegte
// vorwort-Feld über einen erneuten Lauf hinweg weiter, siehe Kommentar oben. ---
$cacheFile = DATA_PATH . '/stadionzeitung.json' ;
$existing = is_file ( $cacheFile )
? ( json_decode (( string ) file_get_contents ( $cacheFile ), true ) ? : [ 'issues' => []])
: [ 'issues' => []];
$oldIssue = null ;
foreach ( $existing [ 'issues' ] ? ? [] as $entry ) {
if (( $entry [ 'slug' ] ? ? '' ) === $slug ) {
$oldIssue = $entry ;
break ;
}
}
$oldVorwort = $oldIssue [ 'vorwort' ] ? ? null ;
// Interview wie das Vorwort chat-gepflegt — überlebt erneute Läufe ebenso.
$oldInterview = $oldIssue [ 'interview' ] ? ? null ;
Stadionzeitung Ausgabe 2 (TSV Ködnitz, 15.08.): Entwurf + neue Aufruf-Seite
- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
2026-08-13 17:27:41 +02:00
// Aufruf-Seite (z. B. Sportfest-Helferaufruf) ebenso chat-gepflegt.
$oldAufruf = $oldIssue [ 'aufruf' ] ? ? null ;
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
// --- Ziel-Ordner vorbereiten: bei erneutem Lauf alte Seiten dieser Ausgabe wegräumen ---
$destDir = PUBLIC_PATH . " /assets/img/stadionzeitung/ { $slug } " ;
if ( ! is_dir ( $destDir )) {
mkdir ( $destDir , 0755 , true );
} else {
// Der Ordner wird KOMPLETT geleert, nicht nach Mustern aufgeräumt: alles
// darin entsteht in diesem Lauf neu (Vorwort- und Rückseiten-Bild, Spieler,
// Verfasser, QR). Ein Muster-Aufräumen hinterlässt sonst Waisen, sobald sich
// Dateinamen ändern — genau das ist passiert: neben den `page-NN`-Resten des
// alten Positions-Schemas lagen auch die `anzeige-*`-Kopien, die jetzt im
// Bestand (public/assets/img/anzeigen/) leben. Die geteilten CTA-QR-Codes
// liegen eine Ebene höher und sind davon nicht betroffen.
foreach ( glob ( " { $destDir } /* " ) ? : [] as $old ) {
if ( is_file ( $old )) {
unlink ( $old );
}
}
}
$widths = [ 480 , 960 , 1440 ];
// --- Anzeigen kommen aus dem DAUERHAFTEN Bestand (data/anzeigen.json), nicht
// mehr aus dem Ablieferordner: Sponsorenmotive ändern sich selten, das Heft
// erscheint oft. Gepflegt wird der Bestand über bin/anzeige-add.php (Bilder) und
// im Chat (Sponsorenname, Reihenfolge, aktiv). Hier wird nichts gerendert, die
// Bilder liegen schon. ---
$anzeigenSeiten = array_values ( array_map (
static function ( array $a ) : array {
$sponsor = trim (( string ) ( $a [ 'sponsor' ] ? ? '' ));
return [
'type' => 'image' ,
'base' => ( string ) $a [ 'base' ],
'widths' => $a [ 'widths' ] ? ? [],
// Alt-Text aus dem Sponsorennamen abgeleitet, damit er nur an EINER
// Stelle gepflegt wird. `alt` im Bestand überschreibt ihn (z. B.
// „Eigenanzeige: …").
'alt' => ( string ) ( $a [ 'alt' ] ? ? ( 'Anzeige: ' . $sponsor )),
];
},
array_filter (
json_load ( 'anzeigen' )[ 'anzeigen' ] ? ? [],
// `bis` gegen das DATUM DER AUSGABE prüfen, nicht gegen heute: ein Heft
// zum Spiel am 1. August darf für ein Fest am 6. September werben, ein
// später angelegtes Heft nicht mehr. Damit retirieren zeitgebundene
// Eigenanzeigen (Sportfest-Plakat, Programm) von selbst, statt bis zum
// nächsten Handgriff im Heft zu bleiben.
static fn ( array $a ) : bool => ( $a [ 'aktiv' ] ? ? true )
&& ! empty ( $a [ 'base' ])
&& ( empty ( $a [ 'bis' ]) || ( string ) $a [ 'bis' ] >= $date )
)
));
// --- Seiten-Bausteine: Live-Seiten brauchen kein Bild, die zwei verbliebenen
// Ausgaben-Bilder (Vorwort-Fallback, Rückseite) schon. Beide bekommen SPRECHENDE
// Dateinamen statt der früheren Positionsnummern (page-07) — die verschoben sich
// bei jeder Umverteilung. ---
$live = static function ( string $page , ? string $team = null ) : array {
$entry = [ 'type' => 'live' , 'page' => $page ];
if ( $team !== null ) {
$entry [ 'team' ] = $team ;
}
return $entry ;
};
$bild = static function ( string $src , string $name , string $alt ) use ( $destDir , $widths , $slug ) : array {
try {
$variants = resize_image_variants ( $src , " { $destDir } / { $name } " , $widths , 82 );
} catch ( RuntimeException $e ) {
throw new RuntimeException ( " Seite ' { $name } ' ( { $src } ) fehlgeschlagen: " . $e -> getMessage ());
}
$generated = array_values ( array_unique ( array_column ( $variants , 'width' )));
sort ( $generated );
return [
'type' => 'image' ,
'base' => " img/stadionzeitung/ { $slug } / { $name } " ,
'widths' => $generated ,
'alt' => $alt ,
];
};
/*
* Inhalt als Liste von BLÖCKEN , Anzeigen kommen anschließend in die Lücken
* dazwischen . Ein Block ist eine Folge von Seiten , die zusammenbleiben MUSS ;
* `anzeigen_danach` sagt , ob nach dem Block eine Lücke für Anzeigen entsteht .
*
* Drei Stellen sind bewusst anzeigenfrei :
* - nach dem Cover : der Leser schlägt auf Inhalt auf , nicht auf Werbung ,
* - innerhalb des Aufschlags Vorwort + Interview ( deshalb EIN Block ),
* - innerhalb der Tabellen ( deshalb EIN Block ) : die Tabellenseiten müssen im
* gefalteten Heft nebeneinander liegen , eine Anzeige dazwischen zerstört
* genau die Doppelseite , die die Paritätsrechnung unten herstellt ,
* - zwischen Impressum und Rückseite : die Rückseite ist die Heft - Außenseite .
*/
$bloecke = [];
try {
$bloecke [] = [ 'name' => 'cover' , 'seiten' => [ $live ( 'cover' )], 'anzeigen_danach' => false ];
$aufschlag = [];
if ( $oldVorwort !== null ) {
$aufschlag [] = $live ( 'vorwort' );
} elseif ( $vorwort !== null ) {
$aufschlag [] = $bild ( $vorwort , 'vorwort' , 'Vorwort der Stadionzeitung' );
}
// Interview-Seite nur, wenn die Ausgabe ein Interview trägt (chat-gepflegtes
// Feld) — das Cover zeigt dann automatisch die Teaser-Zeile dazu.
if ( $oldInterview !== null ) {
$aufschlag [] = $live ( 'interview' );
}
Stadionzeitung Ausgabe 2 (TSV Ködnitz, 15.08.): Entwurf + neue Aufruf-Seite
- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
2026-08-13 17:27:41 +02:00
// Aufruf-Seite (Helferaufruf o. Ä.) reiht sich in denselben anzeigenfreien
// Aufschlag ein, direkt hinter Vorwort/Interview — ein redaktioneller
// Einstieg ohne Werbung dazwischen (chat-gepflegtes `aufruf`-Feld).
if ( $oldAufruf !== null ) {
$aufschlag [] = $live ( 'aufruf' );
}
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
if ( $aufschlag !== []) {
$bloecke [] = [ 'name' => 'aufschlag' , 'seiten' => $aufschlag , 'anzeigen_danach' => true ];
}
$bloecke [] = [ 'name' => 'ergebnisse' , 'seiten' => [ $live ( 'ergebnisse' )], 'anzeigen_danach' => true ];
// Eine Tabellen-Seite je Mannschaft, Reihenfolge wie in matchcenter.json
// (= config 'matchcenter.teams'). Kommt eine Mannschaft dazu (z. B. die
// SG der Zweiten), bekommt die nächste Ausgabe ihre Tabelle automatisch.
$tabellen = [];
foreach ( $mc [ 'teams' ] ? ? [] as $team ) {
if ( ! empty ( $team [ 'key' ])) {
$tabellen [] = $live ( 'tabelle' , ( string ) $team [ 'key' ]);
}
}
if ( $tabellen !== []) {
$bloecke [] = [ 'name' => 'tabellen' , 'seiten' => $tabellen , 'anzeigen_danach' => true ];
}
foreach ([ 'vorschau' , 'termine' , 'news' , 'kontakt' , 'mitglied' , 'historie' , 'sportheim' , 'partner' , 'momente' ] as $seite ) {
$bloecke [] = [ 'name' => $seite , 'seiten' => [ $live ( $seite )], 'anzeigen_danach' => true ];
}
$bloecke [] = [ 'name' => 'impressum' , 'seiten' => [ $live ( 'impressum' )], 'anzeigen_danach' => false ];
if ( $rueckseite !== null ) {
$bloecke [] = [
'name' => 'rueckseite' ,
'seiten' => [ $bild ( $rueckseite , 'rueckseite' , " Rückseite der Stadionzeitung gegen { $opponent } " )],
'anzeigen_danach' => false ,
];
}
} catch ( RuntimeException $e ) {
$fail ( $e -> getMessage ());
}
// --- Anzeigen gleichmäßig auf die Lücken verteilen ---
$luecken = [];
foreach ( $bloecke as $i => $b ) {
if ( $b [ 'anzeigen_danach' ]) {
$luecken [] = $i ;
}
}
$luckCount = count ( $luecken );
$adCount = count ( $anzeigenSeiten );
if ( $luckCount === 0 ) {
$fail ( 'Keine Lücke für Anzeigen — die Vorlage hat keine Inhaltsblöcke.' );
}
// Grundmenge plus Restverteilung. Der Rest wird GESTREUT (nicht vorne gebündelt),
// sonst stünden alle Doppel-Lücken am Heftanfang.
$basis = intdiv ( $adCount , $luckCount );
$rest = $adCount % $luckCount ;
$verteilung = [];
for ( $g = 0 ; $g < $luckCount ; $g ++ ) {
$verteilung [ $g ] = $basis + ( intdiv (( $g + 1 ) * $rest , $luckCount ) - intdiv ( $g * $rest , $luckCount ));
}
if ( $adCount > 2 * $luckCount ) {
$log ( sprintf (
'%d Anzeigen auf %d Lücken — mehr als zwei hintereinander ist damit unvermeidbar (höchstens %d). Weniger Anzeigen oder eine Inhaltsseite mehr.' ,
$adCount ,
$luckCount ,
max ( $verteilung )
), 'WARN' );
}
/** 1-basierte Seitenzahl, auf der ein Block beginnt (null, wenn er fehlt). */
$blockSeite = static function ( string $name ) use ( $bloecke , $verteilung ) : ? int {
$seite = 0 ;
$g = 0 ;
foreach ( $bloecke as $b ) {
if ( $b [ 'name' ] === $name ) {
return $seite + 1 ;
}
$seite += count ( $b [ 'seiten' ]);
if ( $b [ 'anzeigen_danach' ]) {
$seite += $verteilung [ $g ];
$g ++ ;
}
}
return null ;
};
/*
* Doppelseiten - Regel : aufgeschlagen liegen die Seiten ( 2 , 3 ), ( 4 , 5 ), … nebeneinander ,
* die GERADE Seite links . Die erste Tabellenseite muss deshalb auf einer geraden
* Seitenzahl liegen , damit die Tabellen im gefalteten Heft eine Doppelseite bilden .
* Nur Anzeigen in Lücken VOR dem Tabellenblock verschieben dessen Parität — eine
* Anzeige zwischen einer solchen Lücke und einer dahinter zu tauschen kippt sie
* also , ohne die Gesamtseitenzahl zu ändern . Früher war das Handarbeit
* ( „notfalls eine Anzeige verschieben " ), jetzt rechnet es das Skript.
*/
$tabellenIndex = null ;
foreach ( $bloecke as $i => $b ) {
if ( $b [ 'name' ] === 'tabellen' ) {
$tabellenIndex = $i ;
break ;
}
}
if ( $tabellenIndex !== null && ( $seite = $blockSeite ( 'tabellen' )) !== null && $seite % 2 === 1 ) {
$vor = [];
$nach = [];
foreach ( $luecken as $g => $blockIdx ) {
if ( $blockIdx < $tabellenIndex ) {
$vor [] = $g ;
} else {
$nach [] = $g ;
}
}
$getauscht = false ;
// Erst versuchen: eine Anzeige nach vorne holen (Lücke vorne darf auf 2).
foreach ( $vor as $g ) {
if ( $verteilung [ $g ] >= 2 ) {
continue ;
}
foreach ( array_reverse ( $nach ) as $h ) {
if ( $verteilung [ $h ] > 0 ) {
$verteilung [ $g ] ++ ;
$verteilung [ $h ] -- ;
$getauscht = true ;
$log ( " Doppelseiten-Regel: eine Anzeige aus Lücke { $h } nach Lücke { $g } verschoben, damit die Tabellen nebeneinander liegen. " );
break 2 ;
}
}
}
// Sonst umgekehrt: eine Anzeige nach hinten abgeben.
if ( ! $getauscht ) {
foreach ( $vor as $g ) {
if ( $verteilung [ $g ] < 1 ) {
continue ;
}
foreach ( $nach as $h ) {
if ( $verteilung [ $h ] < 2 ) {
$verteilung [ $g ] -- ;
$verteilung [ $h ] ++ ;
$getauscht = true ;
$log ( " Doppelseiten-Regel: eine Anzeige aus Lücke { $g } nach Lücke { $h } verschoben, damit die Tabellen nebeneinander liegen. " );
break 2 ;
}
}
}
}
if ( ! $getauscht ) {
$log ( 'Doppelseiten-Regel NICHT herstellbar (keine Anzeige verschiebbar) — die Tabellen liegen im Heft nicht nebeneinander.' , 'WARN' );
}
}
// --- Zusammensetzen: Block, dann die Anzeigen seiner Lücke ---
$images = [];
$g = 0 ;
$zeiger = 0 ;
foreach ( $bloecke as $b ) {
foreach ( $b [ 'seiten' ] as $seiteEintrag ) {
$images [] = $seiteEintrag ;
}
if ( $b [ 'anzeigen_danach' ]) {
for ( $k = 0 ; $k < $verteilung [ $g ]; $k ++ ) {
$images [] = $anzeigenSeiten [ $zeiger ++ ];
}
$g ++ ;
}
}
$total = count ( $images );
// Layout einmal kompakt protokollieren (statt einer Zeile pro Seite) — daran
// sieht man auf einen Blick, ob die Verteilung stimmt.
$layout = implode ( ' ' , array_map (
static fn ( array $e ) : string => ( $e [ 'type' ] === 'live' ? strtoupper ( substr ( $e [ 'page' ], 0 , 3 )) : '·' ),
$images
));
$log ( " Layout ( { $total } Seiten, · = Anzeige): { $layout } " );
$log ( sprintf (
'%d Anzeigen auf %d Lücken verteilt (%s), längster Block: %d.' ,
$adCount ,
$luckCount ,
implode ( '/' , $verteilung ),
max ( $verteilung ? : [ 0 ])
));
// Fürs gefaltete Heft (Sattelheftung) muss die Seitenzahl durch 4 teilbar
// sein — die Druckfassung füllt fehlende Seiten weiß auf, aber sauberer ist
// eine gemeinsame Lösung beim Erzeugen (weitere Anzeige, Füllseite mit
// Inhalt). Deshalb hier nur eine deutliche Meldung, kein Abbruch.
if ( $total % 4 !== 0 ) {
$log ( " Seitenzahl { $total } ist kein Vielfaches von 4 — die Druckfassung füllt mit " . ( 4 - $total % 4 ) . ' weißen Seiten auf. Besser gemeinsam lösen (z. B. eine Anzeige mehr).' , 'WARN' );
}
Cover-Spielerfoto: Varianten statt 12-MB-Original (iOS Safari)
Das Cover blieb am iPhone leer, während Chrome am Desktop es zeigte. Ursache
war kein fehlendes Bild: das freigestellte Spielerfoto hatte 2584x5093 Pixel und
12,3 MB. iOS Safari verwirft Bilder, deren dekodierte Größe sein Budget
übersteigt — hier gut 53 MB, und auf einer Ausgabe liegen 86 Bilder.
Dargestellt wird das Foto mit 98cqi Höhe, bei 480px Seitenbreite also etwa
238x470 CSS-Pixel. Es gibt jetzt 320/640/1000 als PNG mit erhaltener
Transparenz (0,2 / 0,8 / 1,9 MB); sizes nennt die echte Breite, sonst zöge ein
Handy mit DPR 3 die größte Variante. Das Original ist gelöscht.
Dafür kann resize_image_variants() nun Alpha erhalten ($alpha = true schreibt
PNG statt JPG) und die img-Komponente eine Endung ($image['ext']). Der Default
bleibt JPG, weil bestehende Aufrufer PNG-Quellen absichtlich zu JPG verkleinern.
bin/stadionzeitung-add.php erzeugt die Varianten künftig selbst. Dort stand
"unverändert kopieren, Felix liefert bereits ein web-taugliches Foto" — genau
diese Annahme ist gerissen.
Außerdem: smtp.password stand in der Produktiv-Config noch auf dem Platzhalter
POSTFACH_PASSWORT (von preflight auf dem Server gefunden, einziger harter
Fehler). Übernommen wurde das Passwort aus der Dev-Config, mit dem der Versand
am 27.07.2026 verifiziert wurde; Host, Postfach und Empfänger sind identisch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:58:43 +02:00
// --- Spielerfoto fürs Cover: zuschneiden UND verkleinern. ---
// Hier stand früher „unverändert kopieren, Felix liefert bereits ein web-taugliches
// Foto" — die Annahme stimmte nicht. Das Hutschdorf-Foto hatte 2584x5093 Pixel und
// 12 MB; iOS Safari verwirft Bilder, deren dekodierte Größe sein Budget übersteigt
// (hier gut 53 MB), und auf einer Ausgabe liegen rund 86 Bilder. Ergebnis: das Cover
// blieb am iPhone leer, während Chrome am Desktop es zeigte (31.07.2026).
// Dargestellt wird das Foto mit 98cqi Höhe, bei 480px Seitenbreite also etwa
// 238x470 CSS-Pixel — die 1000er Variante deckt Druck und dreifache Pixeldichte ab.
// resize_image_variants() erhält bei $alpha = true die Freistellung (PNG statt JPG).
// Optional, kein Fehlschlag bei Fehlen.
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
$playerImage = null ;
if ( $spieler !== null ) {
$ext = strtolower ( pathinfo ( $spieler , PATHINFO_EXTENSION ));
Cover-Spielerfoto: Varianten statt 12-MB-Original (iOS Safari)
Das Cover blieb am iPhone leer, während Chrome am Desktop es zeigte. Ursache
war kein fehlendes Bild: das freigestellte Spielerfoto hatte 2584x5093 Pixel und
12,3 MB. iOS Safari verwirft Bilder, deren dekodierte Größe sein Budget
übersteigt — hier gut 53 MB, und auf einer Ausgabe liegen 86 Bilder.
Dargestellt wird das Foto mit 98cqi Höhe, bei 480px Seitenbreite also etwa
238x470 CSS-Pixel. Es gibt jetzt 320/640/1000 als PNG mit erhaltener
Transparenz (0,2 / 0,8 / 1,9 MB); sizes nennt die echte Breite, sonst zöge ein
Handy mit DPR 3 die größte Variante. Das Original ist gelöscht.
Dafür kann resize_image_variants() nun Alpha erhalten ($alpha = true schreibt
PNG statt JPG) und die img-Komponente eine Endung ($image['ext']). Der Default
bleibt JPG, weil bestehende Aufrufer PNG-Quellen absichtlich zu JPG verkleinern.
bin/stadionzeitung-add.php erzeugt die Varianten künftig selbst. Dort stand
"unverändert kopieren, Felix liefert bereits ein web-taugliches Foto" — genau
diese Annahme ist gerissen.
Außerdem: smtp.password stand in der Produktiv-Config noch auf dem Platzhalter
POSTFACH_PASSWORT (von preflight auf dem Server gefunden, einziger harter
Fehler). Übernommen wurde das Passwort aus der Dev-Config, mit dem der Versand
am 27.07.2026 verifiziert wurde; Host, Postfach und Empfänger sind identisch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 16:58:43 +02:00
$istPng = $ext === 'png' ;
// PNG: erst auf den Inhalt zuschneiden (Freistellungen kommen oft mit viel
// leerem Rand aus dem Export-Tool), dann verkleinern. Der Zuschnitt geht in eine
// Zwischendatei, damit das riesige Original nicht im Ausgaben-Ordner landet.
$quelle = $spieler ;
$tmpCrop = " { $destDir } /spieler-zuschnitt.png " ;
if ( $istPng && crop_transparent_png_to_content ( $spieler , $tmpCrop )) {
$quelle = $tmpCrop ;
}
try {
$varianten = resize_image_variants ( $quelle , " { $destDir } /spieler " , [ 320 , 640 , 1000 ], 82 , $istPng );
$playerImage = [
'base' => " img/stadionzeitung/ { $slug } /spieler " ,
'widths' => array_map ( static fn ( array $v ) : int => $v [ 'width' ], $varianten ),
'ext' => $istPng ? 'png' : 'jpg' ,
'alt' => '' ,
];
$groesse = array_sum ( array_map ( static fn ( array $v ) : int => $v [ 'bytes' ], $varianten ));
$log ( sprintf (
'Spielerfoto übernommen: %d Varianten%s, zusammen %.1f MB (Original %.1f MB).' ,
count ( $varianten ),
$istPng ? ' mit Transparenz, auf Inhalt zugeschnitten' : '' ,
$groesse / 1048576 ,
filesize ( $spieler ) / 1048576
));
} catch ( Throwable $e ) {
$log ( " Spielerfoto konnte nicht verarbeitet werden: { $spieler } " , 'WARN' );
}
if ( is_file ( $tmpCrop )) {
unlink ( $tmpCrop );
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
}
}
// --- QR-Code zur digitalen Ausgabe fürs Cover: einmalig, Ziel-URL ändert sich nie.
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
2026-07-31 15:40:59 +02:00
// 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).
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
// QR ist "nice to have" — schlägt die Erzeugung fehl, läuft die Ausgabe ohne weiter. ---
$qrImage = null ;
$qrDest = " { $destDir } /qr.png " ;
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
2026-07-31 15:40:59 +02:00
if ( generate_qr_png ( club_url ( 'stadionzeitung/' . $slug ), $qrDest )) {
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
$qrImage = " img/stadionzeitung/ { $slug } /qr.png " ;
$log ( 'QR-Code erzeugt.' );
} else {
$log ( 'QR-Code konnte nicht erzeugt werden — Cover rendert ohne QR.' , 'WARN' );
}
// Geteilte QRs der CTA-Füße (Kontakt → /mitmachen, News → /news, Termine →
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
2026-07-31 15:40:59 +02:00
// /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.
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
foreach ([ 'mitmachen' , 'news' , 'termine' , 'matchcenter' , 'mitglied-werden' , 'historie' , 'sportheimbuchung' , 'partner-werden' ] as $ctaTarget ) {
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
2026-07-31 15:40:59 +02:00
if ( generate_qr_png ( club_url ( $ctaTarget ), PUBLIC_PATH . " /assets/img/stadionzeitung/qr- { $ctaTarget } .png " )) {
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
$log ( " QR-Code für / { $ctaTarget } erzeugt. " );
} else {
$log ( " QR-Code für / { $ctaTarget } konnte nicht erzeugt werden — die Seite rendert ohne QR. " , 'WARN' );
}
}
Stadionzeitung Ausgabe 2 (TSV Ködnitz, 15.08.): Entwurf + neue Aufruf-Seite
- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
2026-08-13 17:27:41 +02:00
// Aufruf-Seite: QR auf das interne Ziel (z. B. /veranstaltungen/sportfest-2026)
// aus dem chat-gepflegten `aufruf.link`-Feld. Anders als die geteilten CTA-QRs
// ist das Ziel ausgabenspezifisch, deshalb liegt der Code im Ausgaben-Ordner
// (qr-aufruf.png) — der wird bei jedem Lauf ohnehin neu befüllt. club_url()
// wie überall, damit der gedruckte Code auf die echte Domain zeigt.
$aufrufLink = trim (( string ) ( $oldAufruf [ 'link' ] ? ? '' ));
if ( $aufrufLink !== '' ) {
if ( generate_qr_png ( club_url ( $aufrufLink ), " { $destDir } /qr-aufruf.png " )) {
$log ( " QR-Code für die Aufruf-Seite (/ { $aufrufLink } ) erzeugt. " );
} else {
$log ( 'QR-Code für die Aufruf-Seite konnte nicht erzeugt werden — die Seite rendert ohne QR.' , 'WARN' );
}
}
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
// Momente-Seite: QR aufs Instagram-Profil (externe URL aus club.json, die
// einzige Nicht-Website-Adresse unter den Heft-QRs — ein gedruckter Code,
// keine Einbindung, Regel 1 bleibt unberührt).
$instagramUrl = ( string ) ( json_load ( 'club' )[ 'social' ][ 'instagram' ] ? ? '' );
if ( $instagramUrl !== '' && generate_qr_png ( $instagramUrl , PUBLIC_PATH . '/assets/img/stadionzeitung/qr-instagram.png' )) {
$log ( 'QR-Code fürs Instagram-Profil erzeugt.' );
} else {
$log ( 'QR-Code fürs Instagram-Profil konnte nicht erzeugt werden — Momente-Seite rendert ohne QR.' , 'WARN' );
}
// --- Termine-Snapshot für die Live-Seite "termine": die 5 zum Spieltag
// nächsten Termine, EINMALIG eingefroren (termine_build_lists($date)) —
// nicht bei jedem Seitenaufruf neu berechnet. Eine Ausgabe ist eine gedruckte
// Publikation: wer sie Monate später liest, soll weiter sehen, was am
// Spieltag anstand, nicht was von heute aus gesehen ansteht. Das heutige
// Spiel selbst (das Cover zeigt es schon) fliegt raus — sonst listet
// "Kommende Termine" die eigene Ausgabe. ---
$termineUpcoming = array_filter (
termine_build_lists ( $date )[ 'upcoming' ],
static fn ( array $item ) : bool => ( $item [ 'date' ] ? ? '' ) !== $date || ( $item [ 'type' ] ? ? '' ) !== 'spiel'
);
$termineSnapshot = array_slice ( array_values ( $termineUpcoming ), 0 , 5 );
$log ( count ( $termineSnapshot ) . ' Termine für die Live-Seite eingefroren (Stand ' . $date . ').' );
// --- Porträtfoto für die Vorwort-Signatur: normales Foto (kein Alpha), daher
// über resize_image_variants() wie eine Bild-Seite — kleine Breiten reichen,
// es wird nur als kleiner Kreis neben der Signatur gezeigt. Optional. ---
$verfasserPhoto = null ;
if ( $verfasser !== null ) {
$verfasserDest = " { $destDir } /verfasser " ;
try {
$variants = resize_image_variants ( $verfasser , $verfasserDest , [ 120 , 240 ], 82 );
$generatedWidths = array_values ( array_unique ( array_column ( $variants , 'width' )));
sort ( $generatedWidths );
$verfasserPhoto = [
'base' => " img/stadionzeitung/ { $slug } /verfasser " ,
'widths' => $generatedWidths ,
'alt' => '' ,
];
$log ( 'Verfasserfoto übernommen.' );
} catch ( RuntimeException $e ) {
$log ( " Verfasserfoto konnte nicht verarbeitet werden: { $e -> getMessage () } " , 'WARN' );
}
}
// --- data/stadionzeitung.json atomar schreiben (bestehenden Slug ersetzen) ---
$issues = array_values ( array_filter (
$existing [ 'issues' ] ? ? [],
static fn ( array $issue ) : bool => ( $issue [ 'slug' ] ? ? '' ) !== $slug
));
// Immer als Entwurf — auch beim erneuten Lauf über eine schon veröffentlichte
// Ausgabe: der Lauf ersetzt die Seiten, also muss der Stand neu geprüft und
// neu veröffentlicht werden (der alte snapshot passt nicht mehr und fliegt mit).
if ( $oldIssue !== null && ( $oldIssue [ 'status' ] ? ? '' ) === 'veroeffentlicht' ) {
$log ( " Ausgabe ' { $slug } ' war veröffentlicht — steht jetzt wieder auf Entwurf, nach Prüfung neu veröffentlichen (bin/stadionzeitung-publish.php). " , 'WARN' );
}
$issues [] = [
'slug' => $slug ,
'opponent' => $opponent ,
'date' => $date ,
'team_key' => $teamKey ,
'status' => 'entwurf' ,
'pages' => $total ,
'player_image' => $playerImage ,
'qr_image' => $qrImage ,
'vorwort' => $oldVorwort ,
'interview' => $oldInterview ,
Stadionzeitung Ausgabe 2 (TSV Ködnitz, 15.08.): Entwurf + neue Aufruf-Seite
- Neue generische Live-Seite `aufruf` (erste Nutzung: Sportfest-Helferaufruf):
Komponente, Dispatcher, Skript-Einbindung (Planung im Aufschlag, QR aus
aufruf.link, Feld-Carryover), CSS, optionales kleines Poster rechts.
- Ausgabe 2026-08-15-tsv-koednitz als ENTWURF: Vorwort (Benedikt, Revanche
dezent), Interview (Ali, 3 Fragen), Sportfest-Helferseite mit Kontakten aus
ansprechpartner.json; Rückseite/Cover wie Hutschdorf wiederverwendet.
- Interview-Frage größer/prominenter gesetzt.
- Sportfest-Plakat-Anzeige deaktiviert (redundant zur Helferseite) -> 40 Seiten.
Sync-Dateien (instagram/matchcenter) bleiben gitignored, Snapshot wird beim
Veröffentlichen auf dem Server eingefroren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AFnFYLm2MoBskyk9mc84ZW
2026-08-13 17:27:41 +02:00
'aufruf' => $oldAufruf ,
Stadionzeitung, Veranstaltungen, News-Artikel, Lightbox und Anzeigen-Verteilung
Stadionzeitung: Live-Seiten (Tabellen, Ergebnisse, Vorschau, Termine, News,
Kontakt, Historie, Sportheim, Partner, Momente, Impressum), automatisches Cover,
Swipe-Viewer mit Vollbildmodus und die Druckfassung als Falt-Heft. Anzeigen
kommen jetzt aus dem Bestand und werden gleichmäßig verteilt, nie mehr als zwei
hintereinander (vorher vier Blöcke à fünf). Die Doppelseiten-Regel rechnet das
Skript selbst, statt sie von Hand zu prüfen.
Veranstaltungen: Bereich /veranstaltungen mit Detailseite pro Fest, Lebenszyklus
über das Datum (Ankündigung vor dem Fest, Rückblick danach), Galerie mit
Lightbox. veranstaltungen.json ist dritte Termin-Quelle, damit ein Fest nie
doppelt gepflegt wird.
News-Artikel als dritte dynamische Prefix-Route, gemeinsamer detail-head.
Behobene Fehler:
- Wappen-Kontexte setzten Zeilen-Layout auf .crest statt auf die Zeile; das
Wappen wurde zum Grid, Vereinsname und Tore rutschten darunter zusammen
(Ergebnis-Rückblick, Vorschau, Cover).
- --header-h (60px) unterschätzt die Kopfleiste um 32px (Logo 72px + 20px
Versatz): neues abgeleitetes --header-space, sonst klebten Zurück-Links
unter dem Logo.
- base_url in der Produktions-Config ohne www, während .htaccess auf www
umleitet; preflight prüft das jetzt.
- .lightbox brauchte display:none mit (0,2,0), sonst lag das Overlay offen.
Neue Werkzeuge: bin/anzeige-add.php (Anzeigen-Bestand), bin/veranstaltung-bilder.php
(Plakate und Galerien, Alt-Texte folgen der Quelldatei), bin/qr.php (Druck-QR mit
Warnung, wenn base_url nicht auf die echte Domain zeigt).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7
2026-07-31 14:55:27 +02:00
'verfasser_photo' => $verfasserPhoto ,
'termine_snapshot' => $termineSnapshot ,
'images' => $images ,
];
usort ( $issues , static fn ( array $a , array $b ) : int => strcmp ( $b [ 'date' ], $a [ 'date' ]));
$payload = json_encode ([ 'issues' => $issues ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
$tmp = $cacheFile . '.tmp' ;
file_put_contents ( $tmp , $payload );
rename ( $tmp , $cacheFile );
$log ( " OK: Ausgabe ' { $slug } ' ( { $opponent } , { $date } , Team { $teamKey } ) mit { $total } Seiten als Entwurf gespeichert. " );
echo " Fertig (ENTWURF): /stadionzeitung/ { $slug } ( { $total } Seiten, davon { $adCount } Anzeigen aus dem Bestand) \n " ;
echo 'Anzeigen verteilt: ' . implode ( '/' , $verteilung ) . ' je Lücke, längster Block ' . max ( $verteilung ? : [ 0 ]) . " . \n " ;
echo " Veröffentlichen nach Prüfung: php bin/stadionzeitung-publish.php { $slug } \n " ;