Eigene Icon-Sets unter public/icons/{pressekonto,presseecho,businessportal24}
(SVG-Favicon + Touch-/App-Icons); ThemeHelper mappt das aktive Theme auf den
Icon-Ordner (Portal/Admin nutzt den pressekonto-Satz), alle Layout-Heads
binden Favicon + apple-touch-icon domain-abhängig ein.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
167 lines
6.4 KiB
PHP
167 lines
6.4 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||
|
||
@php
|
||
$siteName = $domainName ?? config('app.name', 'Laravel');
|
||
// Inline-@section('title', …)-Werte kommen von Blade bereits escaped an —
|
||
// einmal dekodieren, damit {{ }} unten nicht doppelt escapt („&amp;").
|
||
$pageTitle = html_entity_decode(trim($__env->yieldContent('title')), ENT_QUOTES, 'UTF-8') ?: $siteName;
|
||
$pageDescription = html_entity_decode(trim($__env->yieldContent('meta_description')), ENT_QUOTES, 'UTF-8') ?: $siteName;
|
||
// Self-Canonical als Default aus der Portal-URL + Request-Pfad (ohne Query):
|
||
// pro Portal/Domain getrennt und unabhängig von URL::forceRootUrl – verhindert Cross-Portal-Duplicate.
|
||
$portalBase = rtrim($domainUrl ?? config('app.url'), '/');
|
||
$canonicalUrl = trim($__env->yieldContent('canonical')) ?: ($portalBase.request()->getPathInfo());
|
||
$ogType = trim($__env->yieldContent('og_type')) ?: 'website';
|
||
$ogImage = trim($__env->yieldContent('og_image'));
|
||
@endphp
|
||
|
||
<meta name="description" content="{{ $pageDescription }}">
|
||
|
||
<title>{{ $pageTitle }}</title>
|
||
|
||
@php
|
||
$robotsDirective = trim($__env->yieldContent('robots'));
|
||
@endphp
|
||
|
||
{{-- Robots-Anweisung (z. B. noindex für private Vorschauen) --}}
|
||
@if ($robotsDirective !== '')
|
||
<meta name="robots" content="{{ $robotsDirective }}">
|
||
@endif
|
||
|
||
{{-- Kanonische URL (entfällt bei noindex — es gibt keine öffentliche URL) --}}
|
||
@if (! str_contains($robotsDirective, 'noindex'))
|
||
<link rel="canonical" href="{{ $canonicalUrl }}">
|
||
@endif
|
||
|
||
{{-- Sprach-Alternates (de/en) – pro Seite optional gesetzt --}}
|
||
@hasSection('hreflang')
|
||
@yield('hreflang')
|
||
@endif
|
||
|
||
{{-- OpenGraph / Social --}}
|
||
<meta property="og:type" content="{{ $ogType }}">
|
||
<meta property="og:site_name" content="{{ $siteName }}">
|
||
<meta property="og:title" content="{{ $pageTitle }}">
|
||
<meta property="og:description" content="{{ $pageDescription }}">
|
||
<meta property="og:url" content="{{ $canonicalUrl }}">
|
||
<meta property="og:locale" content="{{ app()->getLocale() === 'de' ? 'de_DE' : 'en_US' }}">
|
||
@if ($ogImage)
|
||
<meta property="og:image" content="{{ $ogImage }}">
|
||
@endif
|
||
<meta name="twitter:card" content="{{ $ogImage ? 'summary_large_image' : 'summary' }}">
|
||
<meta name="twitter:title" content="{{ $pageTitle }}">
|
||
<meta name="twitter:description" content="{{ $pageDescription }}">
|
||
@if ($ogImage)
|
||
<meta name="twitter:image" content="{{ $ogImage }}">
|
||
@endif
|
||
|
||
{{-- Seitenspezifische Zusatz-Metas (z. B. article:* auf PM-Detailseiten) --}}
|
||
@yield('meta_extra')
|
||
|
||
{{-- Strukturierte Daten (JSON-LD) – pro Seite optional gesetzt --}}
|
||
@yield('structured_data')
|
||
|
||
<!-- Domain-spezifisches Favicon -->
|
||
<link rel="icon" href="{{ asset(\App\Helpers\ThemeHelper::getFaviconPath()) }}" type="image/svg+xml">
|
||
<link rel="apple-touch-icon" href="{{ asset(\App\Helpers\ThemeHelper::getAppIconPath()) }}">
|
||
|
||
<!-- Fonts -->
|
||
@include('partials.local-fonts')
|
||
|
||
@php
|
||
$font = \App\Helpers\ThemeHelper::getFont();
|
||
$theme = config('app.theme', 'businessportal24');
|
||
@endphp
|
||
|
||
{{-- app.js bündelt Livewire + Alpine (livewire.esm) — @livewireScriptConfig
|
||
unten verhindert, dass Livewire zusätzlich sein eigenes Script injiziert
|
||
(sonst zwei Alpine-Instanzen und kaputte wire:model-Bindings). --}}
|
||
@livewireStyles
|
||
@vite([\App\Helpers\ThemeHelper::getThemeCssPath(), 'resources/js/app.js'], $domainConfig['assets_dir'] ?? 'build/web')
|
||
|
||
<!-- Sticky Header Script -->
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
const topbar = document.getElementById('topbar');
|
||
const header = document.getElementById('header');
|
||
|
||
if (!topbar || !header) return;
|
||
|
||
let topbarHeight = topbar.offsetHeight;
|
||
let isHeaderSticky = false;
|
||
|
||
// Platzhalter hält im Fluss die Header-Höhe frei, sobald der
|
||
// Header per position:fixed aus dem Fluss genommen wird —
|
||
// ohne den Spacer würde der Inhalt beim Fixieren nach oben springen.
|
||
const spacer = document.createElement('div');
|
||
spacer.setAttribute('aria-hidden', 'true');
|
||
|
||
function makeSticky() {
|
||
spacer.style.height = header.offsetHeight + 'px';
|
||
header.parentNode.insertBefore(spacer, header);
|
||
header.classList.remove('header-normal');
|
||
header.classList.add('header-sticky');
|
||
isHeaderSticky = true;
|
||
}
|
||
|
||
function clearSticky() {
|
||
header.classList.remove('header-sticky');
|
||
header.classList.add('header-normal');
|
||
if (spacer.parentNode) {
|
||
spacer.parentNode.removeChild(spacer);
|
||
}
|
||
isHeaderSticky = false;
|
||
}
|
||
|
||
function updateHeaderPosition() {
|
||
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
||
|
||
if (scrollTop >= topbarHeight && !isHeaderSticky) {
|
||
makeSticky();
|
||
} else if (scrollTop < topbarHeight && isHeaderSticky) {
|
||
clearSticky();
|
||
}
|
||
}
|
||
|
||
updateHeaderPosition();
|
||
|
||
window.addEventListener('scroll', updateHeaderPosition, { passive: true });
|
||
|
||
window.addEventListener('resize', function() {
|
||
topbarHeight = topbar.offsetHeight;
|
||
if (isHeaderSticky) {
|
||
spacer.style.height = header.offsetHeight + 'px';
|
||
}
|
||
updateHeaderPosition();
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<!-- Additional Styles -->
|
||
@stack('styles')
|
||
|
||
<x-web.umami />
|
||
|
||
</head>
|
||
|
||
<body class="antialiased" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||
|
||
<!-- TopBar - statisch oben -->
|
||
|
||
@yield('content')
|
||
|
||
{{-- Schwebendes Kontakt-Widget (unten rechts, alle Web-Domains) --}}
|
||
<x-web.contact-launcher />
|
||
|
||
<!-- Additional Scripts -->
|
||
@stack('scripts')
|
||
|
||
@livewireScriptConfig
|
||
</body>
|
||
|
||
</html>
|