Files
tsv08kulmbach-website/app/pages/stadionzeitung-ausgabe.php
fs a101b0c3ec 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

141 lines
6.6 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
/**
* Einzelne Stadionzeitung-Ausgabe: dynamische Route stadionzeitung/<slug>,
* siehe public/index.php (Prefix-Match, da app/routes.php nur exakte Slugs
* kennt). Ermittelt den Ausgaben-Slug selbst wieder aus der URL, Daten aus
* data/stadionzeitung.json (maschinenverwaltet von bin/stadionzeitung-add.php).
*
* Bewusst OHNE das übliche hero-Component: der Swipe-Viewer soll so viel
* Bildschirmhöhe wie möglich bekommen (gerade am Handy) — stattdessen ein
* schmaler Kopfbereich direkt über dem Viewer.
*/
$urlSlug = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
$ausgabeSlug = substr($urlSlug, strlen('stadionzeitung/'));
/*
* Reservierter Slug „aktuell": leitet auf die neueste veröffentlichte Ausgabe
* weiter. Damit zeigt ein GEDRUCKTER QR-Code (Plakat, Aushang) dauerhaft auf die
* jeweils aktuelle Ausgabe, ohne dass zu jedem Heimspiel ein neuer Code
* entstehen muss. Kollision ausgeschlossen: Ausgaben-Slugs beginnen immer mit
* dem Datum (2026-08-01-…).
*
* Drei Dinge sind hier load-bearing:
* 1. **302, nie 301.** Ein 301 wird von Browsern und Crawlern dauerhaft
* gemerkt — das Plakat zeigte dann für immer auf die erste Ausgabe, und
* zwar unreparierbar auf allen Geräten, die ihn einmal gesehen haben.
* 2. `no-store`, damit ein Handy, das im letzten Monat gescannt hat, nicht
* das alte Ziel wiederverwendet.
* 3. Ohne veröffentlichte Ausgabe geht es aufs Archiv statt in einen 404 —
* ein gedruckter Code darf nie ins Leere führen.
*/
if ($ausgabeSlug === 'aktuell') {
$latest = stadionzeitung_latest();
header('Cache-Control: no-store');
header(
'Location: ' . ($latest !== null ? url('stadionzeitung/' . $latest['slug']) : url('stadionzeitung')),
true,
302
);
// exit, nicht return: nach einem `return` liefe in public/index.php noch
// layout.php und hängte einen kompletten HTML-Body an die Weiterleitung.
// Der Ausgabepuffer aus render_page() ist hier noch leer, es geht also
// nichts verloren.
exit;
}
$issue = null;
foreach (json_load('stadionzeitung')['issues'] ?? [] as $entry) {
if (($entry['slug'] ?? '') === $ausgabeSlug) {
$issue = $entry;
break;
}
}
if ($issue === null) {
// http_response_code(404) ist globaler Engine-State (kein Scope-Problem wie
// $meta/$current) — public/index.php liest ihn nach render_page() wieder aus,
// um $current auf '404' zu setzen (noindex + kein Self-Canonical in meta.php).
http_response_code(404);
require APP_PATH . '/pages/404.php';
return;
}
// Entwurf vs. veröffentlicht (Interview-Beschluss 29.07.2026): Entwürfe sind
// „unsichtbar, aber offen" — sie fehlen im Archiv und in der Sitemap und
// tragen noindex, die direkte URL funktioniert aber (zum Gegenlesen ohne
// Login-System). Die Druckfassung (?druck=1) montiert dieselben Seiten in
// Broschüren-Reihenfolge fürs gefaltete A5-Heft.
$isDraft = !stadionzeitung_is_published($issue);
$isDruck = isset($_GET['druck']);
// Erste Seite ist seit dem automatischen Cover (Phase 3) eine Live-Seite ohne
// eigenes Bild — og_image gibt es nur, wenn die Ausgabe (noch/wieder) mit
// einem Bild beginnt, sonst greift das globale Default-OG-Bild (meta.php).
$firstImage = $issue['images'][0] ?? null;
$cover = ($firstImage !== null && ($firstImage['type'] ?? 'image') === 'image') ? $firstImage : null;
$meta = [
'title' => 'Stadionzeitung gegen ' . $issue['opponent'] . ($isDruck ? ' (Druckfassung)' : ''),
'description' => 'Stadionzeitung zum Heimspiel gegen ' . $issue['opponent'] . ' am ' . german_date($issue['date']) . ' digital zum Durchblättern.',
'scripts' => [$isDruck ? 'stadionzeitung-druck.js' : 'stadionzeitung-viewer.js'],
'schema' => page_schema([
['name' => 'Startseite', 'slug' => ''],
['name' => 'Fußball', 'slug' => 'fussball'],
['name' => 'Stadionzeitung', 'slug' => 'stadionzeitung'],
['name' => 'Gegen ' . $issue['opponent'], 'slug' => 'stadionzeitung/' . $issue['slug']],
]),
];
if ($cover !== null) {
$meta['og_image'] = $cover['base'] . '-' . max($cover['widths']) . '.jpg';
}
// Entwürfe und die Druckfassung (parametrisierte Zweitansicht derselben
// Inhalte) gehören nicht in den Index.
if ($isDraft || $isDruck) {
$meta['noindex'] = true;
}
?>
<?php
// Kopfleiste: im Viewer Titel + Datum, in der Druckfassung stattdessen die
// Blatt-Bilanz und ein Drucken-Knopf griffbereit oben (der ausführliche
// Hinweis + Button am Seitenende bleibt).
$druckSeiten = count($issue['images'] ?? []);
$druckBlaetter = (int) ceil($druckSeiten / 4);
?>
<?php /* Kopfleiste in drei Zonen und zwei Reihen: oben Rückweg links, Titel mittig,
Aktion rechts — darunter eine ruhige Statuszeile. Die Badges standen
früher neben dem Titel und machten die Mitte unruhig; Zustand („Entwurf",
„Druckfassung") und Bilanz sind Information, keine Überschrift. */ ?>
<header class="stadionzeitung-issue-head<?= $isDruck ? ' stadionzeitung-issue-head--druck' : '' ?>">
<div class="container stadionzeitung-issue-head__inner">
<a class="stadionzeitung-issue-head__back" href="<?= e($isDruck ? url('stadionzeitung/' . $issue['slug']) : url('stadionzeitung')) ?>">
<?= icon('chevron-left') ?> <?= $isDruck ? 'Ausgabe' : 'Archiv' ?>
</a>
<p class="stadionzeitung-issue-head__title">Gegen <?= e($issue['opponent']) ?></p>
<?php if ($isDruck): ?>
<p class="stadionzeitung-issue-head__actions">
<button class="btn btn--sm stadionzeitung-issue-head__print" type="button" data-druck-print hidden>
<?= icon('printer') ?> Drucken
</button>
</p>
<?php else: ?>
<p class="stadionzeitung-issue-head__date">
<time datetime="<?= e($issue['date']) ?>"><?= e(german_date($issue['date'])) ?></time>
</p>
<?php endif; ?>
<?php if ($isDraft || $isDruck): ?>
<p class="stadionzeitung-issue-head__state">
<?php if ($isDraft): ?>
<span class="stadionzeitung-issue-head__badge stadionzeitung-issue-head__badge--entwurf">Entwurf</span>
<?php endif; ?>
<?php if ($isDruck): ?>
<span>Druckfassung · <?= $druckBlaetter ?> Blätter · <?= $druckSeiten ?> Seiten</span>
<?php endif; ?>
</p>
<?php endif; ?>
</div>
</header>
<?php component($isDruck ? 'stadionzeitung-druck' : 'stadionzeitung-viewer', ['issue' => $issue]); ?>