1 Commits

Author SHA1 Message Date
Claw AI
9e146ac1eb feat(a11y): ARIA labels, focus management, skip-nav, keyboard nav, contrast fix
Accessibility improvements per WCAG 2.1 AA:
- Skip-to-content link (TA-1)
- ARIA landmarks and roles for nav, main, sections, footer (TA-2)
- Accordion keyboard navigation + aria-expanded (TA-3)
- Lightbox focus trap + focus management + dialog role (TA-4)
- Gallery grid items keyboard accessible (TA-5)
- Improved alt texts for all images (TA-6)
- Focus-visible styles for all interactive elements (TA-7)
- Darker --stone color for WCAG AA contrast compliance (TA-8)

Fix #18
2026-05-13 23:13:47 +00:00
142 changed files with 412 additions and 4107 deletions

0
.continue/mcpServers/new-mcp-server.yaml Executable file → Normal file
View File

0
.dockerignore Executable file → Normal file
View File

View File

@@ -1,131 +0,0 @@
name: Deploy Feature Branch to Test
on:
push:
branches:
- "feature/**"
jobs:
lint-php:
name: PHP Syntax Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PHP
run: apt-get update -qq && apt-get install -y -qq php-cli > /dev/null 2>&1
- name: PHP Lint
run: |
errors=0
while IFS= read -r file; do
if ! php -l "$file" > /dev/null 2>&1; then
echo "❌ Syntax error in $file"
php -l "$file"
errors=1
fi
done < <(find . -name "*.php" -not -path "./vendor/*")
if [ "$errors" -eq 1 ]; then
echo "::error::PHP lint check failed"
exit 1
fi
echo "✅ All PHP files pass syntax check"
lint-css:
name: CSS Lint (stylelint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js & stylelint
run: |
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
npm install -g stylelint stylelint-config-standard stylelint-prettier > /dev/null 2>&1
- name: CSS Lint
run: |
npx stylelint "**/*.css" --config .stylelintrc.json --allow-empty-input
echo "✅ All CSS files pass lint"
lint-html:
name: HTML Lint (htmlhint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js & htmlhint
run: |
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
npm install -g htmlhint > /dev/null 2>&1
- name: HTML Lint
run: |
npx htmlhint "**/*.html" --config .htmlhintrc
echo "✅ All HTML files pass lint"
deploy:
name: Deploy to Test Environment
runs-on: ubuntu-latest
needs: [lint-php, lint-css, lint-html]
container:
volumes:
- /var/www/test/html:/deploy
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Show branch info
run: |
echo "=== Deploying branch: ${{ gitea.ref_name }} ==="
echo "=== Commit: ${{ gitea.sha }} ==="
echo "=== By: ${{ gitea.actor }} ==="
echo "=== All lint checks passed ✅ ==="
date
- name: Deploy to test environment
run: |
echo "Syncing files to test environment..."
apt-get update -qq && apt-get install -y -qq rsync > /dev/null 2>&1 || true
rsync -av --delete \
--exclude='.git' \
--exclude='.gitea' \
--exclude='.husky' \
--exclude='.prettierrc' \
--exclude='.prettierignore' \
--exclude='.stylelintrc.json' \
--exclude='.htmlhintrc' \
--exclude='.gitignore' \
--exclude='.dockerignore' \
--exclude='.continue' \
--exclude='Dockerfile' \
--exclude='nginx.conf' \
--exclude='eslint.config.js' \
--exclude='package.json' \
--exclude='package-lock.json' \
--exclude='docs/' \
--exclude='AGENTS.md' \
--exclude='README.md' \
--exclude='scripts/' \
./ /deploy/
echo "✅ Deployment complete!"
- name: Set permissions
run: |
chown -R 33:33 /deploy/ 2>/dev/null || true
chmod -R 755 /deploy/ 2>/dev/null || true
echo "✅ Permissions set"
- name: Deployment summary
run: |
echo "=========================================="
echo " 🚀 Deployment Summary"
echo "=========================================="
echo " Branch: ${{ gitea.ref_name }}"
echo " Commit: ${{ gitea.sha }}"
echo " Target: http://178.104.150.0:6427/"
echo " Lint: ✅ All checks passed"
echo " Time: $(date)"
echo "=========================================="

View File

@@ -1,64 +0,0 @@
name: Lint
on:
push:
pull_request:
jobs:
lint-php:
name: PHP Syntax Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install PHP
run: |
apt-get update -qq && apt-get install -y -qq php-cli > /dev/null 2>&1
- name: PHP Lint
run: |
errors=0
while IFS= read -r file; do
if ! php -l "$file" > /dev/null 2>&1; then
echo "❌ Syntax error in $file"
php -l "$file"
errors=1
fi
done < <(find . -name "*.php" -not -path "./vendor/*")
if [ "$errors" -eq 1 ]; then
echo "::error::PHP lint check failed"
exit 1
fi
echo "✅ All PHP files pass syntax check"
lint-css:
name: CSS Lint (stylelint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js & stylelint
run: |
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
npm install -g stylelint stylelint-config-standard stylelint-prettier > /dev/null 2>&1
- name: CSS Lint
run: |
npx stylelint "**/*.css" --config .stylelintrc.json --allow-empty-input
echo "✅ All CSS files pass lint"
lint-html:
name: HTML Lint (htmlhint)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Node.js & htmlhint
run: |
apt-get update -qq && apt-get install -y -qq npm nodejs > /dev/null 2>&1
npm install -g htmlhint > /dev/null 2>&1
- name: HTML Lint
run: |
npx htmlhint "**/*.html" --config .htmlhintrc
echo "✅ All HTML files pass lint"

2
.gitignore vendored Executable file → Normal file
View File

@@ -4,5 +4,3 @@
package-lock.json package-lock.json
.continue/ .continue/
.playwright-mcp/ .playwright-mcp/
vendor/
.phpunit.cache/

View File

@@ -1,8 +0,0 @@
# Legacy redirects for old URLs pointing to root
RewriteEngine On
RewriteRule ^impressum\.html$ /impressum [R=301,L]
RewriteRule ^datenschutz\.html$ /datenschutz [R=301,L]
RewriteRule ^haus-schleusingen\.html$ / [R=301,L]
# Everything else goes to public/
RewriteRule ^(.*)$ public/$1 [L]

0
.htmlhintrc Executable file → Normal file
View File

0
.husky/pre-commit Executable file → Normal file
View File

0
.prettierignore Executable file → Normal file
View File

0
.prettierrc Executable file → Normal file
View File

0
.stylelintrc.json Executable file → Normal file
View File

23
AGENTS.md Executable file → Normal file
View File

@@ -1,27 +1,8 @@
# Agent-Richtlinien für dieses Projekt # Agent-Richtlinien für dieses Projekt
## ⚠️ WICHTIG: Pre-Commit Hooks sind Pflicht
**Jeder Commit MUSS die Pre-Commit Lint-Checks durchlaufen. Niemals `--no-verify` verwenden!**
### Für AI-Agents (Claw etc.):
- Verwende `./scripts/safe-commit.sh "Nachricht"` statt `git commit -m "..."`
- Das Script garantiert dass lint-staged läuft (PHP, HTML, CSS, JS, Prettier)
- Wenn ein Linter fehlschlägt: **Fehler beheben, nicht überspringen!**
- Niemals `git commit --no-verify` verwenden
### Lint-Checks die laufen:
- **PHP:** `php -l` Syntax-Check (via `scripts/lint-php.sh`)
- **HTML:** htmlhint
- **CSS:** stylelint + prettier
- **JS:** eslint + prettier
- **JSON/MD:** prettier
---
## Systemumgebung ## Systemumgebung
- **Betriebssystem: Linux** Befehle und Pfade sind Linux-kompatibel. - **Betriebssystem: Windows** Alle Befehle und Pfade müssen Windows-kompatibel sein (z. B. Pfadtrennzeichen `\`, PowerShell-Syntax).
--- ---
@@ -36,7 +17,7 @@
| Pfad | Beschreibung | | Pfad | Beschreibung |
| --------------------------- | -------------------------------------------- | | --------------------------- | -------------------------------------------- |
| `public/index.php` | Einstiegsseite (PHP-Entry-Point) | | `haus-schleusingen.html` | Einstiegsseite (einzige HTML-Datei) |
| `css/haus-schleusingen.css` | Hauptstylesheet | | `css/haus-schleusingen.css` | Hauptstylesheet |
| `js/haus-schleusingen.js` | Haupt-JavaScript | | `js/haus-schleusingen.js` | Haupt-JavaScript |
| `js/masonry.pkgd.min.js` | Masonry-Layout-Bibliothek (nicht bearbeiten) | | `js/masonry.pkgd.min.js` | Masonry-Layout-Bibliothek (nicht bearbeiten) |

0
Dockerfile Executable file → Normal file
View File

20
README.md Executable file → Normal file
View File

@@ -4,8 +4,8 @@ Statische Landingpage für **Haus Schleusingen**.
Das Projekt basiert auf reinem HTML, CSS und JavaScript und wird über einen Nginx-Container ausgeliefert. Das Projekt basiert auf reinem HTML, CSS und JavaScript und wird über einen Nginx-Container ausgeliefert.
<div align="right"> <div align="right">
<a href="docs/screenshot-landingpage.png"> <a href="screenshot-landingpage.png">
<img src="docs/screenshot-landingpage-thumb.png" alt="Screenshot der Landingpage Haus Schleusingen" width="300" /> <img src="screenshot-landingpage-thumb.png" alt="Screenshot der Landingpage Haus Schleusingen" width="300" />
</a> </a>
</div> </div>
@@ -44,7 +44,7 @@ Das Projekt basiert auf reinem HTML, CSS und JavaScript und wird über einen Ngi
## Projektstruktur ## Projektstruktur
``` ```
├── public/index.php # Einstiegsseite (PHP-Entry-Point) ├── haus-schleusingen.html # Einstiegsseite (einzige HTML-Datei)
├── css/ ├── css/
│ └── haus-schleusingen.css # Hauptstylesheet │ └── haus-schleusingen.css # Hauptstylesheet
├── js/ ├── js/
@@ -172,20 +172,6 @@ npm run lint
## Git-Workflow ## Git-Workflow
### Pre-Commit Hooks aktivieren
Die Pre-Commit Hooks (Husky + lint-staged) werden automatisch beim Installieren der Abhängigkeiten eingerichtet:
```bash
npm install
```
Der `prepare`-Script in `package.json` (`"prepare": "husky"`) sorgt dafür, dass Husky die Git Hooks im `.husky/`-Verzeichnis registriert. Nach `npm install` sind die Hooks aktiv kein manueller Schritt nötig.
> **Falls Hooks nicht laufen:** Prüfe ob `.husky/pre-commit` ausführbar ist (`chmod +x .husky/pre-commit`) und ob `core.hooksPath` nicht überschrieben wurde (`git config core.hooksPath`).
### Was wird beim Commit geprüft?
Beim Committen führt **Husky** automatisch den Pre-Commit Hook (`.husky/pre-commit`) aus, der **lint-staged** startet. Beim Committen führt **Husky** automatisch den Pre-Commit Hook (`.husky/pre-commit`) aus, der **lint-staged** startet.
### lint-staged prüft automatisch: ### lint-staged prüft automatisch:

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\View;
abstract class Controller
{
protected View $view;
public function __construct()
{
$this->view = new View();
}
protected function render(string $view, array $data = [], string $layout = 'main'): void
{
foreach ($data as $key => $value) {
$this->view->assign($key, $value);
}
$this->view->render($view, $layout);
}
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class DatenschutzController extends Controller
{
public function index(): void
{
$this->render('datenschutz/index', [
'pageTitle' => 'Datenschutzerklärung Haus Schleusingen',
'pageDescription' => 'Datenschutzerklärung der Website haus-schleusingen.de',
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/datenschutz',
]);
}
}

View File

@@ -1,187 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class HomeController extends Controller
{
public function index(): void
{
session_start();
// --- Helper functions ---
$normalizeContactValue = function (string $value): string {
return trim($value);
};
$escapeContactValue = function (string $value): string {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
};
$containsHeaderInjection = function (string $value): bool {
return (bool) preg_match('/[\r\n]/', $value);
};
// --- Form processing ---
$formErrors = [];
$formSuccess = false;
if (!empty($_SESSION['form_success'])) {
$formSuccess = true;
unset($_SESSION['form_success']);
}
if (!empty($_SESSION['form_errors'])) {
$formErrors = $_SESSION['form_errors'];
unset($_SESSION['form_errors']);
}
if (!empty($_SESSION['form_data'])) {
$formData = $_SESSION['form_data'];
unset($_SESSION['form_data']);
} else {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => ''];
}
// CSRF-Token generieren (nach Session-Start)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF-Token validieren
$csrfToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $csrfToken)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = ['Sicherheitsüberprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.'];
exit;
}
$formData['fname'] = $normalizeContactValue((string) ($_POST['fname'] ?? ''));
$formData['lname'] = $normalizeContactValue((string) ($_POST['lname'] ?? ''));
$formData['email'] = $normalizeContactValue((string) ($_POST['email'] ?? ''));
$formData['phone'] = $normalizeContactValue((string) ($_POST['phone'] ?? ''));
$formData['interest'] = $normalizeContactValue((string) ($_POST['interest'] ?? ''));
$formData['message'] = $normalizeContactValue((string) ($_POST['message'] ?? ''));
$honeypot = $normalizeContactValue((string) ($_POST['website'] ?? ''));
if ($honeypot !== '') {
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
if ($formData['fname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Vornamen an.';
}
if ($formData['lname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Nachnamen an.';
}
if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$formErrors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
}
if ($formData['message'] === '') {
$formErrors[] = 'Bitte geben Sie eine Nachricht ein.';
}
if ($containsHeaderInjection($formData['email']) || $containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) {
$formErrors[] = 'Ungültige Zeichen in den Eingabefeldern.';
}
$formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0;
if ($formTime > 0 && (time() - $formTime) < 3) {
$formErrors[] = 'Das Formular wurde zu schnell abgeschickt. Bitte versuchen Sie es erneut.';
}
$lastSubmit = $_SESSION['last_contact_submit'] ?? 0;
if ($lastSubmit && (time() - $lastSubmit) < 60) {
$formErrors[] = 'Bitte warten Sie einen Moment vor der nächsten Anfrage.';
}
if (empty($formErrors)) {
$to = 'mki@kies-media.de';
$subject = 'Kontaktanfrage: ' . $formData['interest'];
$body = "Von: {$formData['fname']} {$formData['lname']}\n"
. "E-Mail: {$formData['email']}\n";
if ($formData['phone'] !== '') {
$body .= "Telefon: {$formData['phone']}\n";
}
$body .= "Anliegen: {$formData['interest']}\n\n"
. $formData['message'];
$headers = "From: {$formData['email']}\r\n";
$headers .= "Reply-To: {$formData['email']}\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$mailSent = mail($to, $subject, $body, $headers);
if ($mailSent) {
$_SESSION['last_contact_submit'] = time();
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
$formErrors[] = 'Leider konnte die E-Mail nicht gesendet werden. Bitte versuchen Sie es später erneut oder schreiben Sie uns direkt an mki@kies-media.de.';
}
}
}
if (!empty($formErrors)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = $formErrors;
$_SESSION['form_data'] = $formData;
exit;
}
}
$this->render('home/index', [
'formSuccess' => $formSuccess,
'formErrors' => $formErrors,
'formData' => $formData,
'escapeContactValue' => $escapeContactValue,
'pageTitle' => 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete',
'pageDescription' => 'Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar.',
'canonical' => 'https://haus-schleusingen.de/',
'openGraph' => [
'ogTitle' => 'Einfamilienhaus zur Miete in Schleusingen 227 m², 6 Zimmer',
'ogDescription' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m², 6 Zimmer, 3 Etagen + Dachterrasse. Kaltmiete 1.300 €. Ab sofort verfügbar in Schleusingen.',
'ogImage' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'ogUrl' => 'https://haus-schleusingen.de/',
],
'structuredData' => json_encode([
'@context' => 'https://schema.org',
'@type' => 'RealEstateListing',
'name' => 'Einfamilienhaus zur Miete in Schleusingen',
'description' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €.',
'url' => 'https://haus-schleusingen.de/',
'image' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'datePosted' => '2026-05-14',
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => 'Bahnhofstraße 10',
'addressLocality' => 'Schleusingen',
'postalCode' => '98553',
'addressCountry' => 'DE',
],
'offers' => [
'@type' => 'Offer',
'price' => '1300',
'priceCurrency' => 'EUR',
'priceSpecification' => [
'@type' => 'UnitPriceSpecification',
'price' => '1300',
'priceCurrency' => 'EUR',
'unitCode' => 'MON',
'description' => 'Kaltmiete pro Monat',
],
],
'floorSize' => [
'@type' => 'QuantitativeValue',
'value' => '227',
'unitCode' => 'MTK',
],
'numberOfRooms' => [
'@type' => 'QuantitativeValue',
'value' => '6',
],
]),
]);
}
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class ImpressumController extends Controller
{
public function index(): void
{
$this->render('impressum/index', [
'pageTitle' => 'Impressum Haus Schleusingen',
'pageDescription' => 'Impressum der Website haus-schleusingen.de',
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/impressum',
]);
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
class Router
{
private array $routes = [];
public function addRoute(string $path, string $controller, string $action = 'index'): void
{
$this->routes[$path] = [
'controller' => $controller,
'action' => $action,
];
}
public function dispatch(string $uri): void
{
// Normalize: strip query string and trailing slash
$path = parse_url($uri, PHP_URL_PATH);
$path = rtrim($path, '/') ?: '/';
// Direct match
if (isset($this->routes[$path])) {
$this->execute($this->routes[$path]);
return;
}
// Legacy .html redirect (301)
if (preg_match('#^/(impressum|datenschutz)\.html$#', $path, $m)) {
header('Location: /' . $m[1], true, 301);
exit;
}
// 404
http_response_code(404);
echo '<h1>404 Seite nicht gefunden</h1>';
echo '<p><a href="/">Zurück zur Startseite</a></p>';
}
private function execute(array $route): void
{
$controllerClass = $route['controller'];
$action = $route['action'];
if (!class_exists($controllerClass)) {
throw new \RuntimeException("Controller {$controllerClass} nicht gefunden.");
}
$controller = new $controllerClass();
if (!method_exists($controller, $action)) {
throw new \RuntimeException("Action {$action} in {$controllerClass} nicht gefunden.");
}
$controller->$action();
}
}

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
class View
{
private string $viewsPath;
private array $data = [];
public function __construct(?string $viewsPath = null)
{
$this->viewsPath = $viewsPath ?? dirname(__DIR__) . '/views';
}
public function assign(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
public function render(string $view, string $layout = 'main'): void
{
$viewFile = $this->viewsPath . '/' . $view . '.php';
$layoutFile = $this->viewsPath . '/layouts/' . $layout . '.php';
if (!file_exists($viewFile)) {
throw new \RuntimeException("View {$view} nicht gefunden: {$viewFile}");
}
if (!file_exists($layoutFile)) {
throw new \RuntimeException("Layout {$layout} nicht gefunden: {$layoutFile}");
}
// Extract data to variables for the view
extract($this->data, EXTR_SKIP);
// Capture view content
ob_start();
require $viewFile;
$content = ob_get_clean();
// Render layout with $content
require $layoutFile;
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
use App\Core\View;
abstract class Controller
{
protected View $view;
public function __construct()
{
$this->view = new View();
}
protected function render(string $view, array $data = [], string $layout = 'main'): void
{
foreach ($data as $key => $value) {
$this->view->assign($key, $value);
}
$this->view->render($view, $layout);
}
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class DatenschutzController extends Controller
{
public function index(): void
{
$this->render('datenschutz/index', [
'pageTitle' => 'Datenschutzerklärung Haus Schleusingen',
'pageDescription' => 'Datenschutzerklärung der Website haus-schleusingen.de',
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/datenschutz',
]);
}
}

View File

@@ -1,187 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class HomeController extends Controller
{
public function index(): void
{
session_start();
// --- Helper functions ---
$normalizeContactValue = function (string $value): string {
return trim($value);
};
$escapeContactValue = function (string $value): string {
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
};
$containsHeaderInjection = function (string $value): bool {
return (bool) preg_match('/[\r\n]/', $value);
};
// --- Form processing ---
$formErrors = [];
$formSuccess = false;
if (!empty($_SESSION['form_success'])) {
$formSuccess = true;
unset($_SESSION['form_success']);
}
if (!empty($_SESSION['form_errors'])) {
$formErrors = $_SESSION['form_errors'];
unset($_SESSION['form_errors']);
}
if (!empty($_SESSION['form_data'])) {
$formData = $_SESSION['form_data'];
unset($_SESSION['form_data']);
} else {
$formData = ['fname' => '', 'lname' => '', 'email' => '', 'phone' => '', 'interest' => 'Besichtigung anfragen', 'message' => ''];
}
// CSRF-Token generieren (nach Session-Start)
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF-Token validieren
$csrfToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'] ?? '', $csrfToken)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = ['Sicherheitsüberprüfung fehlgeschlagen. Bitte versuchen Sie es erneut.'];
exit;
}
$formData['fname'] = $normalizeContactValue((string) ($_POST['fname'] ?? ''));
$formData['lname'] = $normalizeContactValue((string) ($_POST['lname'] ?? ''));
$formData['email'] = $normalizeContactValue((string) ($_POST['email'] ?? ''));
$formData['phone'] = $normalizeContactValue((string) ($_POST['phone'] ?? ''));
$formData['interest'] = $normalizeContactValue((string) ($_POST['interest'] ?? ''));
$formData['message'] = $normalizeContactValue((string) ($_POST['message'] ?? ''));
$honeypot = $normalizeContactValue((string) ($_POST['website'] ?? ''));
if ($honeypot !== '') {
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
if ($formData['fname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Vornamen an.';
}
if ($formData['lname'] === '') {
$formErrors[] = 'Bitte geben Sie Ihren Nachnamen an.';
}
if ($formData['email'] === '' || !filter_var($formData['email'], FILTER_VALIDATE_EMAIL)) {
$formErrors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
}
if ($formData['message'] === '') {
$formErrors[] = 'Bitte geben Sie eine Nachricht ein.';
}
if ($containsHeaderInjection($formData['email']) || $containsHeaderInjection($formData['fname'] . ' ' . $formData['lname'])) {
$formErrors[] = 'Ungültige Zeichen in den Eingabefeldern.';
}
$formTime = isset($_POST['form_time']) ? (int) $_POST['form_time'] : 0;
if ($formTime > 0 && (time() - $formTime) < 3) {
$formErrors[] = 'Das Formular wurde zu schnell abgeschickt. Bitte versuchen Sie es erneut.';
}
$lastSubmit = $_SESSION['last_contact_submit'] ?? 0;
if ($lastSubmit && (time() - $lastSubmit) < 60) {
$formErrors[] = 'Bitte warten Sie einen Moment vor der nächsten Anfrage.';
}
if (empty($formErrors)) {
$to = 'mki@kies-media.de';
$subject = 'Kontaktanfrage: ' . $formData['interest'];
$body = "Von: {$formData['fname']} {$formData['lname']}\n"
. "E-Mail: {$formData['email']}\n";
if ($formData['phone'] !== '') {
$body .= "Telefon: {$formData['phone']}\n";
}
$body .= "Anliegen: {$formData['interest']}\n\n"
. $formData['message'];
$headers = "From: {$formData['email']}\r\n";
$headers .= "Reply-To: {$formData['email']}\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$headers .= "X-Mailer: PHP/" . phpversion();
$mailSent = mail($to, $subject, $body, $headers);
if ($mailSent) {
$_SESSION['last_contact_submit'] = time();
header('Location: /#form-result');
$_SESSION['form_success'] = true;
exit;
} else {
$formErrors[] = 'Leider konnte die E-Mail nicht gesendet werden. Bitte versuchen Sie es später erneut oder schreiben Sie uns direkt an mki@kies-media.de.';
}
}
}
if (!empty($formErrors)) {
header('Location: /#form-result');
$_SESSION['form_errors'] = $formErrors;
$_SESSION['form_data'] = $formData;
exit;
}
}
$this->render('home/index', [
'formSuccess' => $formSuccess,
'formErrors' => $formErrors,
'formData' => $formData,
'escapeContactValue' => $escapeContactValue,
'pageTitle' => 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete',
'pageDescription' => 'Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar.',
'canonical' => 'https://haus-schleusingen.de/',
'openGraph' => [
'ogTitle' => 'Einfamilienhaus zur Miete in Schleusingen 227 m², 6 Zimmer',
'ogDescription' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m², 6 Zimmer, 3 Etagen + Dachterrasse. Kaltmiete 1.300 €. Ab sofort verfügbar in Schleusingen.',
'ogImage' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'ogUrl' => 'https://haus-schleusingen.de/',
],
'structuredData' => json_encode([
'@context' => 'https://schema.org',
'@type' => 'RealEstateListing',
'name' => 'Einfamilienhaus zur Miete in Schleusingen',
'description' => 'Großzügiges Einfamilienhaus zur Langzeitmiete: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €.',
'url' => 'https://haus-schleusingen.de/',
'image' => 'https://haus-schleusingen.de/bilder/Außenansicht-2.png',
'datePosted' => '2026-05-14',
'address' => [
'@type' => 'PostalAddress',
'streetAddress' => 'Bahnhofstraße 10',
'addressLocality' => 'Schleusingen',
'postalCode' => '98553',
'addressCountry' => 'DE',
],
'offers' => [
'@type' => 'Offer',
'price' => '1300',
'priceCurrency' => 'EUR',
'priceSpecification' => [
'@type' => 'UnitPriceSpecification',
'price' => '1300',
'priceCurrency' => 'EUR',
'unitCode' => 'MON',
'description' => 'Kaltmiete pro Monat',
],
],
'floorSize' => [
'@type' => 'QuantitativeValue',
'value' => '227',
'unitCode' => 'MTK',
],
'numberOfRooms' => [
'@type' => 'QuantitativeValue',
'value' => '6',
],
]),
]);
}
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controllers;
class ImpressumController extends Controller
{
public function index(): void
{
$this->render('impressum/index', [
'pageTitle' => 'Impressum Haus Schleusingen',
'pageDescription' => 'Impressum der Website haus-schleusingen.de',
'robots' => 'noindex',
'canonical' => 'https://haus-schleusingen.de/impressum',
]);
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
class Router
{
private array $routes = [];
public function addRoute(string $path, string $controller, string $action = 'index'): void
{
$this->routes[$path] = [
'controller' => $controller,
'action' => $action,
];
}
public function dispatch(string $uri): void
{
// Normalize: strip query string and trailing slash
$path = parse_url($uri, PHP_URL_PATH);
$path = rtrim($path, '/') ?: '/';
// Direct match
if (isset($this->routes[$path])) {
$this->execute($this->routes[$path]);
return;
}
// Legacy .html redirect (301)
if (preg_match('#^/(impressum|datenschutz)\.html$#', $path, $m)) {
header('Location: /' . $m[1], true, 301);
exit;
}
// 404
http_response_code(404);
echo '<h1>404 Seite nicht gefunden</h1>';
echo '<p><a href="/">Zurück zur Startseite</a></p>';
}
private function execute(array $route): void
{
$controllerClass = $route['controller'];
$action = $route['action'];
if (!class_exists($controllerClass)) {
throw new \RuntimeException("Controller {$controllerClass} nicht gefunden.");
}
$controller = new $controllerClass();
if (!method_exists($controller, $action)) {
throw new \RuntimeException("Action {$action} in {$controllerClass} nicht gefunden.");
}
$controller->$action();
}
}

View File

@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Core;
class View
{
private string $viewsPath;
private array $data = [];
public function __construct(?string $viewsPath = null)
{
$this->viewsPath = $viewsPath ?? dirname(__DIR__) . '/views';
}
public function assign(string $key, mixed $value): void
{
$this->data[$key] = $value;
}
public function render(string $view, string $layout = 'main'): void
{
$viewFile = $this->viewsPath . '/' . $view . '.php';
$layoutFile = $this->viewsPath . '/layouts/' . $layout . '.php';
if (!file_exists($viewFile)) {
throw new \RuntimeException("View {$view} nicht gefunden: {$viewFile}");
}
if (!file_exists($layoutFile)) {
throw new \RuntimeException("Layout {$layout} nicht gefunden: {$layoutFile}");
}
// Extract data to variables for the view
extract($this->data, EXTR_SKIP);
// Capture view content
ob_start();
require $viewFile;
$content = ob_get_clean();
// Render layout with $content
require $layoutFile;
}
}

View File

@@ -1,123 +0,0 @@
<nav id="navbar" class="scrolled">
<div class="nav-logo">Bahnhofstraße 10</div>
<ul class="nav-links">
<li><a href="/#galerie">Galerie</a></li>
<li><a href="/#grundriss">Grundriss</a></li>
<li><a href="/#miete">Miete</a></li>
<li><a href="/#lage">Lage</a></li>
</ul>
<a href="/#kontakt" class="nav-cta" style="text-decoration:none;">Jetzt anfragen</a>
</nav>
<main class="legal-page">
<div class="section-eyebrow">Datenschutz</div>
<h1>Datenschutzerklärung</h1>
<h2>1. Verantwortliche Stelle</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Telefon: 0176 45853923<br />
E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</address>
<p>
Diese Datenschutzerklärung informiert Sie über die Art, den Umfang und den Zweck der Erhebung und Verwendung personenbezogener Daten auf dieser Website.
</p>
<hr class="legal-divider" />
<h2>2. Erhebung und Speicherung personenbezogener Daten</h2>
<h3>a) Beim Besuch der Website</h3>
<p>
Beim Aufrufen dieser Website werden durch den Hosting-Anbieter automatisch Informationen allgemeiner Natur erfasst. Diese Informationen (Server-Logfiles) beinhalten etwa die Art des Webbrowsers, das verwendete Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und Ähnliches. Sie werden insbesondere zu einem sicheren und reibungslosen Betrieb der Website benötigt.
</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit der Anfrage</li>
<li>Zeitzonenunterschied zur Greenwich Mean Time (GMT)</li>
<li>Inhalt der Anforderung (konkrete Seite)</li>
<li>Zugriffsstatus/HTTP-Statuscode</li>
<li>Jeweils übertragene Datenmenge</li>
<li>Website, von der die Anforderung kommt (Referrer-URL)</li>
<li>Verwendeter Browser</li>
<li>Verwendetes Betriebssystem</li>
</ul>
<p>
Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Die Daten werden nicht mit anderen Datenquellen zusammengeführt und nach 30 Tagen automatisch gelöscht.
</p>
<h3>b) Kontakt per E-Mail</h3>
<p>
Auf dieser Website wird die Kontaktaufnahme über einen E-Mail-Link (mailto:) angeboten. Wenn Sie uns per E-Mail kontaktieren, werden Ihre Angaben (E-Mail-Adresse, ggf. Name und Nachricht) zwecks Bearbeitung Ihrer Anfrage gespeichert. Die Daten werden ausschließlich zur Beantwortung Ihrer Anfrage verwendet und nach Abschluss der Kommunikation gelöscht.
</p>
<p>
Rechtsgrundlage ist Art. 6 Abs. 1 lit. a DSGVO (Ihre Einwilligung) oder Art. 6 Abs. 1 lit. b DSGVO (zur Erfüllung eines Vertrags bzw. vorvertraglicher Maßnahmen).
</p>
<hr class="legal-divider" />
<h2>3. Cookies</h2>
<p>
Diese Website verwendet <strong>keine Cookies</strong>. Es werden keine Tracking-Cookies, Werbe-Cookies oder sonstige Cookies gesetzt.
</p>
<h2>4. Tracking &amp; Analyse</h2>
<p>
Diese Website setzt <strong>keine Tracking- oder Analyse-Tools</strong> ein. Es werden keine Besucherstatistiken erstellt, kein Google Analytics, kein Facebook Pixel und keine ähnlichen Dienste verwendet.
</p>
<h2>5. Social-Media-Plugins</h2>
<p>
Diese Website verwendet <strong>keine Social-Media-Plugins</strong> (Facebook, Twitter, Instagram etc.).
</p>
<hr class="legal-divider" />
<h2>6. Google Maps</h2>
<p>
Auf dieser Website wird ein Google Maps-Embed (Kartenansicht) eingebunden. Beim Laden der Karte werden Daten an Google übertragen, darunter möglicherweise Ihre IP-Adresse. Google Maps wird ausschließlich genutzt, um Ihnen die Lage des Mietobjekts anzuzeigen.
</p>
<p>
Weitere Informationen zum Datenschutz bei Google finden Sie unter: <a href="https://policies.google.com/privacy" target="_blank" rel="noopener">https://policies.google.com/privacy</a>
</p>
<p>
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigte Interessen an der Darstellung des Objektstandorts).
</p>
<hr class="legal-divider" />
<h2>7. SSL-Verschlüsselung</h2>
<p>
Diese Seite nutzt aus Sicherheitsgründen eine SSL-Verschlüsselung. Eine verschlüsselte Verbindung erkennen Sie daran, dass die Adresszeile des Browsers von „http://" auf „https://" wechselt und an dem Schloss-Symbol in Ihrer Browserzeile.
</p>
<hr class="legal-divider" />
<h2>8. Ihre Rechte</h2>
<p>Sie haben gegenüber uns folgende Rechte hinsichtlich der Sie betreffenden personenbezogenen Daten:</p>
<ul>
<li><strong>Recht auf Auskunft</strong> (Art. 15 DSGVO)</li>
<li><strong>Recht auf Berichtigung</strong> (Art. 16 DSGVO)</li>
<li><strong>Recht auf Löschung</strong> (Art. 17 DSGVO)</li>
<li><strong>Recht auf Einschränkung der Verarbeitung</strong> (Art. 18 DSGVO)</li>
<li><strong>Recht auf Datenübertragbarkeit</strong> (Art. 20 DSGVO)</li>
<li><strong>Widerspruchsrecht</strong> (Art. 21 DSGVO)</li>
<li><strong>Recht auf Widerruf der Einwilligung</strong> (Art. 7 Abs. 3 DSGVO)</li>
<li><strong>Beschwerderecht bei einer Aufsichtsbehörde</strong> (Art. 77 DSGVO)</li>
</ul>
<p>
Zur Ausübung Ihrer Rechte wenden Sie sich bitte an: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a>
</p>
<a href="/" class="legal-back"> Zurück zum Objekt</a>
</main>
<footer>
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</div>
</footer>

View File

@@ -1,85 +0,0 @@
<nav id="navbar" class="scrolled">
<div class="nav-logo">Bahnhofstraße 10</div>
<ul class="nav-links">
<li><a href="/#galerie">Galerie</a></li>
<li><a href="/#grundriss">Grundriss</a></li>
<li><a href="/#miete">Miete</a></li>
<li><a href="/#lage">Lage</a></li>
</ul>
<a href="/#kontakt" class="nav-cta" style="text-decoration:none;">Jetzt anfragen</a>
</nav>
<main class="legal-page">
<div class="section-eyebrow">Pflichtangaben</div>
<h1>Impressum</h1>
<h2>Angaben gemäß § 5 TMG</h2>
<address>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl<br />
Deutschland
</address>
<h3>Kontakt</h3>
<ul>
<li>Telefon: 0176 45853923</li>
<li>E-Mail: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></li>
</ul>
<hr class="legal-divider" />
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
<p>
Martin Kiesewetter<br />
Am Schaftalsgraben 4<br />
98529 Suhl
</p>
<hr class="legal-divider" />
<h2>Streitschlichtung</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener">https://ec.europa.eu/consumers/odr/</a>
</p>
<p>
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
</p>
<hr class="legal-divider" />
<h2>Haftung für Inhalte</h2>
<p>
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine rechtswidrige Tätigkeit hinweisen.
</p>
<p>
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese Inhalte umgehend entfernen.
</p>
<h2>Haftung für Links</h2>
<p>
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige Anbieter oder Betreiber der Seiten verantwortlich.
</p>
<p>
Die verlinkten Seiten wurden zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar. Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Links umgehend entfernen.
</p>
<h2>Urheberrecht</h2>
<p>
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung, Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers. Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen Gebrauch gestattet.
</p>
<p>
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
</p>
<a href="/" class="legal-back"> Zurück zum Objekt</a>
</main>
<footer>
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links">
<a href="/impressum">Impressum</a>
<a href="/datenschutz">Datenschutz</a>
</div>
</footer>

View File

@@ -1,51 +0,0 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<?php if (!isset($pageTitle)) $pageTitle = 'Einfamilienhaus mieten Schleusingen | 227 m², 6 Zimmer | 1.300 € Kaltmiete'; ?>
<title><?= htmlspecialchars($pageTitle) ?></title>
<?php if (isset($pageDescription)): ?>
<meta name="description" content="<?= htmlspecialchars($pageDescription) ?>" />
<?php else: ?>
<meta name="description" content="Einfamilienhaus zur Langzeitmiete in Schleusingen: 227 m² Wohnfläche, 6 Zimmer, 3 Etagen mit Dachterrasse. Kaltmiete 1.300 €. Bahnhofstraße 10, 98553 Schleusingen. Ab sofort verfügbar." />
<?php endif; ?>
<?php if (isset($robots)): ?>
<meta name="robots" content="<?= htmlspecialchars($robots) ?>" />
<?php endif; ?>
<?php if (isset($canonical)): ?>
<link rel="canonical" href="<?= htmlspecialchars($canonical) ?>" />
<?php endif; ?>
<link rel="icon" type="image/png" sizes="32x32" href="/bilder/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/bilder/favicon/favicon-16x16.png">
<link rel="icon" type="image/x-icon" href="/bilder/favicon/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/bilder/favicon/apple-touch-icon.png">
<link rel="manifest" href="/bilder/favicon/site.webmanifest">
<?php if (isset($openGraph)): extract($openGraph); ?>
<meta property="og:type" content="website" />
<meta property="og:title" content="<?= htmlspecialchars($ogTitle ?? '') ?>" />
<meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? '') ?>" />
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? '') ?>" />
<meta property="og:url" content="<?= htmlspecialchars($ogUrl ?? '') ?>" />
<meta property="og:locale" content="de_DE" />
<meta property="og:site_name" content="Haus Schleusingen" />
<?php endif; ?>
<?php if (isset($structuredData)): ?>
<script type="application/ld+json"><?= $structuredData ?></script>
<?php endif; ?>
<link rel="stylesheet" href="/fonts/fonts.css" />
<link rel="stylesheet" href="/css/haus-schleusingen.css" />
<?php if (isset($extraCss)): ?>
<style><?= $extraCss ?></style>
<?php endif; ?>
</head>
<body>
<?= $content ?>
<script src="/js/haus-schleusingen.js"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 231 KiB

After

Width:  |  Height:  |  Size: 231 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
public/bilder/Bad-2.jpeg → bilder/Bad-2.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 259 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

0
public/bilder/Bad-3.jpeg → bilder/Bad-3.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 211 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

0
public/bilder/Bad-4.jpeg → bilder/Bad-4.jpeg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 220 KiB

0
public/bilder/Bad-small.jpg → bilder/Bad-small.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

0
public/bilder/Bad.jpg → bilder/Bad.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 115 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

0
public/bilder/Küche 1.jpg → bilder/Küche 1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 501 KiB

After

Width:  |  Height:  |  Size: 501 KiB

View File

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 891 KiB

After

Width:  |  Height:  |  Size: 891 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 664 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 416 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 390 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

Before

Width:  |  Height:  |  Size: 587 KiB

After

Width:  |  Height:  |  Size: 587 KiB

View File

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 167 KiB

View File

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 552 KiB

View File

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

Before

Width:  |  Height:  |  Size: 619 KiB

After

Width:  |  Height:  |  Size: 619 KiB

View File

@@ -1,16 +0,0 @@
{
"name": "greggy/landingpage-haus-schleusingen",
"autoload": {
"psr-4": {
"App\\": "app/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"require-dev": {
"phpunit/phpunit": "^11.0"
}
}

1800
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -140,105 +140,23 @@ nav.scrolled .nav-links a:hover {
.nav-cta { .nav-cta {
font-family: "DM Sans", sans-serif; font-family: "DM Sans", sans-serif;
font-size: 0.82rem; font-size: 0.78rem;
font-weight: 600; font-weight: 500;
letter-spacing: 0.12em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
background: var(--accent); background: var(--accent);
color: var(--white); color: var(--white);
border: none; border: none;
padding: 0.8rem 2rem; padding: 0.65rem 1.5rem;
cursor: pointer; cursor: pointer;
border-radius: 2px;
box-shadow: 0 2px 12px rgb(139 105 20 / 35%);
transition: transition:
background 0.3s, background 0.3s,
transform 0.2s, transform 0.2s;
box-shadow 0.3s;
} }
.nav-cta:hover { .nav-cta:hover {
background: var(--accent-light); background: var(--accent-light);
transform: translateY(-2px); transform: translateY(-1px);
box-shadow: 0 4px 20px rgb(139 105 20 / 50%);
}
/* HAMBURGER */
.nav-hamburger {
display: none;
width: 44px;
height: 44px;
background: none;
border: none;
cursor: pointer;
position: relative;
z-index: 110;
align-items: center;
justify-content: center;
padding: 0;
}
.nav-hamburger span,
.nav-hamburger span::before,
.nav-hamburger span::after {
display: block;
width: 22px;
height: 2px;
background: var(--white);
border-radius: 1px;
transition:
transform 0.3s ease,
opacity 0.3s ease,
background 0.4s;
position: absolute;
}
.nav-hamburger span::before,
.nav-hamburger span::after {
content: "";
}
.nav-hamburger span::before {
transform: translateY(-7px);
}
.nav-hamburger span::after {
transform: translateY(7px);
}
nav.scrolled .nav-hamburger span,
nav.scrolled .nav-hamburger span::before,
nav.scrolled .nav-hamburger span::after {
background: var(--dark);
}
.nav-hamburger.active span {
background: transparent;
}
.nav-hamburger.active span::before {
transform: rotate(45deg);
background: var(--dark);
}
.nav-hamburger.active span::after {
transform: rotate(-45deg);
background: var(--dark);
}
.nav-mobile-overlay {
display: none;
position: fixed;
inset: 0;
background: rgb(28 26 23 / 50%);
z-index: 90;
opacity: 0;
transition: opacity 0.3s ease;
}
.nav-mobile-overlay.active {
display: block;
opacity: 1;
} }
/* HERO */ /* HERO */
@@ -1039,36 +957,6 @@ nav.scrolled .nav-hamburger span::after {
transform: translateY(-1px); transform: translateY(-1px);
} }
.form-errors {
background: #fdf2f2;
border: 1px solid #e8a0a0;
padding: 1rem 1.2rem;
margin-bottom: 1.2rem;
}
.form-errors ul {
margin: 0;
padding: 0 0 0 1.2rem;
list-style: disc;
}
.form-errors li {
font-size: 0.85rem;
color: #9e2c2c;
line-height: 1.6;
}
.hp-field {
position: absolute;
left: -9999px;
top: -9999px;
width: 0;
height: 0;
overflow: hidden;
opacity: 0;
pointer-events: none;
}
.form-success { .form-success {
display: none; display: none;
text-align: center; text-align: center;
@@ -1198,59 +1086,6 @@ footer {
display: none; display: none;
} }
.nav-hamburger {
display: flex;
}
/* Mobile slide-down nav */
nav.mobile-open .nav-links {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
right: 0;
background: rgb(253 252 250 / 98%);
backdrop-filter: blur(12px);
padding: 5rem 1.5rem 2rem;
gap: 0;
z-index: 95;
border-bottom: 1px solid var(--warm);
animation: slide-down 0.3s ease;
}
nav.mobile-open .nav-links a {
color: var(--charcoal);
font-size: 1rem;
padding: 1rem 0;
border-bottom: 1px solid var(--warm);
display: flex;
align-items: center;
min-height: 44px;
}
nav.mobile-open .nav-hamburger span,
nav.mobile-open .nav-hamburger span::before,
nav.mobile-open .nav-hamburger span::after {
background: var(--dark);
}
nav.mobile-open .nav-hamburger.active span {
background: transparent;
}
@keyframes slide-down {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-content { .hero-content {
padding: 0 1.5rem 4rem; padding: 0 1.5rem 4rem;
} }

0
docs/docker-preview.png → docker-preview.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

1
eslint.config.js Executable file → Normal file
View File

@@ -13,6 +13,7 @@ module.exports = [
sourceType: "script", sourceType: "script",
globals: { globals: {
...globals.browser, ...globals.browser,
...globals.jquery,
}, },
}, },
plugins: { plugins: {

View File

View File

View File

0
public/fonts/fonts.css → fonts/fonts.css Executable file → Normal file
View File

View File

@@ -1,9 +1,17 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Einfamilienhaus zur Miete - Schleusingen</title>
<link rel="stylesheet" href="fonts/fonts.css" />
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="css/haus-schleusingen.css" />
</head>
<body>
<a href="#main-content" class="skip-link">Zum Inhalt springen</a> <a href="#main-content" class="skip-link">Zum Inhalt springen</a>
<nav id="navbar" role="navigation" aria-label="Hauptnavigation"> <nav id="navbar" role="navigation" aria-label="Hauptnavigation">
<div class="nav-logo">Bahnhofstraße 10</div> <div class="nav-logo">Bahnhofstraße 10</div>
<button class="nav-hamburger" aria-label="Navigation öffnen" aria-expanded="false">
<span></span>
</button>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="#galerie">Galerie</a></li> <li><a href="#galerie">Galerie</a></li>
<li><a href="#grundriss">Grundriss</a></li> <li><a href="#grundriss">Grundriss</a></li>
@@ -17,13 +25,12 @@
Jetzt anfragen Jetzt anfragen
</button> </button>
</nav> </nav>
<div class="nav-mobile-overlay" aria-hidden="true"></div>
<section class="hero" id="hero"> <section class="hero" id="hero">
<div <div
class="hero-bg" class="hero-bg"
id="heroBg" id="heroBg"
style="background-image: url(/bilder/Außenansicht-2.webp)" style="background-image: url(bilder/Außenansicht-2.png)"
></div> ></div>
<div class="hero-overlay"></div> <div class="hero-overlay"></div>
<div class="hero-content" id="heroContent"> <div class="hero-content" id="heroContent">
@@ -97,10 +104,7 @@
</div> </div>
</div> </div>
<div class="intro-img" data-animate> <div class="intro-img" data-animate>
<picture> <img src="bilder/wohnzimmer2.png" alt="Wohnzimmer" />
<source srcset="/bilder/wohnzimmer2.webp" type="image/webp">
<img src="/bilder/wohnzimmer2.png" alt="Wohnzimmer" loading="lazy" />
</picture>
<div class="intro-img-badge">Wohnzimmer · 42,6 m²</div> <div class="intro-img-badge">Wohnzimmer · 42,6 m²</div>
</div> </div>
</section> </section>
@@ -115,88 +119,55 @@
<div class="masonry-grid"> <div class="masonry-grid">
<div class="grid-sizer"></div> <div class="grid-sizer"></div>
<div class="grid-item" data-img="/bilder/Außenansicht-2.webp" role="button" tabindex="0" aria-label="Außenansicht Großansicht öffnen"> <div class="grid-item" data-img="bilder/Außenansicht-2.png" role="button" tabindex="0" aria-label="Außenansicht Großansicht öffnen">
<picture> <img src="bilder/Außenansicht-2-small.png" alt="Außenansicht des Einfamilienhauses" />
<source srcset="/bilder/Außenansicht-2-small.webp" type="image/webp">
<img src="/bilder/Außenansicht-2-small.png" alt="Außenansicht des Einfamilienhauses" loading="lazy" />
</picture>
<span class="grid-item-label">Außenansicht</span> <span class="grid-item-label">Außenansicht</span>
</div> </div>
<div class="grid-item" data-img="/bilder/wohnzimmer2.webp" role="button" tabindex="0" aria-label="Wohnzimmer Großansicht öffnen"> <div class="grid-item" data-img="bilder/wohnzimmer2.png" role="button" tabindex="0" aria-label="Wohnzimmer Großansicht öffnen">
<picture> <img src="bilder/wohnzimmer2-small.png" alt="Wohnzimmer mit 42,6 m² Wohnfläche" />
<source srcset="/bilder/wohnzimmer2-small.webp" type="image/webp">
<img src="/bilder/wohnzimmer2-small.png" alt="Wohnzimmer mit 42,6 m² Wohnfläche" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnzimmer · 42,6 m²</span> <span class="grid-item-label">Wohnzimmer · 42,6 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Küche 1.webp" role="button" tabindex="0" aria-label="Küche Großansicht öffnen"> <div class="grid-item" data-img="bilder/Küche 1.jpg" role="button" tabindex="0" aria-label="Küche Großansicht öffnen">
<picture> <img src="bilder/Küche 1.jpg" alt="Küche mit 18,4 m²" />
<source srcset="/bilder/Küche 1-small.webp" type="image/webp">
<img src="/bilder/Küche 1.jpg" alt="Küche mit 18,4 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Küche · 18,4 m²</span> <span class="grid-item-label">Küche · 18,4 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/schlafzimmer.webp" role="button" tabindex="0" aria-label="Schlafzimmer Großansicht öffnen"> <div class="grid-item" data-img="bilder/schlafzimmer.png" role="button" tabindex="0" aria-label="Schlafzimmer Großansicht öffnen">
<picture> <img src="bilder/schlafzimmer-small.png" alt="Schlafzimmer mit 18 m²" />
<source srcset="/bilder/schlafzimmer-small.webp" type="image/webp">
<img src="/bilder/schlafzimmer-small.png" alt="Schlafzimmer mit 18 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Schlafzimmer · 18 m²</span> <span class="grid-item-label">Schlafzimmer · 18 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Bad.webp" role="button" tabindex="0" aria-label="Badezimmer Großansicht öffnen"> <div class="grid-item" data-img="bilder/Bad.jpg" role="button" tabindex="0" aria-label="Badezimmer Großansicht öffnen">
<picture> <img src="bilder/Bad.jpg" alt="Badezimmer mit 9,8 m²" />
<source srcset="/bilder/Bad-small.webp" type="image/webp">
<img src="/bilder/Bad.jpg" alt="Badezimmer mit 9,8 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Badezimmer · 9,8 m²</span> <span class="grid-item-label">Badezimmer · 9,8 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Kinderzimmer.webp" role="button" tabindex="0" aria-label="Kinderzimmer 1 Großansicht öffnen"> <div class="grid-item" data-img="bilder/Kinderzimmer.png" role="button" tabindex="0" aria-label="Kinderzimmer 1 Großansicht öffnen">
<picture> <img src="bilder/Kinderzimmer-small.png" alt="Kinderzimmer 1 mit 21,7 m²" />
<source srcset="/bilder/Kinderzimmer-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer-small.png" alt="Kinderzimmer 1 mit 21,7 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer 1 · 21,7 m²</span> <span class="grid-item-label">Kinderzimmer 1 · 21,7 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Kinderzimmer 2.webp" role="button" tabindex="0" aria-label="Kinderzimmer 2 Großansicht öffnen"> <div class="grid-item" data-img="bilder/Kinderzimmer 2.jpg" role="button" tabindex="0" aria-label="Kinderzimmer 2 Großansicht öffnen">
<picture> <img src="bilder/Kinderzimmer 2-small.png" alt="Kinderzimmer 2 mit 15,7 m²" />
<source srcset="/bilder/Kinderzimmer 2-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer 2-small.png" alt="Kinderzimmer 2 mit 15,7 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer 2 · 15,7 m²</span> <span class="grid-item-label">Kinderzimmer 2 · 15,7 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/kinderzimmer 2 2.webp" role="button" tabindex="0" aria-label="Kinderzimmer Detail Großansicht öffnen"> <div class="grid-item" data-img="bilder/kinderzimmer 2 2.jpeg" role="button" tabindex="0" aria-label="Kinderzimmer Detail Großansicht öffnen">
<picture> <img src="bilder/kinderzimmer 2 2-small.png" alt="Detailansicht Kinderzimmer" />
<source srcset="/bilder/kinderzimmer 2 2-small.webp" type="image/webp">
<img src="/bilder/kinderzimmer 2 2-small.png" alt="Detailansicht Kinderzimmer" loading="lazy" />
</picture>
<span class="grid-item-label">Kinderzimmer Detail</span> <span class="grid-item-label">Kinderzimmer Detail</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Kinderzimmer 3.webp" role="button" tabindex="0" aria-label="Gästezimmer Großansicht öffnen"> <div class="grid-item" data-img="bilder/Kinderzimmer 3.jpg" role="button" tabindex="0" aria-label="Gästezimmer Großansicht öffnen">
<picture> <img src="bilder/Kinderzimmer 3-small.png" alt="Gästezimmer mit 11,5 m²" />
<source srcset="/bilder/Kinderzimmer 3-small.webp" type="image/webp">
<img src="/bilder/Kinderzimmer 3-small.png" alt="Gästezimmer mit 11,5 m²" loading="lazy" />
</picture>
<span class="grid-item-label">Gästezimmer · 11,5 m²</span> <span class="grid-item-label">Gästezimmer · 11,5 m²</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Bad-2.webp" role="button" tabindex="0" aria-label="Zweites Bad Großansicht öffnen"> <div class="grid-item" data-img="bilder/Bad-2.jpg" role="button" tabindex="0" aria-label="Zweites Bad Großansicht öffnen">
<picture> <img src="bilder/Bad-2-small.jpg" alt="Zweites Badezimmer im Haus" />
<source srcset="/bilder/Bad-2-small.webp" type="image/webp">
<img src="/bilder/Bad-2-small.jpg" alt="Zweites Badezimmer im Haus" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnbereich</span> <span class="grid-item-label">Wohnbereich</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Bad-3.webp" role="button" tabindex="0" aria-label="Drittes Bad Großansicht öffnen"> <div class="grid-item" data-img="bilder/bad3.jpg" role="button" tabindex="0" aria-label="Drittes Bad Großansicht öffnen">
<picture> <img src="bilder/Bad-3-small.jpg" alt="Drittes Badezimmer im Haus" />
<source srcset="/bilder/Bad-3-small.webp" type="image/webp">
<img src="/bilder/Bad-3-small.jpg" alt="Drittes Badezimmer im Haus" loading="lazy" />
</picture>
<span class="grid-item-label">Wohnbereich Detail</span> <span class="grid-item-label">Wohnbereich Detail</span>
</div> </div>
<div class="grid-item" data-img="/bilder/Bad-4.webp" role="button" tabindex="0" aria-label="Wohnbereich Detail Großansicht öffnen"> <div class="grid-item" data-img="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg" role="button" tabindex="0" aria-label="Hausansicht Großansicht öffnen">
<picture> <img
<source srcset="/bilder/Bad-4-small.webp" type="image/webp"> src="bilder/WhatsApp Image 2026-03-30 at 07.50.42 (2).jpeg"
<img src="/bilder/Bad-4-small.jpg" alt="Wohnbereich Detail 3" loading="lazy" /> alt="Weitere Außenansicht des Einfamilienhauses"
</picture> />
<span class="grid-item-label">Hausansicht</span> <span class="grid-item-label">Hausansicht</span>
</div> </div>
</div> </div>
@@ -216,22 +187,42 @@
</div> </div>
<div class="floor-body" id="floor-body-0" role="region" aria-labelledby="floor-title-0"> <div class="floor-body" id="floor-body-0" role="region" aria-labelledby="floor-title-0">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">20,1 m²</span></div> <div class="room-chip">
<div class="room-chip">WC<span class="room-chip-area">0,8 m²</span></div> Flur
<div class="room-chip">Garage / Partykeller<span class="room-chip-area">42,6</span></div> <span class="room-chip-area">20,1</span>
<div class="room-chip">Abstellraum 1<span class="room-chip-area">9,9 m²</span></div> </div>
<div class="room-chip">Abstellraum 2<span class="room-chip-area">7,8 m²</span></div> <div class="room-chip">
<div class="room-chip">Heizungskeller<span class="room-chip-area">18,3 m²</span></div> WC
<span class="room-chip-area">0,8 m²</span>
</div>
<div class="room-chip">
Garage / Partykeller
<span class="room-chip-area">42,6 m²</span>
</div>
<div class="room-chip">
Abstellraum 1
<span class="room-chip-area">9,9 m²</span>
</div>
<div class="room-chip">
Abstellraum 2
<span class="room-chip-area">7,8 m²</span>
</div>
<div class="room-chip">
Heizungskeller
<span class="room-chip-area">18,3 m²</span>
</div>
</div> </div>
<div class="floor-plan floor-plan-multi"> <div class="floor-plan floor-plan-multi">
<picture> <img
<source srcset="/bilder/grundrisse/EG-small.webp" type="image/webp"> src="bilder/grundrisse/EG-small.jpg"
<img src="/bilder/grundrisse/EG-small.jpg" alt="Grundriss Erdgeschoss" loading="lazy" data-img="/bilder/grundrisse/EG.webp" /> alt="Grundriss Erdgeschoss"
</picture> data-img="bilder/grundrisse/EG.png"
<picture> />
<source srcset="/bilder/grundrisse/EG 3D-small.webp" type="image/webp"> <img
<img src="/bilder/grundrisse/EG 3D-small.jpg" alt="Grundriss Erdgeschoss" loading="lazy" data-img="/bilder/grundrisse/EG 3D.webp" /> src="bilder/grundrisse/EG 3D-small.jpg"
</picture> alt="Grundriss Erdgeschoss"
data-img="bilder/grundrisse/EG 3D.png"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -245,22 +236,42 @@
</div> </div>
<div class="floor-body" id="floor-body-1" role="region" aria-labelledby="floor-title-1"> <div class="floor-body" id="floor-body-1" role="region" aria-labelledby="floor-title-1">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">20,1 m²</span></div> <div class="room-chip">
<div class="room-chip">Wohnzimmer<span class="room-chip-area">42,6 m²</span></div> Flur
<div class="room-chip">Gästezimmer<span class="room-chip-area">11,5</span></div> <span class="room-chip-area">20,1</span>
<div class="room-chip">Badezimmer<span class="room-chip-area">9,8 m²</span></div> </div>
<div class="room-chip">Küche<span class="room-chip-area">18,4 m²</span></div> <div class="room-chip">
<div class="room-chip">Schlafzimmer<span class="room-chip-area">18,0 m²</span></div> Wohnzimmer
<span class="room-chip-area">42,6 m²</span>
</div>
<div class="room-chip">
Gästezimmer
<span class="room-chip-area">11,5 m²</span>
</div>
<div class="room-chip">
Badezimmer
<span class="room-chip-area">9,8 m²</span>
</div>
<div class="room-chip">
Küche
<span class="room-chip-area">18,4 m²</span>
</div>
<div class="room-chip">
Schlafzimmer
<span class="room-chip-area">18,0 m²</span>
</div>
</div> </div>
<div class="floor-plan floor-plan-multi"> <div class="floor-plan floor-plan-multi">
<picture> <img
<source srcset="/bilder/grundrisse/OG 1 2-small.webp" type="image/webp"> src="bilder/grundrisse/OG 1 2-small.jpg"
<img src="/bilder/grundrisse/OG 1 2-small.jpg" alt="Grundriss 1. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 1 2.webp" /> alt="Grundriss 1. Obergeschoss"
</picture> data-img="bilder/grundrisse/OG 1 2.png"
<picture> />
<source srcset="/bilder/grundrisse/OG 1 3D-small.webp" type="image/webp"> <img
<img src="/bilder/grundrisse/OG 1 3D-small.jpg" alt="Grundriss 1. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 1 3D.webp" /> src="bilder/grundrisse/OG 1 3D-small.jpg"
</picture> alt="Grundriss 1. Obergeschoss"
data-img="bilder/grundrisse/OG 1 3D.png"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -274,22 +285,42 @@
</div> </div>
<div class="floor-body" id="floor-body-2" role="region" aria-labelledby="floor-title-2"> <div class="floor-body" id="floor-body-2" role="region" aria-labelledby="floor-title-2">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip">Flur<span class="room-chip-area">13,9 m²</span></div> <div class="room-chip">
<div class="room-chip">Kinderzimmer 1<span class="room-chip-area">21,7 m²</span></div> Flur
<div class="room-chip">Kinderzimmer 2<span class="room-chip-area">15,7</span></div> <span class="room-chip-area">13,9</span>
<div class="room-chip">Spielzimmer<span class="room-chip-area">6,3 m²</span></div> </div>
<div class="room-chip">Ankleidezimmer<span class="room-chip-area">1,4 m²</span></div> <div class="room-chip">
<div class="room-chip">Dachterrasse<span class="room-chip-area">9,0 m²</span> <small>(25% von 35,8 m²)</small></div> Kinderzimmer 1
<span class="room-chip-area">21,7 m²</span>
</div>
<div class="room-chip">
Kinderzimmer 2
<span class="room-chip-area">15,7 m²</span>
</div>
<div class="room-chip">
Spielzimmer
<span class="room-chip-area">6,3 m²</span>
</div>
<div class="room-chip">
Ankleidezimmer
<span class="room-chip-area">1,4 m²</span>
</div>
<div class="room-chip">
Dachterrasse
<span class="room-chip-area">9,0 m²</span> <small>(25% von 35,8 m²)</small>
</div>
</div> </div>
<div class="floor-plan floor-plan-multi"> <div class="floor-plan floor-plan-multi">
<picture> <img
<source srcset="/bilder/grundrisse/OG 2 grundriss-small.webp" type="image/webp"> src="bilder/grundrisse/OG 2 grundriss-small.jpg"
<img src="/bilder/grundrisse/OG 2 grundriss-small.jpg" alt="Grundriss 2. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 2 grundriss.webp" /> alt="Grundriss 2. Obergeschoss (1)"
</picture> data-img="bilder/grundrisse/OG 2 grundriss.png"
<picture> />
<source srcset="/bilder/grundrisse/OG 2 3D-small.webp" type="image/webp"> <img
<img src="/bilder/grundrisse/OG 2 3D-small.jpg" alt="Grundriss 2. Obergeschoss" loading="lazy" data-img="/bilder/grundrisse/OG 2 3D.webp" /> src="bilder/grundrisse/OG 2 3D-small.jpg"
</picture> alt="Grundriss 2. Obergeschoss (1)"
data-img="bilder/grundrisse/OG 2 3D.png"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -303,19 +334,30 @@
</div> </div>
<div class="floor-body" id="floor-body-3" role="region" aria-labelledby="floor-title-3"> <div class="floor-body" id="floor-body-3" role="region" aria-labelledby="floor-title-3">
<div class="floor-rooms-grid"> <div class="floor-rooms-grid">
<div class="room-chip">Dachboden unten (ungeheizt)<span class="room-chip-area">52 m²</span></div> <div class="room-chip">
<div class="room-chip">Dachboden Mitte (ungeheizt)<span class="room-chip-area">31 m²</span></div> Dachboden unten (ungeheizt)
<div class="room-chip">Dachboden oben (ungeheizt)<span class="room-chip-area">11</span></div> <span class="room-chip-area">52</span>
</div>
<div class="room-chip">
Dachboden Mitte (ungeheizt)
<span class="room-chip-area">31 m²</span>
</div>
<div class="room-chip">
Dachboden oben (ungeheizt)
<span class="room-chip-area">11 m²</span>
</div>
</div> </div>
<div class="floor-plan floor-plan-multi"> <div class="floor-plan floor-plan-multi">
<picture> <img
<source srcset="/bilder/grundrisse/Dachboden unten 2-small.webp" type="image/webp"> src="bilder/grundrisse/Dachboden unten 2-small.jpg"
<img src="/bilder/grundrisse/Dachboden unten 2-small.jpg" alt="Grundriss Dachboden" loading="lazy" data-img="/bilder/grundrisse/Dachboden unten 2.webp" /> alt="Grundriss Dachboden"
</picture> data-img="bilder/grundrisse/Dachboden unten 2.png"
<picture> />
<source srcset="/bilder/grundrisse/Dachboden unten-small.webp" type="image/webp"> <img
<img src="/bilder/grundrisse/Dachboden unten-small.jpg" alt="Grundriss Dachboden" loading="lazy" data-img="/bilder/grundrisse/Dachboden unten.webp" /> src="bilder/grundrisse/Dachboden unten-small.jpg"
</picture> alt="Grundriss Dachboden"
data-img="bilder/grundrisse/Dachboden unten.png"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -372,14 +414,18 @@
<div class="lage-icon">🛒</div> <div class="lage-icon">🛒</div>
<div> <div>
<div class="lage-title">Einkaufen & Versorgung</div> <div class="lage-title">Einkaufen & Versorgung</div>
<div class="lage-desc">Supermärkte, Ärzte, Apotheken und Schulen sind fußläufig erreichbar</div> <div class="lage-desc">
Supermärkte, Ärzte, Apotheken und Schulen sind fußläufig erreichbar
</div>
</div> </div>
</div> </div>
<div class="lage-item"> <div class="lage-item">
<div class="lage-icon">🚌</div> <div class="lage-icon">🚌</div>
<div> <div>
<div class="lage-title">Öffentlicher Nahverkehr</div> <div class="lage-title">Öffentlicher Nahverkehr</div>
<div class="lage-desc">Zentrale Bushaltestelle ca. 200 m entfernt — direkte Verbindungen in die Region</div> <div class="lage-desc">
Zentrale Bushaltestelle ca. 200 m entfernt — direkte Verbindungen in die Region
</div>
</div> </div>
</div> </div>
<div class="lage-item"> <div class="lage-item">
@@ -393,14 +439,23 @@
<div class="lage-icon">📍</div> <div class="lage-icon">📍</div>
<div> <div>
<div class="lage-title">Genaue Adresse</div> <div class="lage-title">Genaue Adresse</div>
<div class="lage-desc">Schleusinger Bahnhofstraße 10<br />98553 Schleusingen, Thüringen</div> <div class="lage-desc">
Schleusinger Bahnhofstraße 10
<br />
98533 Schleusingen, Thüringen
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="lage-map-wrapper"> <div class="lage-map-wrapper">
<iframe <iframe
src="https://maps.google.com/maps?q=50.5090045,10.7473859&t=&z=16&ie=UTF8&iwloc=&output=embed" src="https://maps.google.com/maps?q=50.5090045,10.7473859&t=&z=16&ie=UTF8&iwloc=&output=embed"
width="100%" height="450" style="border: 0" allowfullscreen="" loading="lazy" width="100%"
height="450"
style="border: 0"
allowfullscreen=""
loading="lazy"
referrerpolicy="no-referrer-when-downgrade" referrerpolicy="no-referrer-when-downgrade"
title="Standort Bahnhofstraße 10, Schleusingen" title="Standort Bahnhofstraße 10, Schleusingen"
></iframe> ></iframe>
@@ -410,79 +465,72 @@
<section class="contact-section" id="kontakt" aria-label="Kontaktformular"> <section class="contact-section" id="kontakt" aria-label="Kontaktformular">
<div class="contact-inner"> <div class="contact-inner">
<div class="section-eyebrow">Kontakt</div> <div class="section-eyebrow">Kontakt</div>
<h2>Interesse?<br /><em>Schreiben Sie uns.</em></h2> <h2>
Interesse?
<br />
<em>Schreiben Sie uns.</em>
</h2>
<p> <p>
Wir freuen uns über Ihre Anfrage und melden uns innerhalb von 24 Stunden. Wir freuen uns über Ihre Anfrage und melden uns innerhalb von 24 Stunden.
Besichtigungstermine sind nach Absprache möglich. Bitte geben Sie bei Ihrer Anfrage ein Besichtigungstermine sind nach Absprache möglich. Bitte geben Sie bei Ihrer Anfrage ein
paar Terminvorschläge an. paar Terminvorschläge an.
</p> </p>
<div class="contact-form"> <div class="contact-form">
<?php if ($formSuccess): ?> <form id="contactForm">
<div id="form-result" class="form-success" style="display: block">
<p>Vielen Dank für Ihre Anfrage!</p>
<br />
<small>Wir haben Ihre Nachricht erhalten und melden uns innerhalb von 24 Stunden bei Ihnen.</small>
</div>
<?php else: ?>
<?php if (!empty($formErrors)): ?>
<div id="form-errors" class="form-errors">
<ul>
<?php foreach ($formErrors as $error): ?>
<li><?= $escapeContactValue($error) ?></li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<form id="contactForm" method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token'] ?? '') ?>" />
<div class="form-row"> <div class="form-row">
<div class="form-field"> <div class="form-field">
<label for="fname">Vorname</label> <label for="fname">Vorname</label>
<input type="text" id="fname" name="fname" placeholder="Max" required value="<?= $escapeContactValue($formData['fname']) ?>" /> <input type="text" id="fname" name="fname" placeholder="Max" required />
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="lname">Nachname</label> <label for="lname">Nachname</label>
<input type="text" id="lname" name="lname" placeholder="Mustermann" required value="<?= $escapeContactValue($formData['lname']) ?>" /> <input type="text" id="lname" name="lname" placeholder="Mustermann" required />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-field"> <div class="form-field">
<label for="email">E-Mail</label> <label for="email">E-Mail</label>
<input type="email" id="email" name="email" placeholder="max@beispiel.de" required value="<?= $escapeContactValue($formData['email']) ?>" /> <input
type="email"
id="email"
name="email"
placeholder="max@beispiel.de"
required
/>
</div> </div>
<div class="form-field"> <div class="form-field">
<label for="phone">Telefon</label> <label for="phone">Telefon</label>
<input type="tel" id="phone" name="phone" placeholder="+49 ..." value="<?= $escapeContactValue($formData['phone']) ?>" /> <input type="tel" id="phone" name="phone" placeholder="+49 ..." />
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-field full"> <div class="form-field full">
<label for="interest">Anliegen</label> <label for="interest">Anliegen</label>
<select id="interest" name="interest"> <select id="interest" name="interest">
<?php <option>Besichtigung anfragen</option>
$interestOptions = ['Besichtigung anfragen', 'Allgemeine Informationen', 'Mietbewerbung einreichen']; <option>Allgemeine Informationen</option>
foreach ($interestOptions as $opt): <option>Mietbewerbung einreichen</option>
$selected = ($formData['interest'] === $opt) ? ' selected' : '';
?>
<option<?= $selected ?>><?= $escapeContactValue($opt) ?></option>
<?php endforeach; ?>
</select> </select>
</div> </div>
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-field full"> <div class="form-field full">
<label for="message">Nachricht</label> <label for="message">Nachricht</label>
<textarea id="message" name="message" rows="4" placeholder="Ihre Nachricht ..." required><?= $escapeContactValue($formData['message']) ?></textarea> <textarea
id="message"
name="message"
rows="4"
placeholder="Ihre Nachricht ..."
></textarea>
</div> </div>
</div> </div>
<div class="hp-field" aria-hidden="true">
<label for="website">Website</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off" />
</div>
<input type="hidden" name="form_time" value="<?= time() ?>" />
<button type="submit" class="btn-submit">Anfrage absenden</button> <button type="submit" class="btn-submit">Anfrage absenden</button>
</form> </form>
<?php endif; ?> <div class="form-success" id="formSuccess">
<p>Vielen Dank für Ihre Anfrage!</p>
<br />
<small>Ihr E-Mail-Programm wurde geöffnet. Bitte senden Sie die E-Mail ab, damit Ihre Anfrage bei uns eingeht.</small>
</div>
</div> </div>
<div class="contact-details"> <div class="contact-details">
<p>Oder schreiben Sie uns direkt: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></p> <p>Oder schreiben Sie uns direkt: <a href="mailto:mki@kies-media.de">mki@kies-media.de</a></p>
@@ -494,8 +542,8 @@ foreach ($interestOptions as $opt):
<footer role="contentinfo"> <footer role="contentinfo">
<div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div> <div class="footer-logo">Bahnhofstraße 10 · Schleusingen</div>
<div class="footer-links"> <div class="footer-links">
<a href="/impressum">Impressum</a> <a href="/impressum" target="_blank">Impressum</a>
<a href="/datenschutz">Datenschutz</a> <a href="#">Datenschutz</a>
</div> </div>
</footer> </footer>
@@ -503,3 +551,7 @@ foreach ($interestOptions as $opt):
<button class="lightbox-close" id="lightboxClose" aria-label="Bildansicht schließen">&times;</button> <button class="lightbox-close" id="lightboxClose" aria-label="Bildansicht schließen">&times;</button>
<img src="" id="lightboxImg" alt="" /> <img src="" id="lightboxImg" alt="" />
</div> </div>
<script src="js/haus-schleusingen.js"></script>
</body>
</html>

169
js/haus-schleusingen.js Normal file
View File

@@ -0,0 +1,169 @@
$(function () {
// Navbar scroll
$(window).on("scroll", function () {
if ($(this).scrollTop() > 60) $("#navbar").addClass("scrolled");
else $("#navbar").removeClass("scrolled");
});
// Hero animation on load
setTimeout(function () {
$("#heroContent").addClass("visible");
$("#heroBg").addClass("loaded");
}, 200);
// Scroll animations
function checkVisible() {
$(".fact, [data-animate]").each(function () {
var el = $(this);
var top = el.offset().top;
var windowBottom = $(window).scrollTop() + $(window).height();
if (windowBottom > top + 60) {
el.addClass("visible");
el.css({ opacity: 1, transform: "translateY(0)" });
}
});
}
$("[data-animate]").css({
opacity: 0,
transform: "translateY(30px)",
transition: "opacity 0.8s ease, transform 0.8s ease",
});
$(window).on("scroll", checkVisible);
checkVisible();
// Floor accordion
$(".floor-header").on("click", function () {
var item = $(this).closest(".floor-item");
var isOpen = item.hasClass("open");
$(".floor-item").removeClass("open");
$(".floor-header").attr("aria-expanded", "false");
$(".floor-body").slideUp(300);
if (!isOpen) {
item.addClass("open");
$(this).attr("aria-expanded", "true");
item.find(".floor-body").slideDown(300);
}
});
// Accordion keyboard handler (Enter/Space)
$(".floor-header").on("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
$(this).trigger("click");
}
});
// Lightbox track last focused element for focus return
var lightboxTrigger = null;
function openLightbox(src) {
lightboxTrigger = document.activeElement;
$("#lightboxImg").attr("src", src).attr("alt", "");
$("#lightbox").addClass("open");
$("body").css("overflow", "hidden");
// Set focus to close button
setTimeout(function () {
$("#lightboxClose").focus();
}, 50);
}
function closeLightbox() {
$("#lightbox").removeClass("open");
$("body").css("overflow", "");
// Return focus to trigger
if (lightboxTrigger) {
$(lightboxTrigger).focus();
lightboxTrigger = null;
}
}
// Lightbox gallery grid items
$(document).on("click", ".grid-item", function () {
var src = $(this).data("img") || $(this).find("img").attr("src");
openLightbox(src);
});
// Lightbox floor plan images in Raumaufteilung
$(document).on("click", ".floor-plan img[data-img]", function () {
var src = $(this).data("img");
openLightbox(src);
});
// Lightbox close handlers
$("#lightboxClose").on("click", function () {
closeLightbox();
});
$("#lightbox").on("click", function (e) {
if (e.target === this) {
closeLightbox();
}
});
// Escape key to close lightbox
$(document).on("keydown", function (e) {
if (e.key === "Escape" && $("#lightbox").hasClass("open")) {
closeLightbox();
}
});
// Focus trap for lightbox
$("#lightbox").on("keydown", function (e) {
if (e.key !== "Tab") return;
var focusable = $(this).find("button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])").filter(":visible");
if (focusable.length === 0) return;
var first = focusable[0];
var last = focusable[focusable.length - 1];
if (e.shiftKey) {
if (document.activeElement === first) {
e.preventDefault();
last.focus();
}
} else {
if (document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
});
// Gallery keyboard handler (Enter/Space)
$(document).on("keydown", ".grid-item", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
$(this).trigger("click");
}
});
// Form submit opens email client with pre-filled mailto: link
$("#contactForm").on("submit", function (e) {
e.preventDefault();
var fname = $("#fname").val().trim();
var lname = $("#lname").val().trim();
var email = $("#email").val().trim();
var phone = $("#phone").val().trim();
var interest = $("#interest").val();
var message = $("#message").val().trim();
var subject = "Kontaktanfrage: " + interest;
var body = "Von: " + fname + " " + lname + "\n";
body += "E-Mail: " + email + "\n";
if (phone) body += "Telefon: " + phone + "\n";
body += "Anliegen: " + interest + "\n\n";
body += message;
var mailto =
"mailto:mki@kies-media.de" +
"?subject=" + encodeURIComponent(subject) +
"&body=" + encodeURIComponent(body);
window.location.href = mailto;
// Show success message
$("#contactForm").hide();
$("#formSuccess").fadeIn(400);
});
});

9
js/masonry.pkgd.min.js vendored Normal file

File diff suppressed because one or more lines are too long

17
nginx.conf Executable file → Normal file
View File

@@ -3,22 +3,9 @@ server {
server_name localhost; server_name localhost;
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.php; index haus-schleusingen.html;
# Gzip aktivieren
gzip on;
gzip_types text/css application/javascript image/svg+xml application/json text/xml;
gzip_min_length 256;
gzip_vary on;
location / { location / {
try_files $uri $uri/ /index.php; try_files $uri $uri/ /haus-schleusingen.html;
}
# Lange Cache-Dauer für Bilder und statische Assets
location ~* \.(jpg|jpeg|png|webp|gif|ico|svg|css|js|woff2?)$ {
expires 30d;
add_header Cache-Control "public, immutable";
access_log off;
} }
} }

8
package.json Executable file → Normal file
View File

@@ -7,16 +7,12 @@
"lint:html": "htmlhint \"**/*.html\"", "lint:html": "htmlhint \"**/*.html\"",
"lint:css": "stylelint \"css/**/*.css\" \"fonts/**/*.css\" && echo Stylelint: No errors found", "lint:css": "stylelint \"css/**/*.css\" \"fonts/**/*.css\" && echo Stylelint: No errors found",
"lint:js": "eslint \"js/**/*.js\" --ignore-pattern \"**/*.min.js\" && echo ESLint: No errors found", "lint:js": "eslint \"js/**/*.js\" --ignore-pattern \"**/*.min.js\" && echo ESLint: No errors found",
"lint": "npm run lint:php && npm run lint:html && npm run lint:css && npm run lint:js", "lint": "npm run lint:html && npm run lint:css && npm run lint:js",
"format": "prettier --write \"**/*.{html,css,js,json,md}\" --ignore-path .prettierignore", "format": "prettier --write \"**/*.{html,css,js,json,md}\" --ignore-path .prettierignore",
"format:check": "prettier --check \"**/*.{html,css,js,json,md}\" --ignore-path .prettierignore", "format:check": "prettier --check \"**/*.{html,css,js,json,md}\" --ignore-path .prettierignore",
"prepare": "husky", "prepare": "husky"
"lint:php": "find . -name \"*.php\" -not -path \"./vendor/*\" -exec php -l {} \\; > /dev/null 2>&1 && echo \"PHP syntax OK\" || (echo \"PHP lint failed\" && exit 1)"
}, },
"lint-staged": { "lint-staged": {
"*.{php}": [
"scripts/lint-php.sh"
],
"*.{html}": [ "*.{html}": [
"htmlhint", "htmlhint",
"prettier --write" "prettier --write"

0
docs/page-preview.png → page-preview.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 1.5 MiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
failOnWarning="true"
failOnRisky="true"
failOnEmptyTestSuite="true"
beStrictAboutOutputDuringTests="true"
>
<testsuites>
<testsuite name="Unit">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
</phpunit>

View File

@@ -1,23 +0,0 @@
# Enable rewrite engine
RewriteEngine On
# Security Headers
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set X-Frame-Options "SAMEORIGIN"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-src https://www.google.com/ https://www.google.de/; connect-src 'self'"
</IfModule>
# Legacy redirects (301) must be before the catch-all
RewriteRule ^impressum\.html$ /impressum [R=301,L]
RewriteRule ^datenschutz\.html$ /datenschutz [R=301,L]
RewriteRule ^haus-schleusingen\.html$ / [R=301,L]
# Serve existing files/directories directly (css, js, images, fonts, etc.)
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# Route everything else through the front controller
RewriteRule ^(.*)$ index.php [QSA,L]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Some files were not shown because too many files have changed in this diff Show More