- /{edition}/newsroom/{slug}/feed: Sichtbarkeit wie die Newsroom-Seite
(aktiv, nicht entfernt, mindestens eine Meldung), Prüfung vor dem Cache,
301 bei zusammengeführter Firma; Autodiscovery und Link „RSS-Feed" auf
der Newsroom-Seite; Hub leitet auf das Portal der Firma.
- /{edition}/rss (RssPageController, static.rss DE/EN): Was RSS ist, wie
ein Reader funktioniert, Hauptfeed, alle Branchen-Feeds mit Adresse,
Muster des Newsroom-Feeds, Hinweise. Portalseite mit Portal-Kontext,
Hub → presseecho, in Sitemap und llms.txt.
- Topleiste, Burger-Menü und Footer verlinken RSS neben FAQ/Hilfe/API.
- FeedController in portal/category/newsroom aufgeteilt; Panel
„Auslieferung" nennt zusätzlich den Newsroom-Feed.
- Tests: FeedTest (+3), RssPageTest (4), StaticPagesTest, VisibilityStatsTest.
- dev/2026-09-01: Nachtrag in Protokoll Teil 11 und Roadmap 3.1.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
271 lines
17 KiB
PHP
271 lines
17 KiB
PHP
@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);
|
||
// RSS-Feed des Newsrooms (Roadmap 3.1) — Adresse aus der Portal-Basis.
|
||
$newsroomFeedUrl = StructuredData::portalBase($domainUrl ?? null).'/'.app()->getLocale().'/newsroom/'.$company->slug.'/feed';
|
||
$portalFeedUrl = StructuredData::portalBase($domainUrl ?? null).'/'.app()->getLocale().'/feed';
|
||
$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('feed_links')
|
||
<link rel="alternate" type="application/rss+xml" title="{{ __('web.rss.newsroom_link_title', ['company' => $company->name, 'brand' => $brandName.$brandAccent]) }}" href="{{ $newsroomFeedUrl }}">
|
||
<link rel="alternate" type="application/rss+xml" title="{{ __('web.rss.portal_link_title', ['brand' => $brandName.$brandAccent]) }}" href="{{ $portalFeedUrl }}">
|
||
@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
|
||
<span class="text-ink-4" aria-hidden="true">·</span>
|
||
<a href="{{ $newsroomFeedUrl }}" type="application/rss+xml" class="inline-flex items-center gap-1.5 font-medium hover:text-ink transition-colors" aria-label="{{ __('web.rss.newsroom_link_aria', ['company' => $company->name]) }}">
|
||
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M2 12.5A1.5 1.5 0 1 1 3.5 14 1.5 1.5 0 0 1 2 12.5ZM2 8a6 6 0 0 1 6 6h2a8 8 0 0 0-8-8Zm0-4a10 10 0 0 1 10 10h2A12 12 0 0 0 2 2Z"/></svg>
|
||
{{ __('web.newsroom_page.feed_link') }}
|
||
</a>
|
||
</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
|