Kontaktseite: eigene /kontakt mit Hero + Formular
Neue /kontakt-Seite (Hero + wiederverwendete contact-form-Komponente, ohne Intro-Block). Behebt die bisher ins 404 laufenden "Kontakt aufnehmen"-CTAs. - contact-form: optionales $intro + Rücksprung-Slug $return (Hidden-Feld) - contact-submit: PRG-Redirect zur absendenden Seite, whitelist-validiert gegen routes.php (kein Open-Redirect) - Verlinkt im Footer-Rechtsbereich und im Menü "Der Verein" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGzc6GhWhmLJt1jC2q1SRZ
This commit is contained in:
@@ -4,25 +4,33 @@ declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* POST /kontakt-senden — Kontaktformular validieren und via Brevo SMTP versenden.
|
||||
* Antwort: PRG-Redirect zu /#kontakt (?sent=1 | ?error=…) oder JSON bei fetch (form.js).
|
||||
* Antwort: PRG-Redirect zurück zur absendenden Seite (#kontakt, ?sent=1 | ?error=…)
|
||||
* oder JSON bei fetch (form.js). Das Formular kommt von / wie auch von /kontakt.
|
||||
*/
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
|
||||
|
||||
// Rücksprungziel gegen die Slug-Whitelist (Single Source of Truth) prüfen — kein Open-Redirect.
|
||||
$return = $field('return');
|
||||
$routes = require APP_PATH . '/routes.php';
|
||||
if (!array_key_exists($return, $routes)) {
|
||||
$return = '';
|
||||
}
|
||||
|
||||
$wantsJson = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json');
|
||||
|
||||
$respond = static function (bool $ok, string $error = '') use ($wantsJson): never {
|
||||
$respond = static function (bool $ok, string $error = '') use ($wantsJson, $return): never {
|
||||
if ($wantsJson) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(['ok' => $ok, 'error' => $error ?: null]);
|
||||
} else {
|
||||
header('Location: ' . url('') . ($ok ? '?sent=1' : '?error=' . $error) . '#kontakt', true, 303);
|
||||
header('Location: ' . url($return) . ($ok ? '?sent=1' : '?error=' . $error) . '#kontakt', true, 303);
|
||||
}
|
||||
exit;
|
||||
};
|
||||
|
||||
$field = static fn (string $key): string => trim((string) ($_POST[$key] ?? ''));
|
||||
|
||||
$route = 'kontakt';
|
||||
$token = $field('ft');
|
||||
|
||||
|
||||
@@ -4,12 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kontaktformular (Felder wie auf der alten Seite). Props:
|
||||
* $intro array{title, text}
|
||||
* $intro array{title, text}|null — optionaler Intro-Block (Überschrift, Teaser, Direkt-Mail).
|
||||
* Weglassen → nur das Formular (z. B. auf /kontakt, wo der Hero den Kontext liefert).
|
||||
* $return string optional — Slug der absendenden Seite (Default '' = Startseite). Die Action
|
||||
* leitet nach PRG hierher zurück (whitelist-validiert), damit /kontakt nicht auf die
|
||||
* Startseite springt.
|
||||
* Felder rendern ausschließlich über component('form-field') — siehe CLAUDE.md.
|
||||
* Liest Flash-Status aus Query-Params (?sent=1 / ?error=...) — PRG-Muster.
|
||||
* Spam-Schutz: Honeypot "company_url" + signierter Timestamp "ft" (form_token()).
|
||||
*/
|
||||
$club = json_load('club');
|
||||
$return = $return ?? '';
|
||||
$intro = $intro ?? null;
|
||||
|
||||
$interests = ['Herrenfußball', 'Damenfußball', 'Jugendfußball', 'Turnen', 'Sonstiges'];
|
||||
|
||||
@@ -37,11 +43,13 @@ $f = [
|
||||
?>
|
||||
<section class="section contact" id="kontakt">
|
||||
<div class="container contact__inner">
|
||||
<?php if (!empty($intro)): ?>
|
||||
<div class="contact__intro">
|
||||
<h2><?= e($intro['title']) ?></h2>
|
||||
<p><?= e($intro['text']) ?></p>
|
||||
<p class="text-muted">Oder direkt per Mail an <a href="mailto:<?= e($club['email']) ?>"><?= e($club['email']) ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<form class="form" method="post" action="<?= e(url('kontakt-senden')) ?>" novalidate>
|
||||
<div class="form__status" role="status" aria-live="polite" tabindex="-1" data-form-status>
|
||||
<?php if ($sent): ?>
|
||||
@@ -73,6 +81,7 @@ $f = [
|
||||
<input type="text" id="contact-company-url" name="company_url" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
<input type="hidden" name="ft" value="<?= e(form_token()) ?>">
|
||||
<input type="hidden" name="return" value="<?= e($return) ?>">
|
||||
|
||||
<p class="form__submit">
|
||||
<button class="btn" type="submit">Nachricht senden</button>
|
||||
|
||||
24
app/pages/kontakt.php
Normal file
24
app/pages/kontakt.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Kontaktseite — Hero + Kontaktformular (dieselbe Komponente wie auf der Startseite).
|
||||
* Inhalte: data/kontakt.json (Single Source of Truth).
|
||||
*/
|
||||
$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.',
|
||||
'og_image' => 'img/pages/vereinsgelaende-1600.jpg',
|
||||
'scripts' => ['form.js'],
|
||||
'schema' => page_schema([
|
||||
['name' => 'Startseite', 'slug' => ''],
|
||||
['name' => 'Kontakt', 'slug' => 'kontakt'],
|
||||
]),
|
||||
];
|
||||
|
||||
component('hero', ['hero' => $page['hero']]);
|
||||
// Kein Intro-Block: Der Hero liefert bereits Überschrift + Teaser, hier folgt nur das Formular.
|
||||
component('contact-form', ['return' => 'kontakt']);
|
||||
@@ -28,6 +28,7 @@ return [
|
||||
'historie' => ['file' => 'historie.php', 'sitemap' => true],
|
||||
'sportheimbuchung' => ['file' => 'sportheimbuchung.php', 'sitemap' => true],
|
||||
'ansprechpartner' => ['file' => 'ansprechpartner.php', 'sitemap' => true],
|
||||
'kontakt' => ['file' => 'kontakt.php', 'sitemap' => true],
|
||||
'partner-werden' => ['file' => 'partner-werden.php', 'sitemap' => true],
|
||||
'mitglied-werden' => ['file' => 'mitglied-werden.php', 'sitemap' => true],
|
||||
'impressum' => ['file' => 'impressum.php', 'sitemap' => true],
|
||||
|
||||
13
data/kontakt.json
Normal file
13
data/kontakt.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"hero": {
|
||||
"compact": true,
|
||||
"align": "bottom-left",
|
||||
"title": "Kontakt",
|
||||
"text": "Fragen zu unseren Abteilungen, Lust mitzumachen oder ein Anliegen an den Verein? Schreib uns – wir melden uns so schnell wie möglich bei dir.",
|
||||
"image": {
|
||||
"base": "img/pages/vereinsgelaende",
|
||||
"widths": [640, 1000, 1600],
|
||||
"alt": "Vereinsgelände des TSV 08 Kulmbach im Katzbachtal"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
"children": [
|
||||
{ "label": "Übersicht", "slug": "verein" },
|
||||
{ "label": "Ansprechpartner", "slug": "ansprechpartner" },
|
||||
{ "label": "Kontakt", "slug": "kontakt" },
|
||||
{ "label": "Historie", "slug": "historie" },
|
||||
{ "label": "Sportheimbuchung", "slug": "sportheimbuchung", "separator": true },
|
||||
{ "label": "Partner werden", "slug": "partner-werden" }
|
||||
@@ -42,6 +43,7 @@
|
||||
{ "label": "Mitglied werden", "slug": "mitglied-werden", "style": "primary" }
|
||||
],
|
||||
"legal": [
|
||||
{ "label": "Kontakt", "slug": "kontakt" },
|
||||
{ "label": "Impressum", "slug": "impressum" },
|
||||
{ "label": "Datenschutz", "slug": "datenschutz" }
|
||||
]
|
||||
|
||||
BIN
public/assets/img/pages/vereinsgelaende-640.jpg
Normal file
BIN
public/assets/img/pages/vereinsgelaende-640.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Reference in New Issue
Block a user