presseportale/resources/views/web/layouts/web-master.blade.php
Kevin Adametz 9d5e5a0b54 web: RSS-Feeds je Branche und je Portal (Roadmap 3.1); dev: Sichtung des Ordners, Protokoll-Teile umnummeriert
- /{edition}/feed und /{edition}/category/{slug}/feed (FeedController,
  RSS 2.0 mit atom:link self, dc:creator, category; jüngste 50 der Ausgabe,
  Branche inkl. aktiver Unterkategorien, nur visibleInArchive).
- Cache über WebPageCache (Key-Gruppe feed), Hub leitet per 301 aufs Portal
  (canonical.portal_only_content_routes).
- Autodiscovery über @yield('feed_links') auf Start-, Branchen- und
  Archivseiten, sichtbarer Link in der Archiv-Leiste, llms.txt nennt die
  Feed-Muster, Block „RSS-Feeds" im Panel „Auslieferung"
  (VisibilityStats::feedLinks).
- Übersetzungen unter web.rss (web.feed ist die Startseiten-Liste).
- Tests: FeedTest (7), VisibilityStatsTest erweitert.
- dev/2026-09-01: Sichtung 18.09. (Teil 11), zweite Teil-Serie des
  Protokolls jetzt 7–10 mit nachgezogenen Verweisen, Anhang 1 abgeglichen,
  E6 abgehakt, 2.6 extern, 1c.5/E8 wartet auf Daten, 3.1 abgehakt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 13:57:09 +00:00

175 lines
6.9 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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;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 = \App\Support\StaticPageCanonical::selfBase($domainUrl ?? null, ($webPortalContextApplied ?? false) === true);
// Statische Seiten liegen auf Hub UND beiden Portalen mit identischem
// Text (Anhang 2, E10): dort führt ein Host, die anderen verweisen
// dorthin. Rechtstexte sind ausgenommen — siehe config/canonical.php.
$canonicalUrl = trim($__env->yieldContent('canonical'))
?: (\App\Support\StaticPageCanonical::urlFor(request()->route()?->getName(), request()->getPathInfo())
?: $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
{{-- RSS-Autodiscovery (Roadmap 3.1) – Portal-Feed und, auf Branchenseiten, der Feed der Branche --}}
@yield('feed_links')
{{-- 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>