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
This commit is contained in:
@@ -31,7 +31,8 @@ $anchor_slug = static function (string $s): string {
|
||||
|
||||
$meta = [
|
||||
'title' => 'Ansprechpartner',
|
||||
'description' => 'Ansprechpartner und Kontaktwege beim TSV 1908 Kulmbach – Geschäftsstelle, Turnabteilung und Vorstand mit E-Mail und Telefon auf einen Blick.',
|
||||
'description' => 'Alle Ansprechpartner des TSV 08 Kulmbach auf einen Blick: Herren-, Frauen- und Jugendfußball sowie Turnen, mit Telefonnummer oder E-Mail. Sprich uns direkt an!',
|
||||
'scripts' => ['persons-tabs.js'],
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
@@ -40,7 +41,7 @@ $meta = [
|
||||
];
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'bottom-left',
|
||||
'align' => 'center',
|
||||
'title' => 'Ansprechpartner',
|
||||
'text' => $page['intro'] ?? '',
|
||||
'image' => [
|
||||
@@ -50,14 +51,29 @@ component('hero', ['hero' => [
|
||||
],
|
||||
]]);
|
||||
|
||||
$i = 0;
|
||||
foreach ($groups as $label => $depts):
|
||||
?>
|
||||
<?php /* EINE Sektion für beide Bereiche, Tab-Leiste direkt über den
|
||||
Karten (assets/js/persons-tabs.js schaltet um und spiegelt den
|
||||
aktiven Bereich in die URL — #kontakt-turnen ist teilbar).
|
||||
Ohne JS bleibt die Leiste hidden und beide Panels stehen mit
|
||||
ihren Überschriften untereinander. */ ?>
|
||||
<section class="section section--tinted" id="kontakt" data-persons-tabs>
|
||||
<div class="container">
|
||||
<div class="filter-bar persons-tabs__bar" role="group" aria-label="Bereich wählen" hidden data-persons-tabs-bar>
|
||||
<?php $i = 0; foreach (array_keys($groups) as $label): ?>
|
||||
<button type="button" class="tab filter-bar__tab" data-tab-target="kontakt-<?= e($anchor_slug($label)) ?>" aria-pressed="<?= $i++ === 0 ? 'true' : 'false' ?>"><?= e($label) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php
|
||||
foreach ($groups as $label => $depts) {
|
||||
component('person-grid', [
|
||||
'departments' => $depts,
|
||||
'title' => $label,
|
||||
'intro' => '',
|
||||
'anchor' => 'kontakt-' . $anchor_slug($label),
|
||||
'tinted' => ($i % 2 === 0),
|
||||
'embedded' => true,
|
||||
]);
|
||||
$i++;
|
||||
endforeach;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
|
||||
@@ -5,10 +5,16 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Datenschutzerklärung — zugeschnitten auf den tatsächlichen Stack:
|
||||
* komplett self-hosted, keine Tracker/Analyse/Werbe-Dienste, keine externen
|
||||
* Fonts/Maps/Embeds. Einzige Verarbeitungen: Server-Logs (Hoster), die drei
|
||||
* Formulare (Versand über den All-Inkl-Mailserver, also derselbe Auftrags-
|
||||
* verarbeiter wie beim Hosting), server-seitig gecachter Instagram-Feed
|
||||
* (Bilder von unserer Domain). Vereinsdaten aus club.json.
|
||||
* Fonts/Maps/Embeds. Einzige Verarbeitungen: Server-Logs (Hoster), die vier
|
||||
* Formulare (Kontakt, Turnabteilung, Mitgliedsantrag, Sportheimbuchung; Versand
|
||||
* über den All-Inkl-Mailserver, also derselbe Auftragsverarbeiter wie beim
|
||||
* Hosting), server-seitig gecachter Instagram-Feed (Bilder von unserer Domain).
|
||||
* Vereinsdaten aus club.json.
|
||||
*
|
||||
* Alle Empfänger liegen auf unserer eigenen Domain bei All-Inkl (info@,
|
||||
* turnen@) — deshalb stimmt die Aussage weiter unten, dass kein zusätzlicher
|
||||
* E-Mail-Dienstleister im Spiel ist. Zeigt ein Postfach künftig auf einen
|
||||
* fremden Anbieter, muss dieser Satz mit.
|
||||
*/
|
||||
$club = json_load('club');
|
||||
$addr = $club['address'] ?? [];
|
||||
@@ -36,7 +42,7 @@ $meta = [
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="legal__body">
|
||||
<div class="legal__body prose">
|
||||
<section>
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>Verantwortlich für die Datenverarbeitung auf dieser Website ist:</p>
|
||||
@@ -123,6 +129,12 @@ $meta = [
|
||||
Tracking-Zwecken</strong> und bindet keine Dienste ein, die solche Cookies
|
||||
setzen würden. Aus diesem Grund benötigen wir auch kein Cookie-Banner.
|
||||
</p>
|
||||
<p>
|
||||
Eine Ausnahme betrifft nur den passwortgeschützten internen Bereich
|
||||
(siehe „Interne Dateiablage“): Wer sich dort anmeldet, erhält ein
|
||||
technisch notwendiges Sitzungs-Cookie, das die Anmeldung für die Dauer
|
||||
der Sitzung merkt. Ohne dieses Cookie ist eine Anmeldung nicht möglich.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
@@ -152,6 +164,15 @@ $meta = [
|
||||
E-Mail-Adresse, optional die Telefonnummer sowie Art, Datum und Personenzahl
|
||||
deiner Veranstaltung, um dir ein Angebot machen zu können.
|
||||
</p>
|
||||
<p>
|
||||
Das <strong>Anfrageformular der Turnabteilung</strong> auf
|
||||
<a href="<?= e(url('turnen')) ?>">/turnen</a> erhebt dieselben Angaben wie das
|
||||
Kontaktformular, statt eines Betreffs jedoch die Turngruppe, für die du dich
|
||||
interessierst; eine Nachricht ist dort freiwillig. Diese Anfragen gehen an das
|
||||
Postfach der Turnabteilung
|
||||
(<a href="mailto:<?= e($club['email_turnen']) ?>"><?= e($club['email_turnen']) ?></a>),
|
||||
weil die Abteilung sie selbst beantwortet.
|
||||
</p>
|
||||
<p>
|
||||
Rechtsgrundlage ist deine Einwilligung (Art. 6 Abs. 1 lit. a DSGVO), die du
|
||||
mit dem Absenden erteilst, sowie unser berechtigtes Interesse bzw. die
|
||||
@@ -160,6 +181,18 @@ $meta = [
|
||||
deine Anfrage abschließend bearbeitet ist und keine gesetzlichen
|
||||
Aufbewahrungspflichten entgegenstehen.
|
||||
</p>
|
||||
<p>
|
||||
Schickst du uns Material für unsere <strong>Vereinshistorie</strong> – Fotos,
|
||||
Zeitungsausschnitte, Dokumente oder Erinnerungen – an
|
||||
<a href="mailto:<?= e($club['email_historie']) ?>"><?= e($club['email_historie']) ?></a>,
|
||||
bewahren wir es gemeinsam mit deinem Namen als Quellenangabe in unserem
|
||||
Vereinsarchiv auf. Grundlage dafür ist unser berechtigtes Interesse an der
|
||||
Dokumentation der Vereinsgeschichte (Art. 6 Abs. 1 lit. f DSGVO).
|
||||
Veröffentlichen – auf dieser Website, in einer Chronik oder Festschrift –
|
||||
tun wir nur, was du uns ausdrücklich freigegeben hast (Art. 6 Abs. 1 lit. a
|
||||
DSGVO); diese Freigabe kannst du jederzeit widerrufen. Sind auf Bildern
|
||||
weitere Personen erkennbar, sag uns bitte, was du darüber weißt.
|
||||
</p>
|
||||
<p>
|
||||
Der technische Versand der Formular-Nachrichten als E-Mail läuft über den
|
||||
Mailserver unseres Webhosters ALL-INKL.COM (siehe oben) — die Daten bleiben
|
||||
@@ -177,6 +210,35 @@ $meta = [
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Interne Dateiablage</h2>
|
||||
<p>
|
||||
Unter <code>/dateien</code> betreiben wir einen
|
||||
<strong>passwortgeschützten Bereich</strong>, in dem Mitglieder Fotos und
|
||||
Videos von Vereinsveranstaltungen für die Website abliefern können. Er ist
|
||||
nicht öffentlich, nicht verlinkt und von Suchmaschinen ausgenommen; die
|
||||
Zugangsdaten geben wir nur intern weiter.
|
||||
</p>
|
||||
<p>
|
||||
Verarbeitet werden dort ausschließlich die hochgeladenen Dateien selbst
|
||||
(einschließlich der darin enthaltenen Aufnahmen von Personen und der
|
||||
technischen Bilddaten wie Aufnahmezeit oder Kameramodell) sowie der
|
||||
gewählte Benutzername mit dem Zeitpunkt der Anmeldung. Fehlgeschlagene
|
||||
Anmeldeversuche halten wir zum Schutz gegen automatisierte Angriffe kurz
|
||||
fest, dabei die IP-Adresse nur als nicht zurückrechenbare Prüfsumme.
|
||||
</p>
|
||||
<p>
|
||||
Die Dateien liegen ausschließlich auf unserem eigenen Webspace, außerhalb
|
||||
des öffentlich erreichbaren Bereichs, und werden nicht an Dritte
|
||||
weitergegeben. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO
|
||||
(berechtigtes Interesse an der Vereinskommunikation); für die
|
||||
Veröffentlichung von Personenaufnahmen auf der Website gilt zusätzlich der
|
||||
Abschnitt „Foto- und Videoaufnahmen“. Wer nicht möchte, dass eine Aufnahme
|
||||
von ihm dort abgelegt oder veröffentlicht wird, kann sich jederzeit an die
|
||||
oben genannten Kontaktdaten wenden – wir löschen sie dann.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Keine Analyse-, Tracking- oder Werbedienste</h2>
|
||||
<p>
|
||||
|
||||
@@ -21,7 +21,7 @@ $cards = [
|
||||
|
||||
$meta = [
|
||||
'title' => 'Fußball in Kulmbach',
|
||||
'description' => 'Herren-, Frauen- und Jugendfußball beim TSV 08 Kulmbach: zwei Herrenmannschaften, Frauenmannschaft und Nachwuchs – aktiv werden im Katzbachtal.',
|
||||
'description' => 'Fußball in Kulmbach beim TSV 08: zwei Herrenmannschaften, Frauenfußball und Jugendteams von der G- bis zur C-Jugend. Komm zum Probetraining im Katzbachtal!',
|
||||
'og_image' => 'img/pages/fussball-hero-1600.jpg',
|
||||
'schema' => page_schema(
|
||||
[
|
||||
|
||||
@@ -3,14 +3,28 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Vereinshistorie — schmaler Hero + interaktive Timeline.
|
||||
* Inhalte: data/historie.json (Single Source of Truth).
|
||||
* Vereinshistorie — schmaler Hero + interaktive Timeline + Aufruf zum Mitsammeln.
|
||||
* Inhalte: data/historie.json (Single Source of Truth), Mailadresse aus data/club.json.
|
||||
*/
|
||||
$historie = json_load('historie');
|
||||
$club = json_load('club');
|
||||
|
||||
// Die Adresse steht ausschließlich in club.json — der Fließtext in historie.json
|
||||
// markiert ihre Stelle mit {mail}, CTA-Label und mailto-Ziel entstehen hier.
|
||||
// Betreff vorbelegt, damit Zuschriften direkt zuordenbar sind.
|
||||
$mail = $club['email_historie'];
|
||||
$contribute = $historie['contribute'] + ['cta' => [
|
||||
'label' => $mail,
|
||||
'href' => 'mailto:' . $mail . '?subject=' . rawurlencode('Material für die Vereinshistorie'),
|
||||
]];
|
||||
$contribute['text'] = array_map(
|
||||
static fn (string $p): string => strtr($p, ['{mail}' => $mail]),
|
||||
$contribute['text']
|
||||
);
|
||||
|
||||
$meta = [
|
||||
'title' => 'Vereinshistorie – Der Katzbachtaler Weg',
|
||||
'description' => 'Erlebe die bewegte Geschichte des TSV 08 Kulmbach – von den ersten Spielen an der Bayreuther Straße 1947 bis zur Hans-Rausch-Sportanlage im Katzbachtal.',
|
||||
'title' => 'Vereinshistorie: Der Katzbachtaler Weg',
|
||||
'description' => 'Erlebe unsere bewegte Geschichte, von den ersten Spielen an der Bayreuther Straße 1947 bis zur Hans-Rausch-Sportanlage im Katzbachtal.',
|
||||
'scripts' => ['timeline.js'],
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
@@ -21,3 +35,4 @@ $meta = [
|
||||
|
||||
component('hero', ['hero' => $historie['hero']]);
|
||||
component('timeline', ['events' => $historie['events']]);
|
||||
component('section', ['section' => $contribute]);
|
||||
|
||||
@@ -10,14 +10,17 @@ declare(strict_types=1);
|
||||
$home = json_load('home');
|
||||
|
||||
$meta = [
|
||||
'title' => 'Fußball & Turnen in Kulmbach – TSV 08 Kulmbach',
|
||||
'title' => 'Fußball & Turnen in Kulmbach | TSV 08 Kulmbach',
|
||||
'title_absolute' => true,
|
||||
'description' => 'Dein Sportverein für Fußball und Turnen in Kulmbach: Jugend-, Herren- und Frauenfußball, Turnen und Trampolin – seit 1908 im Katzbachtal. Jetzt Mitglied werden!',
|
||||
'scripts' => ['form.js', 'carousel.js'],
|
||||
'description' => 'Dein Sportverein für Fußball und Turnen in Kulmbach: Jugend-, Herren- und Frauenfußball, Turnen und Trampolin: seit 1908 in Kulmbach. Jetzt Mitglied werden!',
|
||||
'scripts' => ['form.js', 'carousel.js', 'crest.js'],
|
||||
'schema' => [website_schema()],
|
||||
];
|
||||
|
||||
component('hero', ['hero' => $home['hero']]);
|
||||
// Die nächsten drei Termine als Live-Aufhänger im Hero — Spiele und
|
||||
// Vereins-Events gemischt, aus upcoming_highlights() (baut auf derselben
|
||||
// Merge-Logik auf wie /termine). Keine Termine → der Hero rendert ohne Karte.
|
||||
component('hero', ['hero' => $home['hero'], 'upcoming' => upcoming_highlights(3)]);
|
||||
component('banner-slider');
|
||||
component('instagram-feed');
|
||||
|
||||
@@ -25,12 +28,10 @@ foreach ($home['sections'] as $section) {
|
||||
component('section', ['section' => $section]);
|
||||
}
|
||||
|
||||
component('stats', ['stats' => $home['stats']]);
|
||||
component('partner-grid', ['intro' => $home['partners']]);
|
||||
// Auf der Startseite bewusst nur die Haupt-Ansprechpartner je Bereich; die
|
||||
// vollständige Turn-Disziplin-Liste lebt auf /ansprechpartner.
|
||||
component('person-grid', [
|
||||
'departments' => ['Herrenfußball', 'Frauenfußball', 'Jugendfußball', 'Erwachsenenturnen'],
|
||||
'columns' => 3,
|
||||
]);
|
||||
component('contact-form', ['intro' => $home['contact']]);
|
||||
|
||||
@@ -28,7 +28,7 @@ $meta = [
|
||||
<p class="legal__lead">Angaben gemäß § 5 Digitale-Dienste-Gesetz (DDG).</p>
|
||||
</div>
|
||||
|
||||
<div class="legal__body">
|
||||
<div class="legal__body prose">
|
||||
<section>
|
||||
<h2>Anbieter</h2>
|
||||
<div class="legal__card">
|
||||
|
||||
@@ -21,7 +21,7 @@ $contacts = array_values(array_filter(
|
||||
|
||||
$meta = [
|
||||
'title' => 'Jugendfußball in Kulmbach',
|
||||
'description' => 'Jugendfußball in Kulmbach – entdecke unsere Mannschaften von der G- bis zur C-Jugend, Betreuer und Trainingszeiten. Probiere Fußball in familiärer Atmosphäre aus.',
|
||||
'description' => 'Jugendfußball in Kulmbach beim TSV 08: Teams von der G- bis zur C-Jugend, Trainingszeiten und kostenloses Schnuppertraining. Komm vorbei ins Katzbachtal!',
|
||||
'og_image' => 'img/pages/jugendfussball-hero-1440.jpg',
|
||||
'scripts' => count($contacts) > 1 ? ['contact-slider.js'] : [],
|
||||
'schema' => page_schema(
|
||||
@@ -70,7 +70,7 @@ component('age-groups', ['groups' => $page['groups'] ?? []]);
|
||||
|
||||
component('faq-contact', [
|
||||
'faq' => $page['faq'] ?? [],
|
||||
'faqIntro' => 'Ruf uns direkt an oder schreib uns unkompliziert per WhatsApp – wir beantworten alle Fragen rund um Schnuppertraining und die passende Altersgruppe.',
|
||||
'faqIntro' => 'Ruf uns direkt an oder schreib uns unkompliziert per WhatsApp. Wir beantworten alle Fragen rund um Schnuppertraining und die passende Altersgruppe.',
|
||||
'persons' => $contacts,
|
||||
'contactTitle' => '',
|
||||
'anchor' => 'kontakt',
|
||||
|
||||
@@ -10,7 +10,7 @@ $page = json_load('kontakt');
|
||||
|
||||
$meta = [
|
||||
'title' => 'Kontakt',
|
||||
'description' => 'Kontakt zum TSV 08 Kulmbach: Schreib uns über das Formular – zu Fußball, Turnen oder einem allgemeinen Anliegen. Wir melden uns so schnell wie möglich.',
|
||||
'description' => 'Kontakt zum TSV 08 Kulmbach: Schreib uns über das Formular zu Fußball, Turnen oder deinem Anliegen. Wir melden uns so schnell wie möglich.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'scripts' => ['form.js'],
|
||||
'schema' => page_schema([
|
||||
|
||||
@@ -14,9 +14,9 @@ $teams = $mc['teams'] ?? [];
|
||||
$upcoming = $mc['upcoming'] ?? [];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Matchcenter – Spiele, Ergebnisse & Tabellen',
|
||||
'title' => 'Matchcenter · Spiele, Ergebnisse & Tabellen',
|
||||
'description' => 'Spielplan, Ergebnisse und Tabellen der Aktivenmannschaften des TSV 08 Kulmbach: 1. Mannschaft und Frauenmannschaft – automatisch aktuell.',
|
||||
'og_image' => 'img/pages/fussball-hero-1600.jpg',
|
||||
'og_image' => 'img/pages/matchcenter-hero-1600.jpg',
|
||||
'scripts' => ['crest.js', 'group-tabs.js'],
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
@@ -30,11 +30,16 @@ $meta = [
|
||||
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'bottom-left',
|
||||
'align' => 'center',
|
||||
'title' => 'Matchcenter',
|
||||
'text' => 'Alle Spiele, Ergebnisse und Tabellen unserer Aktivenmannschaften – immer aktuell.',
|
||||
/* Eigenes Hero-Motiv (Spielszene) statt des leeren Platzes von /fussball — die Seite
|
||||
zeigt Spiele, also zeigt der Kopf welche. Quelle: 14_mrz.jpg aus der alten Seite,
|
||||
vor dem Skalieren oben um 230 px beschnitten (1620x1080 → 1620x850). Der Beschnitt
|
||||
rückt die Spielszene in die Bildmitte, sonst schneidet der flache Compact-Hero sie
|
||||
auf Mobil komplett weg. */
|
||||
'image' => [
|
||||
'base' => 'img/pages/fussball-hero',
|
||||
'base' => 'img/pages/matchcenter-hero',
|
||||
'widths' => [640, 1000, 1600],
|
||||
'alt' => '',
|
||||
],
|
||||
|
||||
@@ -19,7 +19,7 @@ $feeOptions = array_map(
|
||||
|
||||
$meta = [
|
||||
'title' => 'Mitglied werden',
|
||||
'description' => 'Mitglied werden beim TSV 08 Kulmbach: Fußball online anmelden oder Turnen per Aufnahmeantrag – alle Beiträge und Infos für deinen Vereinsbeitritt.',
|
||||
'description' => 'Mitglied werden beim TSV 08 Kulmbach: Fußball online anmelden oder Turnen per Aufnahmeantrag. Alle Beiträge und Infos für deinen Vereinsbeitritt.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'scripts' => ['form.js'],
|
||||
'schema' => page_schema(
|
||||
@@ -33,7 +33,9 @@ $meta = [
|
||||
|
||||
component('hero', ['hero' => $page['hero']]);
|
||||
|
||||
component('department-choice', ['choice' => $page['choice'], 'anchor' => 'abteilung']);
|
||||
// Titel nur für Screenreader: der Hero sagt bereits „Wähle deine Abteilung", und die
|
||||
// beiden Karten sprechen optisch für sich. Die h2 bleibt für die Heading-Kette stehen.
|
||||
component('department-choice', ['choice' => $page['choice'], 'anchor' => 'abteilung', 'title_hidden' => true]);
|
||||
?>
|
||||
<section class="section membership-signup" id="anmeldung-fussball">
|
||||
<div class="container membership-signup__inner">
|
||||
|
||||
@@ -26,8 +26,8 @@ $contact_for = static function (array $pos) use ($directory): ?array {
|
||||
|
||||
$meta = [
|
||||
'title' => 'Mitmachen & Ehrenamt',
|
||||
'description' => 'Ehrenamt beim TSV 08 Kulmbach: offene Aufgaben als Jugendtrainer, Betreuer oder Helfer bei Veranstaltungen – mit direktem Ansprechpartner, ohne Vorerfahrung.',
|
||||
'og_image' => 'img/pages/jugendfussball-hero-1440.jpg',
|
||||
'description' => 'Ehrenamt beim TSV 08 Kulmbach: offene Aufgaben als Jugendtrainer, Betreuer oder Helfer bei Veranstaltungen. Direkte Ansprechpartner, keine Vorerfahrung nötig!',
|
||||
'og_image' => 'img/pages/mitmachen-hero-1600.jpg',
|
||||
'scripts' => ['position-filter.js'],
|
||||
'schema' => array_merge(
|
||||
page_schema(
|
||||
@@ -61,7 +61,7 @@ component('hero', ['hero' => $page['hero']]);
|
||||
<?php component('position-card', ['position' => $pos, 'contact' => $contact_for($pos)]); ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<p class="positions__empty" data-position-empty hidden>In diesem Bereich sind aktuell keine Stellen ausgeschrieben. Schau bald wieder vorbei – oder sprich uns einfach <a href="<?= e(url('') . '#kontakt') ?>">direkt an</a>, wir finden gemeinsam eine passende Aufgabe.</p>
|
||||
<p class="positions__empty" data-position-empty hidden>In diesem Bereich sind aktuell keine Stellen ausgeschrieben. Schau bald wieder vorbei oder sprich uns einfach <a href="<?= e(url('') . '#kontakt') ?>">direkt an</a>, wir finden gemeinsam eine passende Aufgabe.</p>
|
||||
</div>
|
||||
</section>
|
||||
<?php if (!empty($page['arguments']['items'])): ?>
|
||||
|
||||
82
app/pages/news-artikel.php
Normal file
82
app/pages/news-artikel.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Einzelne Meldung als Artikel: dynamische Route news/<slug>, siehe
|
||||
* public/index.php (Prefix-Match, da app/routes.php nur exakte Slugs kennt).
|
||||
* Ermittelt den Slug selbst wieder aus der URL, Daten aus data/news.json
|
||||
* (chat-gepflegt, keine Automatisierung).
|
||||
*
|
||||
* Die Seite existiert nur für Meldungen MIT `artikel` (news_has_article()):
|
||||
* eine Kurzmeldung bleibt eine Feed-Zeile auf /news, ihre URL liefert 404. Ohne
|
||||
* diese Bedingung stünde unter jedem Slug eine Seite mit nichts als der
|
||||
* Überschrift.
|
||||
*
|
||||
* Geladen wird bewusst UNGEFILTERT (news_find(), nicht news_load()): ein
|
||||
* Entwurf muss unter seiner direkten URL lesbar bleiben, damit er gegengelesen
|
||||
* werden kann. Sichtbar wird er dadurch nirgends sonst, und er trägt noindex.
|
||||
*
|
||||
* Bewusst OHNE hero (wie die Veranstaltungs-Detailseite): das Titelbild ist der
|
||||
* Aufmacher im Artikelkörper, nicht ein cover-beschnittener Hintergrund.
|
||||
*/
|
||||
$urlSlug = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
|
||||
$newsSlug = substr($urlSlug, strlen('news/'));
|
||||
|
||||
$n = $newsSlug !== '' ? news_find($newsSlug) : null;
|
||||
|
||||
if ($n === null || !news_has_article($n)) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
$isDraft = !news_is_published($n);
|
||||
$pageSlug = 'news/' . $n['slug'];
|
||||
$hatBilder = array_filter(
|
||||
$n['artikel'],
|
||||
static fn (array $b): bool => ($b['typ'] ?? '') === 'bild'
|
||||
) !== [];
|
||||
|
||||
$meta = [
|
||||
'title' => $n['title'],
|
||||
'description' => (string) ($n['text'] ?? $n['title']),
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
['name' => 'News', 'slug' => 'news'],
|
||||
['name' => $n['title'], 'slug' => $pageSlug],
|
||||
]),
|
||||
// Mit $pageSlug: derselbe Article-Knoten (@id) wie auf /news.
|
||||
array_values(array_filter([news_article_schema($n, $pageSlug)]))
|
||||
),
|
||||
];
|
||||
|
||||
// Die Lightbox kommt nur dazu, wenn es auch etwas zu vergrößern gibt.
|
||||
if ($hatBilder) {
|
||||
$meta['scripts'] = ['lightbox.js'];
|
||||
}
|
||||
|
||||
if (!empty($n['image']['base']) && !empty($n['image']['widths'])) {
|
||||
$meta['og_image'] = $n['image']['base'] . '-' . max($n['image']['widths']) . '.jpg';
|
||||
}
|
||||
|
||||
if ($isDraft) {
|
||||
$meta['noindex'] = true;
|
||||
}
|
||||
?>
|
||||
<?php component('detail-head', [
|
||||
'back' => ['href' => url('news'), 'label' => 'Alle News'],
|
||||
'title' => $n['title'],
|
||||
'badge' => $isDraft ? 'Entwurf' : '',
|
||||
'date_iso' => (string) ($n['date'] ?? ''),
|
||||
'date_text' => !empty($n['date']) ? german_date((string) $n['date']) : '',
|
||||
]); ?>
|
||||
<?php component('news-artikel', [
|
||||
'image' => $n['image'] ?? null,
|
||||
'blocks' => $n['artikel'],
|
||||
]); ?>
|
||||
56
app/pages/news.php
Normal file
56
app/pages/news.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Zentrale News-Seite: mischt redaktionelle Meldungen (data/news.json,
|
||||
* chat-gepflegt), Instagram-Posts (data/instagram.json, automatisch gecacht)
|
||||
* und zuletzt gespielte Ergebnisse (data/matchcenter.json, teamübergreifend)
|
||||
* zu einem gemeinsamen, absteigend sortierten Feed. Merge-Logik in
|
||||
* news_build_feed() (app/helpers.php) — dieselbe Grundlage nutzt die
|
||||
* Stadionzeitungs-Vorlage (Phase 3), damit beide nie auseinanderlaufen.
|
||||
*
|
||||
* Meldungen mit Artikeltext (`artikel` in data/news.json) verlinken auf ihre
|
||||
* Detailseite /news/<slug>. Sie stehen immer alle im Feed: die Kappung aus
|
||||
* config('news.max_items') trifft nur Instagram und Ergebnisse, sonst wäre ein
|
||||
* älterer Artikel irgendwann von der Website aus nicht mehr verlinkt.
|
||||
*/
|
||||
$feed = news_build_feed();
|
||||
|
||||
$meta = [
|
||||
'title' => 'News',
|
||||
'description' => 'Alle Neuigkeiten des TSV 08 Kulmbach: aktuelle Meldungen, Fußball-Ergebnisse und Instagram-Beiträge aus dem Vereinsleben in Kulmbach an einem Ort.',
|
||||
'scripts' => ['news-filter.js'],
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
['name' => 'News', 'slug' => 'news'],
|
||||
]),
|
||||
// Meldungen mit Detailseite bekommen ihren Slug mit — dann trägt der
|
||||
// Article-Knoten hier dieselbe @id wie auf /news/<slug>, beide Seiten
|
||||
// beschreiben also nachweislich denselben Artikel.
|
||||
array_values(array_filter(array_map(
|
||||
static fn (array $n): ?array => news_article_schema($n, news_has_article($n) ? 'news/' . $n['slug'] : null),
|
||||
$feed['news']
|
||||
)))
|
||||
),
|
||||
];
|
||||
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'center',
|
||||
'title' => 'News',
|
||||
'text' => 'Unsere Meldungen, Ergebnisse und Instagram-Beiträge an einem Ort.',
|
||||
'image' => [
|
||||
'base' => 'img/pages/fussball-hero',
|
||||
'widths' => [640, 1000, 1600],
|
||||
'alt' => '',
|
||||
],
|
||||
]]);
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<?php component('news-feed', ['items' => $feed['items'], 'featured' => true, 'filterable' => true, 'empty_text' => 'Aktuell gibt es noch keine News.']); ?>
|
||||
</div>
|
||||
</section>
|
||||
@@ -11,7 +11,7 @@ $page = json_load('partner-werden');
|
||||
|
||||
$meta = [
|
||||
'title' => 'Werbepartner werden',
|
||||
'description' => 'Werden Sie Werbepartner des TSV 08 Kulmbach – sichtbar vor Ort, in Print und digital. Einstieg ab 199 € pro Saison. Jetzt unverbindlich anfragen.',
|
||||
'description' => 'Werden Sie Werbepartner des TSV 08 Kulmbach: sichtbar vor Ort, in Print und digital. Einstieg ab 199 € pro Saison. Jetzt unverbindlich anfragen!',
|
||||
'og_image' => 'img/pages/fussball-hero-1600.jpg',
|
||||
'schema' => page_schema(
|
||||
[
|
||||
|
||||
@@ -11,7 +11,7 @@ $club = json_load('club');
|
||||
|
||||
$meta = [
|
||||
'title' => 'Sportheim mieten in Kulmbach',
|
||||
'description' => 'Das Sportheim auf der Hans-Rausch-Sportanlage in Kulmbach für Feiern und Veranstaltungen mieten – Küche, Außenbereich, Beamer optional. Jetzt anfragen.',
|
||||
'description' => 'Sportheim in Kulmbach mieten: auf der Hans-Rausch-Sportanlage, mit Küche, Außenbereich und Beamer auf Wunsch. Jetzt unverbindlich anfragen!',
|
||||
'og_image' => 'img/pages/sportheim-2.jpg',
|
||||
'schema' => page_schema(
|
||||
[
|
||||
|
||||
140
app/pages/stadionzeitung-ausgabe.php
Normal file
140
app/pages/stadionzeitung-ausgabe.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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]); ?>
|
||||
41
app/pages/stadionzeitung.php
Normal file
41
app/pages/stadionzeitung.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Stadionzeitung-Archiv: Vorschau-Karten aller Ausgaben, verlinkt auf den
|
||||
* Swipe-Viewer je Ausgabe (stadionzeitung/<slug>, dynamische Route in
|
||||
* public/index.php). Daten: data/stadionzeitung.json (maschinenverwaltet von
|
||||
* bin/stadionzeitung-add.php — nie von Hand editieren).
|
||||
*/
|
||||
// Entwürfe bleiben unsichtbar (kein Archiv-Eintrag, siehe status-Feld) —
|
||||
// erreichbar sind sie nur über ihre direkte URL, zum Gegenlesen.
|
||||
$issues = array_values(array_filter(
|
||||
json_load('stadionzeitung')['issues'] ?? [],
|
||||
'stadionzeitung_is_published'
|
||||
));
|
||||
|
||||
$meta = [
|
||||
'title' => 'Stadionzeitung',
|
||||
'description' => 'Die Stadionzeitung zu jedem Heimspiel des TSV 08 Kulmbach digital zum Durchblättern – alle Ausgaben im Archiv.',
|
||||
'og_image' => 'img/pages/fussball-hero-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Fußball', 'slug' => 'fussball'],
|
||||
['name' => 'Stadionzeitung', 'slug' => 'stadionzeitung'],
|
||||
]),
|
||||
];
|
||||
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'center',
|
||||
'title' => 'Stadionzeitung',
|
||||
'text' => 'Die Stadionzeitung zu jedem Heimspiel – digital zum Durchblättern, auch unterwegs am Handy.',
|
||||
'image' => [
|
||||
'base' => 'img/pages/fussball-hero',
|
||||
'widths' => [640, 1000, 1600],
|
||||
'alt' => '',
|
||||
],
|
||||
]]);
|
||||
|
||||
component('stadionzeitung-cards', ['issues' => $issues]);
|
||||
@@ -10,8 +10,8 @@ $slug = 'fussball/damen';
|
||||
$team = json_load('teams')['damen'];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Frauenmannschaft – Frauenfußball Kulmbach',
|
||||
'description' => 'Frauen- und Mädchenfußball beim TSV 08 Kulmbach: Ob erfahren oder Wiedereinsteigerin – im Katzbachtal findest du dein Team. Komm zum Probetraining.',
|
||||
'title' => 'Frauenmannschaft · Frauenfußball Kulmbach',
|
||||
'description' => 'Frauenfußball in Kulmbach beim TSV 08: Ob erfahren oder Wiedereinsteigerin, im Katzbachtal findest du dein Team. Komm zum Probetraining!',
|
||||
'og_image' => $team['hero']['image']['base'] . '-1600.jpg',
|
||||
'schema' => team_schema($team, $slug),
|
||||
];
|
||||
|
||||
@@ -9,8 +9,8 @@ $slug = 'fussball/erste-mannschaft';
|
||||
$team = json_load('teams')['erste-mannschaft'];
|
||||
|
||||
$meta = [
|
||||
'title' => '1. Mannschaft – Kreisklasse Kulmbach',
|
||||
'description' => 'Die erste Herrenmannschaft des TSV 08 Kulmbach spielt auf Kreisklasse-Niveau – ambitioniert und mit echtem Vereinsgeist. Komm vorbei und spiel mit.',
|
||||
'title' => '1. Mannschaft · Kreisklasse Kulmbach',
|
||||
'description' => 'Die 1. Mannschaft des TSV 08 Kulmbach spielt Fußball in der Kreisklasse im Katzbachtal. Ehrgeizig, aber familiär: Komm zum Probetraining!',
|
||||
'og_image' => $team['hero']['image']['base'] . '-1600.jpg',
|
||||
'schema' => team_schema($team, $slug),
|
||||
];
|
||||
|
||||
@@ -9,8 +9,8 @@ $slug = 'fussball/zweite-mannschaft';
|
||||
$team = json_load('teams')['zweite-mannschaft'];
|
||||
|
||||
$meta = [
|
||||
'title' => '2. Mannschaft – Herrenfußball Kulmbach',
|
||||
'description' => 'Die zweite Herrenmannschaft des TSV 08 Kulmbach spielt in Spielgemeinschaft mit dem TSV Melkendorf – Fußball mit Spaß für alle, die gern kicken.',
|
||||
'title' => '2. Mannschaft · Herrenfußball Kulmbach',
|
||||
'description' => 'Die zweite Herrenmannschaft des TSV 08 Kulmbach: Spielgemeinschaft mit dem TSV Melkendorf. Fußball mit Spaß für alle, die gern kicken. Komm vorbei!',
|
||||
'og_image' => $team['hero']['image']['base'] . '-1600.jpg',
|
||||
'scripts' => (count($team['contact_names'] ?? []) > 1) ? ['contact-slider.js'] : [],
|
||||
'schema' => team_schema($team, $slug),
|
||||
|
||||
62
app/pages/termine.php
Normal file
62
app/pages/termine.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Zentrale Termine/Events-Seite: kombiniert Fußball-Spieltermine (aus
|
||||
* data/matchcenter.json — next_matches/last_results, NICHT dupliziert) mit
|
||||
* manuell gepflegten Vereins-Events (data/events.json, chat-gepflegt wie
|
||||
* teams.json/partners.json — keine Automatisierung, kein Bin-Skript) und den
|
||||
* Veranstaltungen mit eigener Detailseite (data/veranstaltungen.json, dorthin
|
||||
* verlinkt). Getrennt
|
||||
* in kommend/vergangen, je eine gemeinsame chronologische Liste — kein Filter
|
||||
* nach Kategorie (v1, siehe Interview). Merge-Logik in termine_build_lists()
|
||||
* (app/helpers.php) — dieselbe Grundlage nutzt die Stadionzeitungs-Vorlage
|
||||
* (Phase 3), damit beide nie auseinanderlaufen.
|
||||
*/
|
||||
$termine = termine_build_lists();
|
||||
|
||||
$meta = [
|
||||
'title' => 'Termine',
|
||||
'description' => 'Alle kommenden Termine des TSV 08 Kulmbach: Fußball-Spiele unserer Mannschaften und Vereins-Events in Kulmbach auf einen Blick. Schau vorbei im Katzbachtal!',
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
['name' => 'Termine', 'slug' => 'termine'],
|
||||
]),
|
||||
sportsevent_nodes($termine['upcoming_matches']),
|
||||
array_values(array_filter(array_map('event_schema', $termine['events']))),
|
||||
// Veranstaltungen tragen zusätzlich ihre eigene URL, damit dieser Knoten
|
||||
// und der auf der Detailseite dasselbe Event beschreiben.
|
||||
array_values(array_filter(array_map(
|
||||
static fn (array $v): ?array => event_schema($v, 'veranstaltungen/' . (string) ($v['slug'] ?? '')),
|
||||
$termine['veranstaltungen']
|
||||
)))
|
||||
),
|
||||
];
|
||||
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'center',
|
||||
'title' => 'Termine',
|
||||
'text' => 'Alle kommenden Spiele und Vereins-Events auf einen Blick.',
|
||||
'image' => [
|
||||
'base' => 'img/pages/fussball-hero',
|
||||
'widths' => [640, 1000, 1600],
|
||||
'alt' => '',
|
||||
],
|
||||
]]);
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2>Kommende Termine</h2>
|
||||
<?php component('termine-list', ['items' => $termine['upcoming'], 'empty_text' => 'Aktuell sind keine Termine geplant.']); ?>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section--sunken">
|
||||
<div class="container">
|
||||
<h2>Vergangene Termine</h2>
|
||||
<?php component('termine-list', ['items' => $termine['past'], 'empty_text' => 'Noch keine vergangenen Termine erfasst.']); ?>
|
||||
</div>
|
||||
</section>
|
||||
@@ -14,7 +14,7 @@ $d = $data['disciplines']['eltern-kind-turnen'];
|
||||
$meta = [
|
||||
'title' => 'Eltern-Kind- & Kinderturnen in Kulmbach',
|
||||
'description' => $d['hero']['text'],
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'scripts' => ['group-tabs.js'],
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
|
||||
@@ -11,8 +11,8 @@ $d = json_load('turnen')['disciplines']['erwachsenenturnen'];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Erwachsenenturnen in Kulmbach',
|
||||
'description' => 'Beweglich bleiben, Kraft und Koordination trainieren – Erwachsenenturnen beim TSV 08 Kulmbach im Mäximum Gym. Trainingszeiten und Kontakt auf einen Blick.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'description' => 'Beweglich bleiben, Kraft und Koordination trainieren: Erwachsenenturnen beim TSV 08 Kulmbach im Mäximum Gym. Trainingszeiten und Kontakt auf einen Blick.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Turnen', 'slug' => 'turnen'],
|
||||
|
||||
@@ -11,8 +11,8 @@ $d = json_load('turnen')['disciplines']['step-aerobic-fitness-pilates'];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Step Aerobic, Fitness & Pilates in Kulmbach',
|
||||
'description' => 'Ausdauer, Kräftigung und Beweglichkeit für Erwachsene – Step Aerobic, Fitness & Pilates beim TSV 08 Kulmbach in der Max-Hundt-Schule. Zeiten und Kontakt.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'description' => 'Ausdauer, Kräftigung und Beweglichkeit für Erwachsene: Step Aerobic, Fitness & Pilates beim TSV 08 Kulmbach in der Max-Hundt-Schule. Zeiten und Kontakt.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Turnen', 'slug' => 'turnen'],
|
||||
|
||||
@@ -11,8 +11,8 @@ $d = json_load('turnen')['disciplines']['trampolin'];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Trampolinturnen in Kulmbach',
|
||||
'description' => 'Sprünge, Schrauben und Salti auf dem Großtrampolin – Trampolinturnen beim TSV 08 Kulmbach in der Carl-von-Linde-Realschule. Zeiten und Kontakt im Überblick.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'description' => 'Sprünge, Schrauben und Salti auf dem Großtrampolin: Trampolinturnen beim TSV 08 Kulmbach in der Carl-von-Linde-Realschule. Zeiten und Kontakt im Überblick.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Turnen', 'slug' => 'turnen'],
|
||||
|
||||
@@ -11,8 +11,8 @@ $d = json_load('turnen')['disciplines']['wettkampfturnen'];
|
||||
|
||||
$meta = [
|
||||
'title' => 'Wettkampfturnen in Kulmbach',
|
||||
'description' => 'Leistungsorientiertes Gerätturnen mit Training und Wettkämpfen – Wettkampfturnen beim TSV 08 Kulmbach im Mäximum Gym. Trainingszeiten und Kontakt im Überblick.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'description' => 'Leistungsorientiertes Gerätturnen mit Training und Wettkämpfen: Wettkampfturnen beim TSV 08 Kulmbach im Mäximum Gym. Trainingszeiten und Kontakt im Überblick.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Turnen', 'slug' => 'turnen'],
|
||||
|
||||
@@ -3,14 +3,24 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Turn-Übersichtsseite (reiner Hub). Inhalt: data/turnen.json.
|
||||
* Struktur: Hero → Disziplin-Karten. Keine weiteren Sektionen — die Detailinhalte
|
||||
* (Beiträge, FAQ, Trainingszeiten) leben auf den Disziplin-Unterseiten.
|
||||
* Turn-Übersichtsseite (Hub + Abteilungskontakt). Inhalt: data/turnen.json.
|
||||
* Struktur: Hero → Disziplin-Karten → Anfrageformular. Die Detailinhalte
|
||||
* (Beiträge, Trainingszeiten) leben auf den Disziplin-Unterseiten.
|
||||
*
|
||||
* Das Formular ist die allgemeine contact-form-Komponente mit
|
||||
* department => 'turnen': Auswahlliste sind die Disziplinen, Empfänger ist die
|
||||
* Turnabteilung (club.email_turnen) statt info@. Die alte Seite hatte dafür ein
|
||||
* eigenes, zweites Formular — hier ist es dieselbe Komponente und dieselbe
|
||||
* Action, damit der Spam-Schutz nicht doppelt gepflegt werden muss.
|
||||
*/
|
||||
$page = json_load('turnen')['overview'];
|
||||
|
||||
// Heros nutzen das Vereins-Default-Bild; nur die Übersichts-Kacheln zeigen die
|
||||
// (vorläufigen) themenbezogenen Unsplash-Platzhalter aus public/assets/img/pages/.
|
||||
// Heros aller Turn-Seiten zeigen VORÜBERGEHEND das Drohnen-Luftbild des
|
||||
// Vereinsgeländes (img/pages/vereinsgelaende, Quelle: data/turnen.json) statt des
|
||||
// Fußballplatz-Bildes. Ehrlicher wäre ein Bild aus der Turnhalle — liegt eines
|
||||
// vor, tauschen die sechs Hero-Blöcke in data/turnen.json und das og_image der
|
||||
// sechs Seiten-Dateien. Die Übersichts-Kacheln zeigen weiter die (vorläufigen)
|
||||
// themenbezogenen Platzhalter aus public/assets/img/pages/.
|
||||
$cards = [
|
||||
['name' => 'Erwachsenenturnen', 'slug' => 'turnen/erwachsenenturnen', 'category' => 'Mäximum Gym', 'image' => ['base' => 'img/pages/turnen-erwachsenenturnen', 'widths' => [640, 1000, 1600], 'alt' => 'Erwachsenenturnen beim TSV 08 Kulmbach']],
|
||||
['name' => 'Eltern-Kind- & Kinderturnen', 'slug' => 'turnen/eltern-kind-turnen', 'category' => 'ab ca. 2 Jahren', 'image' => ['base' => 'img/pages/turnen-eltern-kind', 'widths' => [640, 1000, 1600], 'alt' => 'Eltern-Kind- und Kinderturnen beim TSV 08 Kulmbach']],
|
||||
@@ -22,7 +32,7 @@ $cards = [
|
||||
$meta = [
|
||||
'title' => 'Turnen in Kulmbach',
|
||||
'description' => 'Die Turnabteilung des TSV 08 Kulmbach: Eltern-Kind- und Kinderturnen, Erwachsenenturnen, Step Aerobic, Fitness und Pilates, Trampolin- und Wettkampfturnen.',
|
||||
'og_image' => 'img/hero/hero-1600.jpg',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Turnen', 'slug' => 'turnen'],
|
||||
@@ -36,3 +46,8 @@ component('team-cards', [
|
||||
'title_hidden' => true,
|
||||
'anchor' => 'gruppen',
|
||||
]);
|
||||
component('contact-form', [
|
||||
'intro' => $page['contact'],
|
||||
'return' => 'turnen',
|
||||
'department' => 'turnen',
|
||||
]);
|
||||
|
||||
157
app/pages/veranstaltung.php
Normal file
157
app/pages/veranstaltung.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Einzelne Veranstaltung: dynamische Route veranstaltungen/<slug>, siehe
|
||||
* public/index.php (Prefix-Match, da app/routes.php nur exakte Slugs kennt).
|
||||
* Ermittelt den Slug selbst wieder aus der URL, Daten aus
|
||||
* data/veranstaltungen.json — Texte chat-gepflegt, `plakat` und `galerie`
|
||||
* schreibt bin/veranstaltung-bilder.php.
|
||||
*
|
||||
* Geladen wird hier bewusst UNGEFILTERT (nicht über veranstaltungen_load()):
|
||||
* ein Entwurf muss unter seiner direkten URL lesbar bleiben, damit er
|
||||
* gegengelesen werden kann. Sichtbar wird er dadurch nirgends sonst, und er
|
||||
* trägt noindex.
|
||||
*
|
||||
* Bewusst OHNE das übliche hero-Component (wie die Stadionzeitungs-Ausgabe):
|
||||
* ein Archiv-Eintrag hat kein eigenes Titelbild, und das einzige vorhandene
|
||||
* Bild ist ein Plakat im Hochformat, das als cover-beschnittener
|
||||
* Hero-Hintergrund zerstört würde. Stattdessen ein schmaler Kopf mit Rückweg.
|
||||
* Die Übersichtsseite /veranstaltungen behält den regulären Unterseiten-Hero.
|
||||
*/
|
||||
$urlSlug = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
|
||||
$veranstaltungSlug = substr($urlSlug, strlen('veranstaltungen/'));
|
||||
|
||||
$v = null;
|
||||
foreach (json_load('veranstaltungen')['veranstaltungen'] ?? [] as $entry) {
|
||||
if (($entry['slug'] ?? '') === $veranstaltungSlug) {
|
||||
$v = $entry;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($v === 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;
|
||||
}
|
||||
|
||||
$isDraft = !veranstaltung_is_published($v);
|
||||
// Der Lebenszyklus läuft über das Datum, nicht über ein Feld von Hand:
|
||||
// vor dem Fest wirbt die Seite, danach erzählt sie. Maßgeblich ist der LETZTE
|
||||
// Tag (termin_is_upcoming()), damit ein dreitägiges Fest am Samstag nicht schon
|
||||
// zum Rückblick wird.
|
||||
$isVorbei = !termin_is_upcoming($v, date('Y-m-d'));
|
||||
|
||||
$plakate = $v['plakate'] ?? [];
|
||||
$galerie = $v['galerie'] ?? [];
|
||||
$dateText = german_date_range((string) $v['date_start'], $v['date_end'] ?? null);
|
||||
|
||||
$meta = [
|
||||
'title' => $v['title'],
|
||||
'description' => (string) ($v['text'] ?? $v['title']),
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
['name' => 'Veranstaltungen', 'slug' => 'veranstaltungen'],
|
||||
['name' => $v['title'], 'slug' => 'veranstaltungen/' . $v['slug']],
|
||||
]),
|
||||
array_values(array_filter([event_schema($v, 'veranstaltungen/' . $v['slug'])]))
|
||||
),
|
||||
];
|
||||
|
||||
// Die Lightbox kommt nur dazu, wenn es auch etwas zu vergrößern gibt.
|
||||
if ($plakate !== [] || $galerie !== []) {
|
||||
$meta['scripts'] = ['lightbox.js'];
|
||||
}
|
||||
|
||||
$ogBild = $plakate[0] ?? $galerie[0] ?? null;
|
||||
if ($ogBild !== null && !empty($ogBild['widths'])) {
|
||||
$meta['og_image'] = $ogBild['base'] . '-' . max($ogBild['widths']) . '.jpg';
|
||||
}
|
||||
|
||||
if ($isDraft) {
|
||||
$meta['noindex'] = true;
|
||||
}
|
||||
|
||||
// Einleitung: vor dem Fest die Ankündigung, danach der Rückblick. Fehlt der
|
||||
// jeweilige Text noch, trägt der Teaser aus `text` die Sektion — die Seite darf
|
||||
// nie leer sein, sie startet ja mit nichts als dem Plakat.
|
||||
// Fällt der jeweilige Text noch, trägt der Teaser aus `text` die Sektion — die
|
||||
// Seite darf nie leer sein, sie startet ja mit nichts als dem Plakat.
|
||||
$introText = $isVorbei ? ($v['rueckblick'] ?? null) : ($v['ankuendigung'] ?? null);
|
||||
$introSection = !empty($introText) ? [
|
||||
'id' => 'ueberblick',
|
||||
'title' => $isVorbei ? 'Unser Rückblick' : 'Das erwartet dich',
|
||||
// Ohne Bild läuft die Sektion einspaltig auf Lesebreite (split--plain).
|
||||
'text' => $introText,
|
||||
] : null;
|
||||
?>
|
||||
<?php component('detail-head', [
|
||||
'back' => ['href' => url('veranstaltungen'), 'label' => 'Alle Veranstaltungen'],
|
||||
'title' => $v['title'],
|
||||
'badge' => $isDraft ? 'Entwurf' : '',
|
||||
'date_iso' => (string) $v['date_start'],
|
||||
'date_text' => $dateText,
|
||||
'location' => (string) ($v['location'] ?? ''),
|
||||
]); ?>
|
||||
<?php /* Einleitungstext nur, wenn wirklich einer gepflegt ist — die Regel für die
|
||||
ganze Seite: gezeigt wird, was in der JSON steht, und sonst nichts. */ ?>
|
||||
<?php if ($introSection !== null): ?>
|
||||
<?php component('section', ['section' => $introSection]); ?>
|
||||
<?php endif; ?>
|
||||
<?php /* Plakate und Galerie tauschen den Platz: vor dem Fest wirbt das Plakat,
|
||||
danach tragen die Fotos die Seite und das Plakat bleibt als Andenken. */ ?>
|
||||
<?php if ($isVorbei): ?>
|
||||
<?php component('veranstaltung-galerie', ['bilder' => $galerie, 'title' => 'Bilder']); ?>
|
||||
<?php component('veranstaltung-plakate', ['plakate' => $plakate, 'title' => 'Plakate von damals', 'compact' => true]); ?>
|
||||
<?php else: ?>
|
||||
<?php component('veranstaltung-plakate', ['plakate' => $plakate, 'title' => count($plakate) > 1 ? 'Plakat und Programm' : 'Plakat']); ?>
|
||||
<?php component('veranstaltung-galerie', ['bilder' => $galerie, 'title' => 'Bilder']); ?>
|
||||
<?php endif; ?>
|
||||
<?php /* Zeitraum, Beginn, Adresse und Karte in einem Block — aber nur, solange das
|
||||
Fest noch aussteht. Hinterher wäre es reine Doppelung: Datum und Ort
|
||||
stehen im Kopf, und eine Anfahrtskarte zu einem Fest von letztem Jahr
|
||||
hilft niemandem mehr. Alles Weitere steht auf den Plakaten. */ ?>
|
||||
<?php if (!$isVorbei): ?>
|
||||
<?php component('veranstaltung-info', [
|
||||
'date_text' => $dateText,
|
||||
'date_iso' => (string) $v['date_start'],
|
||||
'time' => (string) ($v['time'] ?? ''),
|
||||
'location' => (string) ($v['location'] ?? ''),
|
||||
'karte' => (string) ($v['karte'] ?? ''),
|
||||
'anfahrt' => $v['anfahrt'] ?? [],
|
||||
]); ?>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
// phase steuert, welche Abschnitte wann stehen: „Anfahrt und Parken" hilft nur
|
||||
// vor dem Fest. Ohne Angabe steht ein Abschnitt immer — der harmlosere Default:
|
||||
// geschriebener Text verschwindet nie überraschend.
|
||||
foreach ($v['abschnitte'] ?? [] as $abschnitt):
|
||||
$phase = $abschnitt['phase'] ?? 'immer';
|
||||
if ($phase !== 'immer' && $phase !== ($isVorbei ? 'rueckblick' : 'ankuendigung')) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<?php component('section', ['section' => $abschnitt]); ?>
|
||||
<?php endforeach; ?>
|
||||
<?php /* Das Programm als Liste ist die Ausnahme, nicht die Regel: normalerweise
|
||||
trägt es das Programmplakat. Nur wenn `programm` gepflegt ist, kommt es
|
||||
zusätzlich als lesbare Liste dazu (Screenreader, Suchmaschinen). */ ?>
|
||||
<?php component('veranstaltung-programm', [
|
||||
'programm' => $v['programm'] ?? [],
|
||||
'title' => $isVorbei ? 'So lief unser Programm' : 'Programm',
|
||||
'compact' => $isVorbei,
|
||||
]); ?>
|
||||
<?php /* Helfer-Aufruf nur vor dem Fest: hinterher sind die Schichten gelaufen. */ ?>
|
||||
<?php if (!$isVorbei && !empty($v['helfer'])): ?>
|
||||
<?php component('cta-band', ['band' => $v['helfer'], 'anchor' => 'helfen']); ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($v['partner'])): ?>
|
||||
<?php component('partner-grid', ['intro' => $v['partner'], 'anchor' => 'fest-partner']); ?>
|
||||
<?php endif; ?>
|
||||
77
app/pages/veranstaltungen.php
Normal file
77
app/pages/veranstaltungen.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Übersicht aller Veranstaltungen mit eigener Detailseite (Sportfest,
|
||||
* Jubiläum, …) — Quelle data/veranstaltungen.json, geladen über
|
||||
* veranstaltungen_load() (filtert Entwürfe, sortiert neueste zuerst).
|
||||
*
|
||||
* Zwei Abschnitte statt einer gemischten Liste: oben was ansteht (der nächste
|
||||
* Termin als große Karte mit Plakat), unten das Archiv. Auch in der Sommerpause
|
||||
* ohne anstehendes Fest ist die Seite damit gefüllt.
|
||||
*
|
||||
* Abgrenzung zu /termine: dort steht der vollständige Kalender (Spiele +
|
||||
* Vereins-Termine + Veranstaltungen), hier nur das, was eine eigene Seite hat.
|
||||
*/
|
||||
$today = date('Y-m-d');
|
||||
$alle = veranstaltungen_load();
|
||||
|
||||
$kommend = array_values(array_filter($alle, static fn (array $v): bool => termin_is_upcoming($v, $today)));
|
||||
// veranstaltungen_load() sortiert absteigend (fürs Archiv richtig) — was
|
||||
// ansteht, gehört aufsteigend sortiert: das Nächste zuerst.
|
||||
usort($kommend, static fn (array $a, array $b): int => (string) $a['date_start'] <=> (string) $b['date_start']);
|
||||
|
||||
$vergangen = array_values(array_filter($alle, static fn (array $v): bool => !termin_is_upcoming($v, $today)));
|
||||
|
||||
$meta = [
|
||||
'title' => 'Veranstaltungen',
|
||||
'description' => 'Unsere Feste und Veranstaltungen: Sportfest, Frühschoppen und alles, was wir im Katzbachtal auf die Beine stellen.',
|
||||
'schema' => array_merge(
|
||||
page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Der Verein', 'slug' => 'verein'],
|
||||
['name' => 'Veranstaltungen', 'slug' => 'veranstaltungen'],
|
||||
]),
|
||||
// Nur die anstehenden als Event-Knoten: ein vergangenes Fest ist für
|
||||
// Suchmaschinen kein Termin mehr, den man sich vormerken kann. Mit Slug,
|
||||
// damit es derselbe Knoten ist wie auf /termine und der Detailseite.
|
||||
array_values(array_filter(array_map(
|
||||
static fn (array $v): ?array => event_schema($v, 'veranstaltungen/' . (string) ($v['slug'] ?? '')),
|
||||
$kommend
|
||||
)))
|
||||
),
|
||||
];
|
||||
|
||||
component('hero', ['hero' => [
|
||||
'compact' => true,
|
||||
'align' => 'center',
|
||||
'title' => 'Veranstaltungen',
|
||||
'text' => 'Sportfest, Frühschoppen, Feiern: hier findest du alles zu unseren Festen, vorher das Programm und hinterher die Bilder.',
|
||||
'image' => [
|
||||
'base' => 'img/pages/vereinsgelaende',
|
||||
'widths' => [640, 1000, 1600],
|
||||
'alt' => '',
|
||||
],
|
||||
]]);
|
||||
?>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2>Was ansteht</h2>
|
||||
<?php component('veranstaltungen-cards', [
|
||||
'items' => $kommend,
|
||||
'cta' => 'Zum Programm',
|
||||
'empty_text' => 'Gerade ist kein Fest geplant. Sobald der nächste Termin steht, findest du ihn hier.',
|
||||
]); ?>
|
||||
</div>
|
||||
</section>
|
||||
<section class="section section--sunken">
|
||||
<div class="container">
|
||||
<h2>Rückblick</h2>
|
||||
<?php component('veranstaltungen-cards', [
|
||||
'items' => $vergangen,
|
||||
'cta' => 'Bilder ansehen',
|
||||
'empty_text' => 'Noch kein Fest im Rückblick. Nach unserem nächsten Sportfest findest du hier die Bilder.',
|
||||
]); ?>
|
||||
</div>
|
||||
</section>
|
||||
@@ -26,7 +26,7 @@ $cards = [
|
||||
|
||||
$meta = [
|
||||
'title' => 'Der Verein',
|
||||
'description' => 'Der TSV 1908 Kulmbach e. V. – seit 1908 dein Sportverein im Katzbachtal: über 650 Mitglieder, Fußball und Turnen, zuhause auf der Hans-Rausch-Sportanlage.',
|
||||
'description' => 'Der TSV 1908 Kulmbach e. V.: seit 1908 dein Sportverein in Kulmbach mit über 650 Mitgliedern. Fußball und Turnen auf der Hans-Rausch-Sportanlage im Katzbachtal.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'schema' => page_schema(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user