presseportale/resources/views/web/newsroom.blade.php
Kevin Adametz b60a28dc9c web: Entitaeten im JSON-LD verknuepft, News-Sitemap und llms.txt (Roadmap 2.1, 2.2, 2.7)
- App\Support\StructuredData: Portal-Organization als Publisher mit @id
  und Logo (ImageObject), Newsroom-@id; NewsArticle.author auf der
  Meldung und Organization auf der Newsroom-Seite beschreiben jetzt
  dieselbe Entitaet. Startseiten nutzen denselben Publisher.
- /sitemap-news.xml je Spoke (NewsSitemapController): im Archiv
  sichtbare Meldungen der letzten 48 h, max. 1.000, news:publication
  mit Portalname und Sprache; 15 min gecacht, mit den anderen Sitemaps
  geleert; in robots.txt der Spokes. Hub liefert leere Liste.
- /llms.txt je Host (LlmsTxtController): sachliche Beschreibung,
  URL-Aufbau, aktive Kategorien, Seiten fuer Unternehmen, Rechtliches;
  Hub verweist auf beide Portale. Keine Aussagen ueber KI-Nennungen.
- Protokoll Teil 5 (Block 2) inkl. Betreiber-Schritten fuer das
  Publisher Center und Befund zu 2.5/E5; Roadmap 2.1/2.7 abgehakt.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 10:15:28 +00:00

258 lines
16 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.

