fix(i18n): map gallery + hero + floorplan images to real filenames; force-DE on legal pages

- home: map 9/12 gallery items to real filenames (Wohnzimmer-1 -> wohnzimmer2.png,
  Badezimmer-1 -> Bad.jpg, etc.); remove 3 items whose source images are missing
- home: hero-bg.jpg -> Außenansicht-2.webp (file exists)
- home: floorplan image -> /bilder/grundrisse/<name>.png (subdir + correct name)
- layout: og:image fallback Aussenansicht-2.webp (ASCII) -> Außenansicht-2.png (UTF-8)
- layout: hero-bg.jpg fallback -> Außenansicht-2.png (UTF-8)
- Controller::render(): add $forceLocale param for legal pages
- ImpressumController / DatenschutzController: force 'de' (TMG §5 / GDPR)
  so <html lang=de> is emitted regardless of cookie
This commit is contained in:
Hermes
2026-06-04 16:13:54 +00:00
parent d9b4c71735
commit 4bc035b783
5 changed files with 50 additions and 38 deletions

View File

@@ -25,10 +25,13 @@ abstract class Controller
* Render a view inside a layout. * Render a view inside a layout.
* *
* @param array<string,mixed> $data * @param array<string,mixed> $data
* @param string|null $forceLocale If set, overrides the locale resolved from
* cookie/Accept-Language for this render. Used by legal pages (Impressum,
* Datenschutz) that must be served in German only by German law.
*/ */
protected function render(string $view, array $data = [], string $layout = 'main'): void protected function render(string $view, array $data = [], string $layout = 'main', ?string $forceLocale = null): void
{ {
$locale = LocaleController::current(); $locale = $forceLocale ?? LocaleController::current();
$i18n = static fn (string $key, array $params = []): string => I18n::t($key, $params, $locale); $i18n = static fn (string $key, array $params = []): string => I18n::t($key, $params, $locale);
$globals = [ $globals = [

View File

@@ -11,17 +11,18 @@ class DatenschutzController extends Controller
{ {
public function index(): void public function index(): void
{ {
$locale = LocaleController::current(); // Legal pages (Datenschutzerklärung) must be served in German only by GDPR / German law.
// Force German locale for render() so <html lang="de"> + German meta are emitted
// regardless of cookie/Accept-Language.
$this->render('datenschutz/index', [ $this->render('datenschutz/index', [
'pageTitle' => I18n::t('legal.privacy_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale), 'pageTitle' => I18n::t('legal.privacy_h1', [], 'de') . ' ' . I18n::t('site.title', [], 'de'),
'pageDescription' => I18n::t('legal.privacy_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale), 'pageDescription' => I18n::t('legal.privacy_h1', [], 'de') . ' ' . I18n::t('site.title', [], 'de'),
'robots' => 'noindex', 'robots' => 'noindex',
'canonical' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz', 'canonical' => I18n::t('site.canonical_base', [], 'de') . '/datenschutz',
'ogLocale' => Locale::toOgLocale($locale), 'ogLocale' => Locale::toOgLocale('de'),
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/datenschutz', 'ogUrl' => I18n::t('site.canonical_base', [], 'de') . '/datenschutz',
'ogTitle' => I18n::t('legal.privacy_h1', [], $locale), 'ogTitle' => I18n::t('legal.privacy_h1', [], 'de'),
'ogDescription' => I18n::t('legal.privacy_h1', [], $locale), 'ogDescription' => I18n::t('legal.privacy_h1', [], 'de'),
]); ], 'main', 'de');
} }
} }

View File

@@ -11,17 +11,18 @@ class ImpressumController extends Controller
{ {
public function index(): void public function index(): void
{ {
$locale = LocaleController::current(); // Legal pages (Impressum) must be served in German only by German law (TMG §5).
// Force German locale for render() so <html lang="de"> + German meta are emitted
// regardless of cookie/Accept-Language.
$this->render('impressum/index', [ $this->render('impressum/index', [
'pageTitle' => I18n::t('legal.imprint_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale), 'pageTitle' => I18n::t('legal.imprint_h1', [], 'de') . ' ' . I18n::t('site.title', [], 'de'),
'pageDescription' => I18n::t('legal.imprint_h1', [], $locale) . ' ' . I18n::t('site.title', [], $locale), 'pageDescription' => I18n::t('legal.imprint_h1', [], 'de') . ' ' . I18n::t('site.title', [], 'de'),
'robots' => 'noindex', 'robots' => 'noindex',
'canonical' => I18n::t('site.canonical_base', [], $locale) . '/impressum', 'canonical' => I18n::t('site.canonical_base', [], 'de') . '/impressum',
'ogLocale' => Locale::toOgLocale($locale), 'ogLocale' => Locale::toOgLocale('de'),
'ogUrl' => I18n::t('site.canonical_base', [], $locale) . '/impressum', 'ogUrl' => I18n::t('site.canonical_base', [], 'de') . '/impressum',
'ogTitle' => I18n::t('legal.imprint_h1', [], $locale), 'ogTitle' => I18n::t('legal.imprint_h1', [], 'de'),
'ogDescription' => I18n::t('legal.imprint_h1', [], $locale), 'ogDescription' => I18n::t('legal.imprint_h1', [], 'de'),
]); ], 'main', 'de');
} }
} }

View File

@@ -15,22 +15,22 @@ declare(strict_types=1);
*/ */
$gridItems = [ $gridItems = [
['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.exterior', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-2'], // NOTE: image filenames reflect the actual files in public/bilder/ on the server.
['img' => 'bilder/Wohnzimmer-1.png', 'key' => 'gallery.living', 'alt' => 'gallery.alt.living', 'class' => 'span-2 row-1'], // 3 items were removed (gästezimmer / wohnbereich / wohnbereich-detail)
['img' => 'bilder/Küche-1.png', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''], // because no matching files exist in the image inventory.
['img' => 'bilder/Schlafzimmer-1.png','key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''], ['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.exterior', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-2'],
['img' => 'bilder/Badezimmer-1.png', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''], ['img' => 'bilder/wohnzimmer2.png', 'key' => 'gallery.living', 'alt' => 'gallery.alt.living', 'class' => 'span-2 row-1'],
['img' => 'bilder/Kinderzimmer-1-1.png', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''], ['img' => 'bilder/Küche 1.jpg', 'key' => 'gallery.kitchen', 'alt' => 'gallery.alt.kitchen', 'class' => ''],
['img' => 'bilder/Kinderzimmer-2.png','key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''], ['img' => 'bilder/schlafzimmer.png', 'key' => 'gallery.bedroom', 'alt' => 'gallery.alt.bedroom', 'class' => ''],
['img' => 'bilder/Kinderzimmer-Detail.png','key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'], ['img' => 'bilder/Bad.jpg', 'key' => 'gallery.bath', 'alt' => 'gallery.alt.bath', 'class' => ''],
['img' => 'bilder/Gästezimmer.png', 'key' => 'gallery.guest', 'alt' => 'gallery.alt.guest', 'class' => ''], ['img' => 'bilder/Kinderzimmer 2.jpg', 'key' => 'gallery.kid1', 'alt' => 'gallery.alt.kid1', 'class' => ''],
['img' => 'bilder/Wohnbereich.png', 'key' => 'gallery.area1', 'alt' => 'gallery.alt.living', 'class' => ''], ['img' => 'bilder/Kinderzimmer 3.jpg', 'key' => 'gallery.kid2', 'alt' => 'gallery.alt.kid2', 'class' => ''],
['img' => 'bilder/Wohnbereich-Detail.png', 'key' => 'gallery.area2', 'alt' => 'gallery.alt.living', 'class' => ''], ['img' => 'bilder/kinderzimmer 2 2.webp', 'key' => 'gallery.kid_detail', 'alt' => 'gallery.alt.kid_detail', 'class' => 'span-2 row-1'],
['img' => 'bilder/Außenansicht-1.png','key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'], ['img' => 'bilder/Außenansicht-2.png', 'key' => 'gallery.area3', 'alt' => 'gallery.alt.exterior', 'class' => 'span-2 row-1'],
]; ];
?> ?>
<header class="hero" id="hero"> <header class="hero" id="hero">
<img src="/bilder/hero-bg.jpg" alt="" class="hero-bg" id="heroBg" loading="eager" decoding="async" fetchpriority="high"> <img src="/bilder/Außenansicht-2.webp" alt="" class="hero-bg" id="heroBg" loading="eager" decoding="async" fetchpriority="high">
<div class="hero-content" id="heroContent"> <div class="hero-content" id="heroContent">
<span class="hero-tag"><?= htmlspecialchars($t('hero.tag'), ENT_QUOTES) ?></span> <span class="hero-tag"><?= htmlspecialchars($t('hero.tag'), ENT_QUOTES) ?></span>
<h1 class="hero-h1"> <h1 class="hero-h1">
@@ -105,6 +105,13 @@ $gridItems = [
</div> </div>
<?php <?php
$floorImageMap = [
'eg' => 'bilder/grundrisse/EG.png',
'og1' => 'bilder/grundrisse/OG 1 2.png',
'og2' => 'bilder/grundrisse/OG 2 grundriss.png',
'attic' => 'bilder/grundrisse/Dachboden unten.png',
];
$floors = [ $floors = [
['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg', ['id' => 'eg', 'titleKey' => 'floors.eg.title', 'areaKey' => 'floors.eg.area', 'altKey' => 'floors.alt.eg',
'rooms' => [ 'rooms' => [
@@ -148,7 +155,7 @@ $gridItems = [
<span class="floor-area"><?= htmlspecialchars($t($floor['areaKey']), ENT_QUOTES) ?></span> <span class="floor-area"><?= htmlspecialchars($t($floor['areaKey']), ENT_QUOTES) ?></span>
</summary> </summary>
<div class="floor-body"> <div class="floor-body">
<img src="/bilder/grundriss-<?= htmlspecialchars($floor['id'], ENT_QUOTES) ?>.png" <img src="/<?= htmlspecialchars($floorImageMap[$floor['id']] ?? 'bilder/grundrisse/EG.png', ENT_QUOTES) ?>"
alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>" alt="<?= htmlspecialchars($t($floor['altKey']), ENT_QUOTES) ?>"
loading="lazy" decoding="async" loading="lazy" decoding="async"
class="floor-plan-img"> class="floor-plan-img">

View File

@@ -28,7 +28,7 @@ $canonical = $canonical ?? $canonicalBase . ($currentPath === '/' ? '
$siteName = I18n::t('site.name', [], $locale); $siteName = I18n::t('site.name', [], $locale);
$ogTitle = $openGraph['ogTitle'] ?? $title; $ogTitle = $openGraph['ogTitle'] ?? $title;
$ogDescription = $openGraph['ogDescription'] ?? $description; $ogDescription = $openGraph['ogDescription'] ?? $description;
$ogImage = $openGraph['ogImage'] ?? 'https://haus-schleusingen.de/bilder/Aussenansicht-2.webp'; $ogImage = $openGraph['ogImage'] ?? 'https://haus-schleusingen.de/bilder/Außenansicht-2.png';
$ogUrl = $openGraph['ogUrl'] ?? $canonical; $ogUrl = $openGraph['ogUrl'] ?? $canonical;
$hreflangs = Locale::hreflangAlternates($currentPath === '/' ? '/' : $currentPath, $canonicalBase); $hreflangs = Locale::hreflangAlternates($currentPath === '/' ? '/' : $currentPath, $canonicalBase);
@@ -72,7 +72,7 @@ $navItems = [
<meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? $pageDescription ?? $t('site.description'), ENT_QUOTES) ?>"> <meta property="og:description" content="<?= htmlspecialchars($ogDescription ?? $pageDescription ?? $t('site.description'), ENT_QUOTES) ?>">
<meta property="og:locale" content="<?= htmlspecialchars($ogLocale ?? Locale::toOgLocale($locale), ENT_QUOTES) ?>"> <meta property="og:locale" content="<?= htmlspecialchars($ogLocale ?? Locale::toOgLocale($locale), ENT_QUOTES) ?>">
<meta property="og:site_name" content="<?= htmlspecialchars($ogSiteName ?? $t('site.name'), ENT_QUOTES) ?>"> <meta property="og:site_name" content="<?= htmlspecialchars($ogSiteName ?? $t('site.name'), ENT_QUOTES) ?>">
<meta property="og:image" content="<?= htmlspecialchars($ogImage ?? ($t('site.canonical_base') . '/bilder/hero-bg.jpg'), ENT_QUOTES) ?>"> <meta property="og:image" content="<?= htmlspecialchars($ogImage ?? ($t('site.canonical_base') . '/bilder/Außenansicht-2.png'), ENT_QUOTES) ?>">
<?php if (isset($structuredData)): ?> <?php if (isset($structuredData)): ?>
<script type="application/ld+json"><?= $structuredData ?></script> <script type="application/ld+json"><?= $structuredData ?></script>