@extends('web.layouts.web-master')
@php
use App\Support\StructuredData;
use Illuminate\Support\Str;
\Carbon\Carbon::setLocale(app()->getLocale());
$brandConfig = config('domains.domains.'.config('app.theme').'.brand', []);
$brandName = $brandConfig['name'] ?? 'businessportal';
$brandAccent = $brandConfig['accent'] ?? '24';
$fmt = fn ($value) => number_format((int) $value, 0, ',', '.');
$palette = ['#0098A6', '#21A038', '#0018A8', '#1E1E1E', '#E20074', '#003781', '#C84A1E', '#5A6E2F'];
$logoUrl = $company->logoUrl();
$initial = mb_strtoupper(mb_substr((string) $company->name, 0, 1));
$color = $palette[crc32((string) $company->id) % count($palette)];
$boilerplate = filled($company->boilerplate)
? trim((string) Str::of(strip_tags((string) $company->boilerplate))->squish())
: null;
$websiteHost = filled($company->website) ? preg_replace('#^https?://(www\.)?#', '', rtrim((string) $company->website, '/')) : null;
$leadImage = $leadRelease?->images->first();
$leadImageUrl = $leadImage?->variantUrl('detail') ?? $leadImage?->variantUrl('card') ?? $leadImage?->url();
$leadPlaceholderUrl = $leadImageUrl ? null : $leadRelease?->chosenPlaceholderUrl();
$leadCatName = $leadRelease?->localizedCategoryName();
$leadLead = $leadRelease
? (filled($leadRelease->subtitle) ? $leadRelease->subtitle : $leadRelease->teaserText(220))
: null;
$leadReadTime = $leadRelease?->readTimeMinutes() ?? 1;
// SEO: Der Newsroom ist die kanonische Firmenseite im Portal —
// Organization-Markup verknüpft PMs (author) mit diesem Profil.
// Portal-Basis wie beim Canonical im Layout (unabhängig von forceRootUrl).
$newsroomUrl = StructuredData::newsroomUrl(StructuredData::portalBase($domainUrl ?? null), $company->slug);
$jsonLd = [
'@context' => 'https://schema.org',
'@type' => 'Organization',
'@id' => StructuredData::newsroomId($newsroomUrl),
'name' => $company->name,
'url' => $newsroomUrl,
...($company->websiteUrl() ? ['sameAs' => [$company->websiteUrl()]] : []),
...($logoUrl ? ['logo' => $logoUrl] : []),
...($boilerplate ? ['description' => Str::limit($boilerplate, 300)] : []),
];
@endphp
@section('title', $company->name.' – Newsroom · '.$brandName.$brandAccent)
@section('meta_description', Str::limit($boilerplate ?? __('web.newsroom_page.meta_description', ['company' => $company->name]), 155))
@section('og_type', 'profile')
@section('hreflang')
<link rel="alternate" hreflang="de" href="{{ url('/de/newsroom/'.$company->slug) }}">
<link rel="alternate" hreflang="en" href="{{ url('/en/newsroom/'.$company->slug) }}">
<link rel="alternate" hreflang="x-default" href="{{ url('/de/newsroom/'.$company->slug) }}">
@endsection
@section('structured_data')
<x-web.structured-data :data="$jsonLd" />
@endsection
@section('content')
<x-web.site-header />
<main class="bg-bg text-ink">
{{-- Breadcrumb --}}
<div class="border-b border-bg-rule">
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex items-center gap-2 text-[11.5px] text-ink-3">
<a href="{{ route('web.home') }}" class="hover:text-ink transition-colors">{{ __('web.nav.home') }}</a>
<span class="text-ink-4" aria-hidden="true">/</span>
<a href="{{ route('newsrooms') }}" class="hover:text-ink transition-colors">Newsrooms</a>
<span class="text-ink-4" aria-hidden="true">/</span>
<span class="text-ink font-semibold truncate max-w-[55vw]">{{ $company->name }}</span>
</div>
</div>
{{-- Masthead --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8 lg:pt-9">
<div class="flex items-center justify-between gap-4 mb-4">
<div class="eyebrow">Newsroom</div>
<a href="{{ route('newsrooms') }}" class="inline-flex items-center gap-1.5 text-[12px] font-semibold text-ink-3 hover:text-ink transition-colors whitespace-nowrap" aria-label="{{ __('web.newsroom_page.back_aria') }}">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M15 19l-7-7 7-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
{{ __('web.newsroom_page.all_newsrooms') }}
</a>
</div>
{{-- Firmenprofil --}}
<div class="bg-bg-elev border border-bg-rule p-6 sm:p-8">
<div class="flex items-start gap-4 sm:gap-6">
@if ($logoUrl)
<img src="{{ $logoUrl }}" alt="{{ $company->name }}" class="w-16 h-16 sm:w-24 sm:h-24 object-contain bg-white border border-bg-rule flex-shrink-0">
@else
<div class="w-16 h-16 sm:w-24 sm:h-24 flex items-center justify-center text-white text-[30px] sm:text-[44px] font-bold font-serif flex-shrink-0" style="background: {{ $color }};">
{{ $initial }}
</div>
@endif
<div class="min-w-0 flex-1">
<h1 class="font-serif text-[28px] sm:text-[38px] lg:text-[42px] font-semibold m-0 leading-[1.05] tracking-[-1px] text-ink">{{ $company->name }}</h1>
<div class="mt-3 text-[12px] text-ink-3 flex items-center gap-2.5 flex-wrap">
@if ($websiteHost)
<a href="{{ $company->websiteUrl() }}" target="_blank" rel="nofollow noopener" class="inline-flex items-center gap-1.5 font-medium hover:text-ink transition-colors">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"><circle cx="6" cy="6" r="5" stroke="currentColor" stroke-width="1"/><path d="M1 6h10M6 1c2 1.5 2 8.5 0 10M6 1c-2 1.5-2 8.5 0 10" stroke="currentColor" stroke-width="1"/></svg>
{{ $websiteHost }}
</a>
<span class="text-ink-4" aria-hidden="true">·</span>
@endif
<span class="inline-flex items-center gap-1.5">
<span class="w-1.5 h-1.5 rounded-full bg-brand pulse-dot"></span>
{{ __('web.newsroom_page.recent_stat', ['count' => $fmt($newsroomStats['recent'] ?? 0)]) }}
</span>
@if (! empty($newsroomStats['sinceYear']))
<span class="text-ink-4" aria-hidden="true">·</span>
<span>{{ __('web.newsroom_page.since', ['year' => $newsroomStats['sinceYear']]) }}</span>
@endif
</div>
</div>
</div>
@if ($boilerplate)
<p class="font-serif text-[15.5px] leading-[1.6] m-0 mt-5 pt-5 border-t border-bg-rule text-ink-2">{{ Str::limit($boilerplate, 360) }}</p>
@endif
{{-- Verwaltungszugang (Roadmap 1b.4): ein Hinweis, kein Angebot.
Der Zugang ist kostenlos und führt zur Firmen-E-Mail. --}}
<p class="text-[12px] text-ink-3 m-0 mt-5 pt-4 border-t border-bg-rule">
{{ __('web.newsroom_page.manage_hint', ['company' => $company->name]) }}
<a href="{{ route('newsroom.claim', ['slug' => $company->slug]) }}" rel="nofollow" class="font-medium underline underline-offset-2 hover:text-ink transition-colors">{{ __('web.newsroom_page.manage_link') }}</a>
</p>
@if ($categories->isNotEmpty())
<div class="flex flex-wrap items-center gap-2 mt-5 pt-5 border-t border-bg-rule">
<span class="eyebrow muted text-[10px] mr-1.5">{{ __('web.newsroom_page.industries') }}</span>
@foreach ($categories as $cat)
<a href="{{ $cat['href'] }}" class="inline-flex items-baseline gap-[7px] px-3 py-1.5 text-[12px] font-medium border border-bg-rule-strong bg-bg text-ink hover:bg-white hover:border-brand transition-colors">
{{ $cat['name'] }}
<span class="font-mono text-[10.5px] text-ink-3 font-medium">{{ $fmt($cat['count']) }}</span>
</a>
@endforeach
</div>
@endif
</div>
</section>
{{-- Stat strip --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-6">
<div class="grid grid-cols-2 lg:grid-cols-4 border-y border-bg-rule">
<div class="px-6 py-5 border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.category.stat_today') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['today'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_window_sub', ['date' => $referenceNow->translatedFormat('j. M Y')]) }}</div>
</div>
<div class="px-6 py-5 lg:border-r border-bg-rule">
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsroom_page.last_30_days') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['recent'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsroom_page.published_releases') }}</div>
</div>
<div class="px-6 py-5 border-r border-bg-rule border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.spotlight.stat_archive') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['total'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.spotlight.stat_archive_sub') }}</div>
</div>
<div class="px-6 py-5 border-t lg:border-t-0">
<div class="eyebrow muted text-[10px] mb-2.5">{{ __('web.newsroom_page.total_hits') }}</div>
<div class="font-serif text-[32px] sm:text-[36px] font-semibold leading-none tracking-[-0.6px] text-ink">{{ $fmt($newsroomStats['totalHits'] ?? 0) }}</div>
<div class="text-[11.5px] text-ink-3 mt-2">{{ __('web.newsroom_page.total_hits_sub') }}</div>
</div>
</div>
</section>
@if ($leadRelease)
{{-- Top-Meldung --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-10">
<article>
<a href="{{ route('release.detail', ['slug' => $leadRelease->slug]) }}" class="block group">
{{-- Kein hatch-dark: dessen background-image würde den bg-hero-grad-Verlauf überschreiben.
Text im Flow (min-h + flex), damit lange Titel auf Mobile nicht geclippt werden. --}}
<div class="relative overflow-hidden bg-hero-grad min-h-[220px] sm:min-h-[280px] lg:min-h-[340px] flex flex-col justify-end">
@if ($leadImageUrl)
<img src="{{ $leadImageUrl }}" alt="{{ $leadRelease->title }}" class="absolute inset-0 w-full h-full object-cover" loading="lazy">
@elseif ($leadPlaceholderUrl)
<img src="{{ $leadPlaceholderUrl }}" alt="" aria-hidden="true" class="absolute inset-0 w-full h-full object-cover" loading="lazy">
@else
<x-web.hero-fallback />
@endif
<div class="absolute top-4 left-4 sm:top-5 sm:left-5 z-10 flex gap-2">
<span class="bp-tag">{{ __('web.newsroom_page.latest') }}</span>
</div>
<div class="relative z-10 px-4 sm:px-9 pb-6 sm:pb-8 pt-14 sm:pt-[70px] text-white" style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.92));">
@if ($leadCatName)
<div class="eyebrow mb-3" style="color:#FF8B6F;">{{ $leadCatName }}</div>
@endif
<h2 class="font-serif text-[22px] sm:text-[28px] lg:text-[34px] font-semibold m-0 leading-[1.12] tracking-[-0.6px] max-w-[760px] group-hover:text-white">
{{ $leadRelease->title }}
</h2>
@if ($leadLead)
<p class="font-serif text-[13.5px] sm:text-[14.5px] leading-[1.5] mt-3 sm:mt-3.5 max-w-[640px] text-white/85">{{ $leadLead }}</p>
@endif
<div class="mt-3.5 sm:mt-[18px] flex items-center gap-3.5 text-[12px] text-white/70 flex-wrap">
@if ($leadRelease->published_at)
<span class="font-mono">{{ $leadRelease->published_at->translatedFormat('d.m.Y · H:i') }}</span>
<span aria-hidden="true">·</span>
@endif
<span>{{ __('web.shared.read_time', ['minutes' => $leadReadTime]) }}</span>
</div>
</div>
</div>
</a>
</article>
</section>
@endif
{{-- Meldungen + Sidebar --}}
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-12 mb-16 grid gap-9 lg:grid-cols-[1.7fr_1fr]">
<div>
<header class="flex items-baseline justify-between mb-4 min-h-[34px] flex-wrap gap-3">
<h2 class="font-serif text-[28px] font-semibold m-0 tracking-[-0.3px] text-ink">
{{ __('web.category.all_releases') }} <span class="text-[14px] text-ink-3 font-normal ml-1.5">· {{ $fmt($newsroomStats['total'] ?? 0) }}</span>
</h2>
</header>
<hr class="rule-strong">
@forelse ($releases as $release)
<x-web.feed-item :release="$release" />
@empty
<p class="text-[13px] text-ink-3 py-6">{{ __('web.newsroom_page.empty') }}</p>
@endforelse
<x-web.pagination :paginator="$releases" />
</div>
<aside class="flex flex-col gap-9">
<x-web.most-read :releases="$mostReadReleases ?? []" />
{{-- Eigenen Newsroom starten --}}
<section class="border border-bg-rule-strong bg-bg-elev p-6">
<div class="eyebrow mb-2">{{ __('web.publisher_cta.eyebrow') }}</div>
<h3 class="font-serif text-[20px] font-semibold m-0 mb-2 tracking-[-0.2px] leading-[1.2] text-ink">
{{ __('web.newsroom_page.own_title') }}
</h3>
<p class="text-[12.5px] leading-[1.5] m-0 mb-4 text-ink-2">
{{ __('web.newsroom_page.own_text') }}
</p>
<a href="{{ route('veroeffentlichen') }}"
class="w-full inline-flex items-center justify-center gap-2 px-[18px] py-2.5 text-[13px] font-semibold bg-white border border-bg-rule-strong text-ink rounded-[2px] cursor-pointer hover:border-brand hover:text-brand transition-colors">
{{ __('web.newsroom_page.own_cta') }} →
</a>
</section>
<x-web.publisher-cta />
</aside>
</section>
</main>
<x-web.site-footer />
@endsection