- R-02/K-03: Aufrufaggregation und Merge ändern updated_at nicht mehr; neue Spalte press_releases.content_changed_at (INSTANT), contentModifiedAt() für Sitemap-lastmod, dateModified und article:modified_time - R-03: Sitemap und Legacy-view_url mit /en/ für englische Meldungen - R-04/O01: X-Pk-Ref je Empfänger per HMAC signiert; auf eine Sperre zählt — hart wie weich — nur ein gültig signierter Rückläufer, unsignierte standardmäßig nur zur Sichtung (BOUNCE_UNSIGNED_KIND=policy) - R-05/R-06: Unmerge setzt nur unveränderte Füllwerte zurück (company_merge_moves.merged_value), Canonical-Neubewertung nach Merge und Unmerge, assignAll setzt veraltete Verweise zurück - R-07 bis R-11: Crawler zählen nicht als Aufruf, Mail-Klick gedrosselt und verzögert geschrieben, Claim-Zähler erst nach Versand, email_bounces nach 395 Tagen gelöscht, Canonical-Portal über Kategorie-ID, llms-Cache geleert, Feed-Teaser ohne Doppel-Escaping, Merge-Ketten aufgelöst - N17/N18: kein falsches „gelistet seit", Rollen im Indexbericht wie Sitemap - OPT-1/OPT-2: topFeeds in SQL, purge-events löscht nach Grenz-ID Enthält aus PressRelease.php auch die Card-Bild-Helfer (B-01) und wasEverPublished()/neverPublished() (A-05), weil die Datei ungeteilt committet ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
810 lines
59 KiB
PHP
810 lines
59 KiB
PHP
@extends('web.layouts.web-master')
|
||
|
||
@php
|
||
use App\Enums\Portal;
|
||
use App\Services\PressRelease\CanonicalAssigner;
|
||
use App\Support\ImageCredit;
|
||
use App\Support\StructuredData;
|
||
use Illuminate\Support\Str;
|
||
|
||
// Private Vorschau über Magic-Link (/pm-vorschau/{token}): identisches
|
||
// Layout wie die spätere öffentliche Seite, plus Hinweis-Banner und noindex.
|
||
$isPreview = ($isPreview ?? false) === true;
|
||
$previewExpiresAt = $previewExpiresAt ?? null;
|
||
$previewPortalOptions = $previewPortalOptions ?? null;
|
||
|
||
$company = $release->company;
|
||
|
||
$categoryTranslation = $release->category?->translations?->firstWhere('locale', app()->getLocale())
|
||
?? $release->category?->translations?->first();
|
||
$categoryName = $categoryTranslation?->name;
|
||
$categorySlug = $categoryTranslation?->slug;
|
||
$categoryHref = $categorySlug ? route('kategorie', ['slug' => $categorySlug]) : route('web.home');
|
||
|
||
$parentTranslation = $release->category?->parent?->translations?->firstWhere('locale', app()->getLocale())
|
||
?? $release->category?->parent?->translations?->first();
|
||
$parentName = $parentTranslation?->name;
|
||
$parentSlug = $parentTranslation?->slug;
|
||
$parentHref = $parentSlug ? route('kategorie', ['slug' => $parentSlug]) : route('web.home');
|
||
|
||
// In der Vorschau ersatzweise der geplante Termin, damit die Meta-Zeile
|
||
// der späteren Veröffentlichung entspricht.
|
||
$publishedAt = $release->published_at ?? ($isPreview ? $release->scheduledAtLocal() : null);
|
||
$embargoAt = $release->embargoAtLocal();
|
||
|
||
$bodyHtml = $release->renderedText();
|
||
$wordCount = $release->wordCount();
|
||
$readTime = $release->readTimeMinutes();
|
||
|
||
$teaser = $release->teaserText(180);
|
||
|
||
// Subline nur bei explizit gesetztem Untertitel — kein Fallback auf den Textanfang,
|
||
// sonst erscheint der erste Absatz doppelt. Der Teaser dient nur der Meta-Description.
|
||
$lead = $release->subtitle;
|
||
|
||
$image = $release->images->first();
|
||
// In der anonymen Token-Vorschau ist die PM noch nicht öffentlich; die
|
||
// Media-Route braucht daher eine signierte URL, um die Bilder auszuliefern.
|
||
$signMedia = $isPreview ?? false;
|
||
$imageUrl = $image?->variantUrl('detail', $signMedia) ?? $image?->variantUrl('card', $signMedia) ?? $image?->url($signMedia);
|
||
$imageCaption = $image?->description ?: $image?->title;
|
||
// Bildnachweis, KI-Kennzeichnung und Verantwortlich-Zeile aus einer
|
||
// Auflösung (Bildkennzeichnung D.1, Decision-Update 3.4). Die sichtbare
|
||
// Zeile unter dem Bild ist die Nennung, das Overlay die Erklärung.
|
||
$imageCreditInfo = $image ? ImageCredit::for($image, $company) : null;
|
||
$imageCredit = $imageCreditInfo?->credit;
|
||
$imageAiBadge = $imageCreditInfo?->aiLabel;
|
||
$imageReportUrl = route('melden', ['edition' => app()->getLocale()]);
|
||
|
||
// Ohne Pressefoto: die im User-Panel gewählte Platzhalter-Variante.
|
||
// Null bei Altbeständen ohne Variante — dann generischer Hero-Fallback.
|
||
$placeholderUrl = $imageUrl ? null : $release->chosenPlaceholderUrl();
|
||
|
||
$companyName = $company?->name;
|
||
$companyInitial = $companyName ? mb_strtoupper(mb_substr($companyName, 0, 1)) : '·';
|
||
$companyHref = $company?->slug ? route('newsroom', ['slug' => $company->slug]) : route('newsrooms');
|
||
|
||
$countryNames = ['DE' => __('web.countries.de'), 'AT' => __('web.countries.at'), 'CH' => __('web.countries.ch')];
|
||
$companyCountry = $company?->country_code ? ($countryNames[strtoupper($company->country_code)] ?? null) : null;
|
||
|
||
$boilerplate = filled($release->boilerplate_override) ? $release->boilerplate_override : $company?->boilerplate;
|
||
|
||
$keywords = collect(preg_split('/[,;\n]+/', (string) $release->keywords))
|
||
->map(fn ($keyword) => trim($keyword))
|
||
->filter()
|
||
->unique()
|
||
->take(8);
|
||
|
||
$contact = $release->contacts->first();
|
||
$contactName = $contact ? trim(implode(' ', array_filter([$contact->title, $contact->first_name, $contact->last_name]))) : null;
|
||
// Legacy-Bestände ohne Kontakt-Datensatz: der Block erscheint trotzdem,
|
||
// solange die Firma erreichbare Angaben hat (wie im Altsystem).
|
||
$contactEmail = $contact?->email ?: $company?->email;
|
||
$hasContactBlock = $contact || filled($contactEmail) || filled($company?->address) || filled($company?->website);
|
||
|
||
// Attributions-Zeile wie im Legacy-System: Urheber (Firma der PM) und
|
||
// Übermittler (Profilfeld agency_name des einliefernden Accounts, z. B.
|
||
// "connektar.de Presseverteiler" bei API-Einlieferungen).
|
||
$agencyName = $release->user?->agency_name;
|
||
|
||
$publisherVerified = (bool) ($company?->is_active && filled($company?->website));
|
||
$isRecommended = $release->content_score !== null && $release->content_score >= 80;
|
||
|
||
// In der Vorschau wird der Vorschau-Link selbst geteilt — die öffentliche
|
||
// URL existiert vor der Veröffentlichung noch nicht.
|
||
$shareUrl = $isPreview ? request()->fullUrl() : route('release.detail', ['slug' => $release->slug]);
|
||
$brandLabel = config('app.theme') === 'presseecho' ? 'presseecho' : 'businessportal24';
|
||
|
||
// Doppelveröffentlichung (Anhang 2, 1c.8): rel=canonical zeigt auf die
|
||
// Erstquelle — die eigene Adresse, oder bei zweiter Fassung/portal=both
|
||
// das Portal, das laut Kategorie-Zuordnung führt. Geteilt wird weiterhin
|
||
// die Adresse, auf der der Leser gerade ist.
|
||
$canonicalUrl = $isPreview
|
||
? $shareUrl
|
||
: app(CanonicalAssigner::class)->canonicalUrlFor($release, $brandLabel === 'presseecho' ? Portal::Presseecho : Portal::Businessportal24);
|
||
|
||
// dateModified/article:modified_time = letzte inhaltliche Änderung, nie
|
||
// vor datePublished (PressRelease::contentModifiedAt()); updated_at
|
||
// bewegen auch Aufrufzähler und Merge (Umsetzungsaudit 29.09.2026, R-02).
|
||
$modifiedAt = $release->contentModifiedAt() ?? $publishedAt;
|
||
|
||
// SEO: NewsArticle + BreadcrumbList als JSON-LD — nur für die öffentliche
|
||
// Seite (die Vorschau ist noindex und hat keine stabile URL).
|
||
$jsonLd = null;
|
||
|
||
if (! $isPreview) {
|
||
$portalBase = StructuredData::portalBase($domainUrl ?? null);
|
||
$authorNewsroomUrl = $company?->slug ? StructuredData::newsroomUrl($portalBase, $company->slug) : null;
|
||
|
||
// URLs aus der Portal-Basis (wie der Canonical) statt route(), damit
|
||
// Host und Canonical auch bei forceRootUrl-Abweichungen übereinstimmen.
|
||
$breadcrumbs = collect([
|
||
['name' => $brandLabel, 'item' => $portalBase.'/'.app()->getLocale()],
|
||
$categoryName && $categorySlug ? ['name' => $categoryName, 'item' => $portalBase.'/'.app()->getLocale().'/category/'.$categorySlug] : null,
|
||
['name' => $release->title, 'item' => $portalBase.'/'.app()->getLocale().'/press-release/'.$release->slug],
|
||
])->filter()->values();
|
||
|
||
$jsonLd = [
|
||
'@context' => 'https://schema.org',
|
||
'@graph' => [
|
||
array_filter([
|
||
'@type' => 'NewsArticle',
|
||
'mainEntityOfPage' => ['@type' => 'WebPage', '@id' => $canonicalUrl],
|
||
'headline' => Str::limit($release->title, 110),
|
||
'description' => filled($lead) ? $lead : $teaser,
|
||
'image' => $imageUrl ? [$imageUrl] : null,
|
||
'datePublished' => $publishedAt?->toIso8601String(),
|
||
'dateModified' => $modifiedAt?->toIso8601String(),
|
||
'articleSection' => $categoryName,
|
||
'keywords' => $keywords->isNotEmpty() ? $keywords->implode(', ') : null,
|
||
'wordCount' => $wordCount ?: null,
|
||
'inLanguage' => app()->getLocale(),
|
||
'isAccessibleForFree' => true,
|
||
// author trägt die @id der Newsroom-Organization (2.1): Meldung
|
||
// und Newsroom-Seite beschreiben dieselbe Entität.
|
||
'author' => $companyName ? array_filter([
|
||
'@type' => 'Organization',
|
||
'@id' => $authorNewsroomUrl ? StructuredData::newsroomId($authorNewsroomUrl) : null,
|
||
'name' => $companyName,
|
||
'url' => $authorNewsroomUrl,
|
||
]) : null,
|
||
'publisher' => StructuredData::publisher($portalBase),
|
||
], fn ($value) => $value !== null),
|
||
[
|
||
'@type' => 'BreadcrumbList',
|
||
'itemListElement' => $breadcrumbs->map(fn (array $crumb, int $index) => [
|
||
'@type' => 'ListItem',
|
||
'position' => $index + 1,
|
||
'name' => $crumb['name'],
|
||
'item' => $crumb['item'],
|
||
])->all(),
|
||
],
|
||
],
|
||
];
|
||
}
|
||
@endphp
|
||
|
||
@section('title', $release->title.' – '.$brandLabel)
|
||
@section('meta_description', filled($lead) ? $lead : $teaser)
|
||
@section('og_type', 'article')
|
||
@if ($imageUrl)
|
||
@section('og_image', $image?->variantUrl('og', $signMedia) ?? $imageUrl)
|
||
@endif
|
||
@if ($isPreview)
|
||
@section('robots', 'noindex,nofollow')
|
||
@else
|
||
@section('canonical', $canonicalUrl)
|
||
|
||
@section('meta_extra')
|
||
@if ($publishedAt)
|
||
<meta property="article:published_time" content="{{ $publishedAt->toIso8601String() }}">
|
||
@endif
|
||
@if ($publishedAt && $modifiedAt?->greaterThan($publishedAt))
|
||
<meta property="article:modified_time" content="{{ $modifiedAt->toIso8601String() }}">
|
||
@endif
|
||
@if ($categoryName)
|
||
<meta property="article:section" content="{{ $categoryName }}">
|
||
@endif
|
||
@foreach ($keywords as $keyword)
|
||
<meta property="article:tag" content="{{ $keyword }}">
|
||
@endforeach
|
||
@if ($imageUrl && ($imageCaption || $release->title))
|
||
<meta property="og:image:alt" content="{{ $imageCaption ?: $release->title }}">
|
||
@endif
|
||
@endsection
|
||
|
||
@section('structured_data')
|
||
<x-web.structured-data :data="$jsonLd" />
|
||
@endsection
|
||
@endif
|
||
|
||
@section('content')
|
||
{{-- Lese-Fortschrittsbalken --}}
|
||
<div class="fixed top-0 left-0 right-0 h-0.5 bg-bg-rule z-[60]"
|
||
x-data="{ progress: 0 }"
|
||
x-init="const update = () => { const h = document.body.scrollHeight - window.innerHeight; progress = h > 0 ? Math.min(100, (window.scrollY / h) * 100) : 0; }; update(); window.addEventListener('scroll', update, { passive: true }); window.addEventListener('resize', update);"
|
||
aria-hidden="true">
|
||
<div class="h-full bg-brand transition-[width] duration-150 ease-out" :style="`width:${progress}%`"></div>
|
||
</div>
|
||
|
||
<x-web.site-header />
|
||
|
||
@if ($isPreview)
|
||
{{-- Hinweis-Banner: private Vorschau, nicht öffentlich erreichbar --}}
|
||
<div class="bg-amber-50 border-b border-amber-200 text-amber-900">
|
||
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 py-2.5 flex flex-wrap items-center gap-x-4 gap-y-1 text-[12.5px]">
|
||
<span class="text-[9.5px] font-bold tracking-[0.18em] uppercase px-2.5 py-[3px] border border-amber-400 font-mono whitespace-nowrap">
|
||
● {{ __('web.detail.preview_label') }}
|
||
</span>
|
||
<span>{{ __('web.detail.preview_text') }}</span>
|
||
<span class="font-mono text-[11.5px] text-amber-800">
|
||
{{ __('web.detail.preview_status', ['status' => $release->status->label()]) }}
|
||
@if ($previewExpiresAt)
|
||
· {{ __('web.detail.preview_valid_until', ['date' => $previewExpiresAt->translatedFormat('j. F Y, H:i')]) }}
|
||
@endif
|
||
</span>
|
||
|
||
@if (! empty($previewPortalOptions))
|
||
{{-- PM erscheint auf beiden Portalen: Ansicht umschalten --}}
|
||
<span class="sm:ml-auto inline-flex items-center gap-2">
|
||
<span class="text-[10px] font-semibold tracking-[0.14em] uppercase text-amber-800 font-mono">{{ __('web.detail.preview_portal_switch') }}</span>
|
||
<span class="inline-flex items-stretch border border-amber-400 divide-x divide-amber-400 font-mono text-[10.5px]">
|
||
@foreach ($previewPortalOptions as $option)
|
||
<a href="{{ $option['url'] }}"
|
||
@class([
|
||
'px-2.5 py-[3px] whitespace-nowrap transition-colors cursor-pointer',
|
||
'bg-amber-400/40 font-bold text-amber-950' => $option['active'],
|
||
'hover:bg-amber-100 text-amber-900' => ! $option['active'],
|
||
])
|
||
@if ($option['active']) aria-current="true" @endif>
|
||
{{ $option['label'] }}
|
||
</a>
|
||
@endforeach
|
||
</span>
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
<main class="bg-bg text-ink">
|
||
<article>
|
||
{{-- Breadcrumbs --}}
|
||
<div class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-7">
|
||
<nav class="flex flex-wrap gap-2 text-[11.5px] text-ink-3 mb-2 font-mono tracking-wide" aria-label="{{ __('web.detail.breadcrumbs') }}">
|
||
<a href="{{ route('web.home') }}" class="cursor-pointer hover:text-ink transition-colors">{{ __('web.nav.home') }}</a>
|
||
<span aria-hidden="true">/</span>
|
||
@if ($parentName)
|
||
<a href="{{ $parentHref }}" class="cursor-pointer hover:text-ink transition-colors">{{ $parentName }}</a>
|
||
<span aria-hidden="true">/</span>
|
||
@endif
|
||
@if ($categoryName)
|
||
<a href="{{ $categoryHref }}" class="cursor-pointer text-brand hover:text-brand-deep transition-colors">{{ $categoryName }}</a>
|
||
<span aria-hidden="true">/</span>
|
||
@endif
|
||
<span class="text-ink-2">{{ __('web.detail.press_release') }}</span>
|
||
</nav>
|
||
</div>
|
||
|
||
@if (! $imageUrl)
|
||
{{-- Ohne Pressefoto: Headline direkt in der Platzhalter-Fläche der PM
|
||
(gewählte Variante wie im User-Panel, sonst generischer Fallback). --}}
|
||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-[18px]">
|
||
<article class="relative overflow-hidden bg-hero-grad">
|
||
@if ($placeholderUrl)
|
||
<img src="{{ $placeholderUrl }}" alt="" aria-hidden="true"
|
||
class="absolute inset-0 h-full w-full object-cover" loading="eager">
|
||
@else
|
||
<x-web.hero-fallback />
|
||
@endif
|
||
|
||
@if ($embargoAt)
|
||
<div class="absolute top-5 left-5 sm:top-6 sm:left-9 flex items-center gap-2.5 flex-wrap">
|
||
<span class="text-[9.5px] font-bold tracking-[0.18em] uppercase text-white px-2.5 py-[3px] border border-white/50 font-mono bg-black/25">
|
||
● {{ __('web.detail.embargo_lifted') }}
|
||
</span>
|
||
<span class="text-[11px] text-white/70 font-mono">{{ $embargoAt->translatedFormat('j. F Y, H:i') }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="relative px-5 sm:px-9 pt-[110px] sm:pt-[150px] pb-7 sm:pb-9"
|
||
style="background:linear-gradient(180deg,transparent,rgba(0,0,0,0.55) 55%,rgba(0,0,0,0.82));">
|
||
<div class="flex items-center gap-3.5 mb-4 flex-wrap">
|
||
@if ($categoryName)
|
||
<a href="{{ $categoryHref }}" class="bp-cat cursor-pointer transition-colors" style="color:#FF8B6F;">{{ $categoryName }}</a>
|
||
<span class="w-1 h-1 rounded-full bg-white/40" aria-hidden="true"></span>
|
||
@endif
|
||
<span class="text-[11px] text-white/60 italic font-serif">{{ __('web.detail.audience') }}</span>
|
||
</div>
|
||
|
||
<h1 class="font-serif text-[30px] sm:text-[38px] lg:text-[44px] font-semibold m-0 tracking-[-0.8px] leading-[1.1] max-w-[920px] text-white" style="text-wrap:balance;">
|
||
{{ $release->title }}
|
||
</h1>
|
||
|
||
@if (filled($lead))
|
||
<p class="font-serif text-[16.5px] sm:text-[19px] leading-[1.45] m-0 mt-5 text-white/85 max-w-[820px] font-normal" style="text-wrap:pretty;">
|
||
{{ $lead }}
|
||
</p>
|
||
@endif
|
||
|
||
<div class="flex flex-wrap items-center text-[12.5px] text-white/75 mt-6" style="gap:10px 22px;">
|
||
@if ($companyName)
|
||
<span class="inline-flex items-center gap-2">
|
||
<span class="w-7 h-7 bg-white text-ink inline-flex items-center justify-center text-[13px] font-bold font-serif">{{ $companyInitial }}</span>
|
||
<span class="font-semibold text-white">{{ $companyName }}</span>
|
||
</span>
|
||
<span class="w-px h-[14px] bg-white/25" aria-hidden="true"></span>
|
||
@endif
|
||
@if ($publishedAt)
|
||
<time datetime="{{ $publishedAt->toIso8601String() }}" class="font-mono text-[11.5px] text-white/65">
|
||
{{ $publishedAt->translatedFormat('j. F Y') }} · {{ __('web.detail.time_at', ['time' => $publishedAt->format('H:i')]) }}
|
||
</time>
|
||
@endif
|
||
<span class="font-mono text-[11.5px] text-white/65">{{ __('web.shared.read_time', ['minutes' => $readTime]) }} · {{ __('web.detail.words', ['count' => number_format($wordCount, 0, ',', '.')]) }}@unless ($isPreview) · {{ __('web.detail.views', ['count' => number_format((int) $release->hits, 0, ',', '.')]) }}@endunless</span>
|
||
@if ($companyCountry)
|
||
<span class="inline-flex items-center gap-1.5 font-mono text-[11.5px] text-white/65">
|
||
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M7 1a5 5 0 015 5c0 4-5 7-5 7s-5-3-5-7a5 5 0 015-5z" stroke="currentColor" stroke-width="1.3" fill="none" /><circle cx="7" cy="6" r="1.5" fill="currentColor" /></svg>
|
||
{{ $companyCountry }}
|
||
</span>
|
||
@endif
|
||
|
||
@if ($isRecommended)
|
||
<span class="inline-flex items-center gap-1.5 px-2 py-[3px] text-[9.5px] font-bold tracking-[0.14em] uppercase text-white border border-white/40 font-mono cursor-help"
|
||
title="{{ __('web.detail.recommended_hint') }}">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path d="M12 2l9 4v6c0 5-3.5 9-9 10-5.5-1-9-5-9-10V6z" stroke="currentColor" stroke-width="2" fill="none" />
|
||
<path d="M8 12l3 3 5-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||
</svg>
|
||
{{ __('web.detail.recommended') }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
@else
|
||
{{-- Hero Meta Block --}}
|
||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8">
|
||
<div class="pt-[18px] pb-8 border-b border-bg-rule">
|
||
@if ($embargoAt)
|
||
<div class="flex items-center gap-2.5 mb-3.5 flex-wrap">
|
||
<span class="text-[9.5px] font-bold tracking-[0.18em] uppercase text-brand px-2.5 py-[3px] border border-brand font-mono">
|
||
● {{ __('web.detail.embargo_lifted') }}
|
||
</span>
|
||
<span class="text-[11px] text-ink-3 font-mono">{{ $embargoAt->translatedFormat('j. F Y, H:i') }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="flex items-center gap-3.5 mb-4 flex-wrap">
|
||
@if ($categoryName)
|
||
<a href="{{ $categoryHref }}" class="bp-cat cursor-pointer hover:text-brand-deep transition-colors">{{ $categoryName }}</a>
|
||
<span class="w-1 h-1 rounded-full bg-ink-4" aria-hidden="true"></span>
|
||
@endif
|
||
<span class="text-[11px] text-ink-3 italic font-serif">{{ __('web.detail.audience') }}</span>
|
||
</div>
|
||
|
||
<h1 class="font-serif text-[32px] sm:text-[40px] lg:text-[48px] font-semibold m-0 mb-[22px] tracking-[-0.8px] leading-[1.1] max-w-[980px]" style="text-wrap:balance;">
|
||
{{ $release->title }}
|
||
</h1>
|
||
|
||
@if (filled($lead))
|
||
<p class="font-serif text-[18px] sm:text-[21px] leading-[1.45] m-0 mb-7 text-ink-2 max-w-[880px] font-normal" style="text-wrap:pretty;">
|
||
{{ $lead }}
|
||
</p>
|
||
@endif
|
||
|
||
<div class="flex flex-wrap items-center text-[12.5px] text-ink-2" style="gap:10px 22px;">
|
||
@if ($companyName)
|
||
<span class="inline-flex items-center gap-2">
|
||
<span class="w-7 h-7 bg-black text-white inline-flex items-center justify-center text-[13px] font-bold font-serif">{{ $companyInitial }}</span>
|
||
<span class="font-semibold">{{ $companyName }}</span>
|
||
</span>
|
||
<span class="w-px h-[14px] bg-bg-rule-strong" aria-hidden="true"></span>
|
||
@endif
|
||
@if ($publishedAt)
|
||
<time datetime="{{ $publishedAt->toIso8601String() }}" class="font-mono text-[11.5px] text-ink-3">
|
||
{{ $publishedAt->translatedFormat('j. F Y') }} · {{ __('web.detail.time_at', ['time' => $publishedAt->format('H:i')]) }}
|
||
</time>
|
||
@endif
|
||
<span class="font-mono text-[11.5px] text-ink-3">{{ __('web.shared.read_time', ['minutes' => $readTime]) }} · {{ __('web.detail.words', ['count' => number_format($wordCount, 0, ',', '.')]) }}@unless ($isPreview) · {{ __('web.detail.views', ['count' => number_format((int) $release->hits, 0, ',', '.')]) }}@endunless</span>
|
||
@if ($companyCountry)
|
||
<span class="inline-flex items-center gap-1.5 font-mono text-[11.5px] text-ink-3">
|
||
<svg width="11" height="11" viewBox="0 0 14 14" fill="none" aria-hidden="true"><path d="M7 1a5 5 0 015 5c0 4-5 7-5 7s-5-3-5-7a5 5 0 015-5z" stroke="currentColor" stroke-width="1.3" fill="none" /><circle cx="7" cy="6" r="1.5" fill="currentColor" /></svg>
|
||
{{ $companyCountry }}
|
||
</span>
|
||
@endif
|
||
|
||
@if ($isRecommended)
|
||
<span class="inline-flex items-center gap-1.5 px-2 py-[3px] text-[9.5px] font-bold tracking-[0.14em] uppercase text-brand bg-brand/[0.04] border border-brand/30 font-mono cursor-help"
|
||
title="{{ __('web.detail.recommended_hint') }}">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path d="M12 2l9 4v6c0 5-3.5 9-9 10-5.5-1-9-5-9-10V6z" stroke="currentColor" stroke-width="2" fill="none" />
|
||
<path d="M8 12l3 3 5-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||
</svg>
|
||
{{ __('web.detail.recommended') }}
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{{-- Hero Image --}}
|
||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-8">
|
||
<div class="relative flex items-end p-[18px] bg-bg-dark overflow-hidden" style="aspect-ratio:21/9;">
|
||
<img src="{{ $imageUrl }}" alt="{{ $imageCaption ?: $release->title }}"
|
||
class="absolute inset-0 h-full w-full object-cover" loading="eager">
|
||
@if ($imageAiBadge)
|
||
{{-- KI-Badge am Bild (Art. 50 KI-VO): kräftig abgedunkelter
|
||
Hintergrund, damit es auch auf hellen Bildern lesbar bleibt.
|
||
Rechts unten, damit es das „i"-Zeichen links nicht überlappt. --}}
|
||
<span class="relative ml-auto text-[9.5px] font-bold tracking-[0.18em] uppercase text-white px-2.5 py-[3px] border border-white/60 font-mono bg-black/75 shadow-sm">
|
||
{{ $imageAiBadge }}
|
||
</span>
|
||
@endif
|
||
|
||
@if ($imageCreditInfo?->hasDetails())
|
||
{{-- Detailfenster zum Bildnachweis (Bildkennzeichnung D.2, E-B13):
|
||
eigenes „i"-Zeichen, kein „cr"-Logo. Alpine ohne Plugin: Fokus
|
||
wandert in den Dialog und beim Schließen zurück; Escape schließt.
|
||
Auf Mobil ein Bottom-Sheet, ab sm ein Popover am Icon. --}}
|
||
<div x-data="{ open: false, show() { this.open = true; this.$nextTick(() => this.$refs.close.focus()) }, hide() { this.open = false; this.$nextTick(() => this.$refs.trigger.focus()) } }"
|
||
@keydown.escape.window="if (open) hide()"
|
||
class="absolute bottom-3 left-3 z-10" data-image-credit>
|
||
<button type="button" x-ref="trigger" @click="open ? hide() : show()" :aria-expanded="open ? 'true' : 'false'"
|
||
aria-haspopup="dialog" aria-controls="image-credit-dialog"
|
||
class="flex size-7 items-center justify-center rounded-full bg-black/60 text-white ring-1 ring-white/50 backdrop-blur-[2px] hover:bg-black/80 focus:outline-none focus-visible:ring-2 focus-visible:ring-white"
|
||
title="{{ __('web.detail.image_credit_open') }}">
|
||
<span class="sr-only">{{ __('web.detail.image_credit_open') }}</span>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<circle cx="12" cy="12" r="9.5" stroke="currentColor" stroke-width="1.6" />
|
||
<path d="M12 10.5v6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||
<circle cx="12" cy="7.6" r="1.15" fill="currentColor" />
|
||
</svg>
|
||
</button>
|
||
<div x-show="open" x-cloak @click.outside="hide()" id="image-credit-dialog" role="dialog" aria-modal="true"
|
||
aria-labelledby="image-credit-title"
|
||
class="fixed inset-x-0 bottom-0 z-40 max-h-[80vh] overflow-y-auto border-t border-bg-rule-strong bg-bg p-5 text-[13px] text-ink shadow-2xl sm:absolute sm:inset-auto sm:bottom-10 sm:left-0 sm:w-[22rem] sm:border sm:shadow-lg">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<h2 id="image-credit-title" class="m-0 text-[11px] font-semibold uppercase tracking-[0.14em] text-ink-3">{{ __('web.detail.image_credit_title') }}</h2>
|
||
<button type="button" x-ref="close" @click="hide()" class="text-[12px] text-ink-3 underline-offset-2 hover:text-ink hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-ink">
|
||
{{ __('web.detail.image_credit_close') }}
|
||
</button>
|
||
</div>
|
||
<ul class="m-0 mt-3 list-none space-y-2 p-0 leading-[1.5]">
|
||
@if ($imageCreditInfo->credit)
|
||
<li>{{ $imageCreditInfo->creditLabel }}: <span class="font-medium">{{ $imageCreditInfo->credit }}</span></li>
|
||
@endif
|
||
@if ($imageCreditInfo->licenseUrl)
|
||
<li>{{ __('web.detail.image_license') }}: <a href="{{ $imageCreditInfo->licenseUrl }}" rel="nofollow noopener" target="_blank" class="underline underline-offset-2">{{ \Illuminate\Support\Str::limit($imageCreditInfo->licenseUrl, 60) }}</a></li>
|
||
@endif
|
||
@if ($imageCreditInfo->sourceUrl)
|
||
<li>{{ __('web.detail.image_source_link') }}: <a href="{{ $imageCreditInfo->sourceUrl }}" rel="nofollow noopener" target="_blank" class="underline underline-offset-2">{{ \Illuminate\Support\Str::limit($imageCreditInfo->sourceUrl, 60) }}</a></li>
|
||
@endif
|
||
@if ($imageCreditInfo->aiLabel)
|
||
<li>
|
||
{{ $image->ai_declaration?->value === 'ai_edited' ? __('web.detail.image_ai_hint_edited') : __('web.detail.image_ai_hint_generated') }}
|
||
@if ($imageCreditInfo->aiSourceLabel)
|
||
<span class="block text-ink-3">{{ __('web.detail.image_ai_source', ['source' => $imageCreditInfo->aiSourceLabel]) }}</span>
|
||
@endif
|
||
</li>
|
||
@endif
|
||
@if ($imageCreditInfo->responsibleLine)
|
||
<li class="text-ink-3">{{ $imageCreditInfo->responsibleLine }}</li>
|
||
@endif
|
||
</ul>
|
||
<a href="{{ $imageReportUrl }}" class="mt-4 inline-block text-[12px] text-ink-3 underline underline-offset-2 hover:text-ink">{{ __('web.detail.image_report') }}</a>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@if ($imageCaption || $imageCreditInfo?->line())
|
||
<div class="text-[11.5px] text-ink-3 mt-2.5 italic max-w-[720px]" data-image-credit-line>
|
||
{{ $imageCaption }}@if ($imageCaption && $imageCreditInfo?->line()) · @endif@if ($imageCreditInfo?->line())<span class="not-italic">{{ $imageCreditInfo->line() }}</span>@endif
|
||
</div>
|
||
@endif
|
||
</section>
|
||
@endif
|
||
|
||
{{-- Body + Sidebar --}}
|
||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 pt-10 grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-10 lg:gap-14">
|
||
{{-- Body --}}
|
||
<div>
|
||
{{-- KI-Label (Art. 50 KI-VO) in Einreicher-Stimme am Anfang
|
||
des Inhalts; neutrales ℹ-Icon, kein Flux auf Web-Domains. --}}
|
||
@if ($aiTextLabel = $release->aiTextPublicLabel())
|
||
<div class="flex items-start gap-2 border border-bg-rule-strong bg-bg-elev px-3 py-2.5 mb-6 max-w-[680px] text-[12px] leading-[1.5] text-ink-3">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true" class="shrink-0 mt-0.5">
|
||
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8" fill="none" />
|
||
<path d="M12 11v5" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
||
<circle cx="12" cy="8" r="1.1" fill="currentColor" />
|
||
</svg>
|
||
<span>{{ $aiTextLabel }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="pm-body max-w-[680px]">
|
||
{!! $bodyHtml !!}
|
||
</div>
|
||
|
||
@if (filled($boilerplate))
|
||
<div class="mt-9 pt-5 border-t border-bg-rule-strong max-w-[680px]">
|
||
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.about_company') }}</div>
|
||
<p class="text-[13px] leading-[1.6] text-ink-3 m-0 max-w-[620px]">{{ $boilerplate }}</p>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Inline-Werbeplatz (Position der früheren Inline-Ad); in der privaten Vorschau ausgeblendet --}}
|
||
@unless ($isPreview)
|
||
<x-web.ad-slot variant="inline" />
|
||
@endunless
|
||
|
||
{{-- Pressekontakt (Fallback ohne Kontakt-Datensatz: Firmendaten) --}}
|
||
@if ($hasContactBlock)
|
||
<div class="mt-8 px-5 sm:px-7 py-6 bg-white border border-bg-rule-strong max-w-[680px]">
|
||
<div class="eyebrow mb-3 text-[10.5px]">{{ __('web.detail.press_contact') }}</div>
|
||
|
||
<div class="flex gap-1.5 items-center mb-1 flex-wrap">
|
||
<strong class="font-serif text-[19px] font-semibold tracking-[-0.2px]">{{ $contactName ?: $companyName }}</strong>
|
||
@if ($publisherVerified)
|
||
<span title="{{ __('web.detail.verified_hint') }}" class="inline-flex items-center text-verify-border flex-shrink-0">
|
||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||
<path d="M12 1l2.5 3 3.8-.4-.5 3.8L21 9l-2.6 2.5L19 15l-3.8.4L12 19l-2.5-3.6L5.7 15 5.6 11.4 3 9l2.5-1.6L4.7 4 8.5 4z" fill="currentColor" />
|
||
<path d="M7.5 12l3 3 6-6" stroke="white" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
||
</svg>
|
||
</span>
|
||
@endif
|
||
</div>
|
||
@if ($contact?->responsibility || $companyName)
|
||
<div class="text-[12.5px] text-ink-3 mb-3.5 leading-[1.5]">
|
||
@if ($contact?->responsibility){{ $contact->responsibility }}<br />@endif
|
||
{{ $companyName }}
|
||
</div>
|
||
@endif
|
||
|
||
<div class="flex flex-col gap-2 text-[12.5px] font-mono">
|
||
@if ($company?->address)
|
||
<span class="flex items-center gap-2">
|
||
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M7 1a5 5 0 015 5c0 4-5 7-5 7s-5-3-5-7a5 5 0 015-5zm0 3.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z" fill="currentColor" /></svg>
|
||
<span class="text-ink-2">{{ $company->address }}</span>
|
||
</span>
|
||
@endif
|
||
@if ($contact?->phone)
|
||
<span class="flex items-center gap-2">
|
||
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M2 3l3-1 1.5 3-1.5 1c.5 2 2 3.5 4 4l1-1.5L13 10l-1 3c-5 0-10-5-10-10z" fill="currentColor" /></svg>
|
||
<a href="tel:{{ preg_replace('/[^0-9+]/', '', $contact->phone) }}" class="text-ink-2 hover:text-ink transition-colors">{{ $contact->phone }}</a>
|
||
</span>
|
||
@endif
|
||
@if ($contactEmail)
|
||
<span class="flex items-center gap-2">
|
||
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M1 3h12v8H1zM1 3l6 4 6-4" stroke="currentColor" stroke-width="1.2" fill="none" /></svg>
|
||
<a href="mailto:{{ $contactEmail }}" class="text-ink underline underline-offset-2 hover:text-brand transition-colors">{{ $contactEmail }}</a>
|
||
</span>
|
||
@endif
|
||
@if ($company?->website)
|
||
<span class="flex items-center gap-2">
|
||
<svg width="14" height="14" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.2" fill="none" /><path d="M1.5 7h11M7 1.5c2 1.5 2 9.5 0 11M7 1.5c-2 1.5-2 9.5 0 11" stroke="currentColor" stroke-width="1" fill="none" /></svg>
|
||
<a href="{{ $company->websiteUrl() }}" rel="nofollow noopener" target="_blank" class="text-ink underline underline-offset-2 hover:text-brand transition-colors">{{ Str::of($company->website)->after('//')->rtrim('/') }}</a>
|
||
</span>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Attribution: Urheber & übermittelnder Verteiler (Legacy-Konvention) --}}
|
||
@if ($companyName)
|
||
<p class="mt-6 text-[12px] text-ink-3 italic max-w-[680px]">
|
||
@if ($agencyName && $agencyName !== $companyName)
|
||
{{ __('web.detail.attribution_via', ['company' => $companyName, 'agency' => $agencyName]) }}
|
||
@elseif ($agencyName)
|
||
{{ __('web.detail.attribution_agency_only', ['agency' => $agencyName]) }}
|
||
@else
|
||
{{ __('web.detail.attribution', ['company' => $companyName]) }}
|
||
@endif
|
||
</p>
|
||
@endif
|
||
|
||
{{-- Zwei Handlungsfelder, klar getrennt und mit Button-Charakter:
|
||
rechtliches Anliegen (T13) und Verwaltung über die hinterlegte
|
||
Adresse (Roadmap 1b.2/1b.4). Vorher eine Textzeile in 11,5 px,
|
||
die überlesen wurde. Die Empfängeradresse steht maskiert dabei,
|
||
damit klar ist, wohin der Link geht — ohne sie preiszugeben. --}}
|
||
@php
|
||
$portalUrl = rtrim(config('domains.domain_portal_url', config('app.url')), '/');
|
||
$claimRecipients = ($company && ! $company->isGone() && $company->slug)
|
||
? app(\App\Services\Company\NewsroomClaimService::class)->maskedRecipients($company)
|
||
: collect();
|
||
@endphp
|
||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||
<div class="border border-bg-rule-strong bg-bg-elev p-5">
|
||
<div class="eyebrow muted text-[10px] mb-2">{{ __('web.detail.legal_eyebrow') }}</div>
|
||
<p class="text-[13px] leading-[1.5] text-ink-2 m-0 mb-3.5">{{ __('web.detail.legal_question') }} {{ __('web.detail.reports_free') }}</p>
|
||
<div class="flex flex-wrap gap-2">
|
||
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']) }}" class="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12.5px] font-semibold bg-white border border-bg-rule-strong text-ink hover:border-brand hover:text-brand transition-colors">
|
||
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M7 4v3.5M7 9.5v.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" /></svg>
|
||
{{ __('web.detail.report_content') }}
|
||
</a>
|
||
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'dsgvo']) }}" class="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12.5px] font-semibold bg-white border border-bg-rule-strong text-ink hover:border-brand hover:text-brand transition-colors">
|
||
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><path d="M7 1l5 2v4c0 3-2.2 5.2-5 6-2.8-.8-5-3-5-6V3z" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linejoin="round" /></svg>
|
||
{{ __('web.detail.remove_data') }}
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="border border-bg-rule-strong bg-bg-elev p-5">
|
||
<div class="eyebrow muted text-[10px] mb-2">{{ __('web.detail.manage_eyebrow') }}</div>
|
||
<p class="text-[13px] leading-[1.5] text-ink-2 m-0 mb-2">{{ __('web.detail.manage_question') }}</p>
|
||
@if ($claimRecipients->isNotEmpty())
|
||
<p class="text-[12px] text-ink-3 m-0 mb-1">{{ __('web.detail.manage_recipient') }}</p>
|
||
<ul class="m-0 mb-3.5 p-0 list-none font-mono text-[13px] text-ink">
|
||
@foreach ($claimRecipients as $recipient)
|
||
<li>{{ $recipient }}</li>
|
||
@endforeach
|
||
</ul>
|
||
<a href="{{ route('newsroom.claim', ['slug' => $company->slug]) }}" rel="nofollow" class="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12.5px] font-semibold bg-brand text-white hover:opacity-90 transition-opacity">
|
||
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><path d="M1 3h12v8H1zM1 3l6 4 6-4" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
|
||
{{ __('web.detail.manage_cta') }}
|
||
</a>
|
||
<p class="text-[11.5px] text-ink-3 m-0 mt-3">
|
||
{{ __('web.detail.manage_hint') }}
|
||
<a href="{{ $portalUrl }}/anmeldelink" class="underline underline-offset-2 hover:text-ink transition-colors">{{ __('web.detail.manage_other_address') }}</a>
|
||
</p>
|
||
@else
|
||
<a href="{{ $portalUrl }}/anmeldelink" class="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12.5px] font-semibold bg-brand text-white hover:opacity-90 transition-opacity">
|
||
<svg width="12" height="12" viewBox="0 0 14 14" class="flex-shrink-0" aria-hidden="true"><path d="M1 3h12v8H1zM1 3l6 4 6-4" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
|
||
{{ __('web.detail.manage_link') }}
|
||
</a>
|
||
<p class="text-[11.5px] text-ink-3 m-0 mt-3">{{ __('web.detail.manage_hint') }}</p>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Sidebar --}}
|
||
<aside class="flex flex-col gap-7">
|
||
{{-- Publisher --}}
|
||
@if ($companyName)
|
||
<div class="p-4 bg-white border border-bg-rule-strong">
|
||
<div class="eyebrow muted text-[9.5px] mb-3">{{ __('web.detail.published_by') }}</div>
|
||
<div class="flex gap-3 items-start mb-3">
|
||
<span class="w-11 h-11 bg-black text-white inline-flex items-center justify-center text-[20px] font-bold font-serif flex-shrink-0">{{ $companyInitial }}</span>
|
||
<div class="min-w-0">
|
||
<strong class="block font-serif text-[14px] font-semibold leading-[1.25] tracking-[-0.1px] mb-0.5">{{ $companyName }}</strong>
|
||
@if ($companyCountry)
|
||
<div class="text-[10.5px] text-ink-4 mt-1 font-mono">{{ $companyCountry }}</div>
|
||
@endif
|
||
</div>
|
||
</div>
|
||
|
||
@if ($publisherVerified)
|
||
<div class="flex items-center gap-2 px-2.5 py-2 mb-3 bg-verify-bg border border-verify-border text-verify-ink cursor-help"
|
||
title="{{ __('web.detail.verified_by', ['brand' => $brandLabel]) }}">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" class="text-verify-border flex-shrink-0" aria-hidden="true">
|
||
<path d="M12 1l2.5 3 3.8-.4-.5 3.8L21 9l-2.6 2.5L19 15l-3.8.4L12 19l-2.5-3.6L5.7 15 5.6 11.4 3 9l2.5-1.6L4.7 4 8.5 4z" fill="currentColor" />
|
||
<path d="M7.5 12l3 3 6-6" stroke="white" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round" />
|
||
</svg>
|
||
<span class="text-[11.5px] font-semibold">{{ __('web.detail.verified_publisher') }}</span>
|
||
</div>
|
||
@endif
|
||
|
||
<div class="pt-3 border-t border-bg-rule flex flex-col gap-2">
|
||
<a href="{{ $companyHref }}" class="px-2.5 py-2 text-center text-[11.5px] font-semibold bg-ink text-white hover:bg-black transition-colors cursor-pointer">{{ __('web.detail.to_newsroom') }} →</a>
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Teilen --}}
|
||
<div x-data="{
|
||
url: @js($shareUrl),
|
||
title: @js($release->title),
|
||
copied: false,
|
||
{{-- Share-Klicks als Ereignis loggen (ohne Personenbezug); in der Vorschau kein Tracking. --}}
|
||
trackUrl: @js($isPreview ? null : route('release.share', ['slug' => $release->slug])),
|
||
track(channel) {
|
||
if (!this.trackUrl) return;
|
||
fetch(this.trackUrl, {
|
||
method: 'POST',
|
||
keepalive: true,
|
||
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]')?.content ?? '' },
|
||
body: JSON.stringify({ channel })
|
||
}).catch(() => {});
|
||
},
|
||
copy() { navigator.clipboard?.writeText(this.url).then(() => { this.copied = true; this.track('copy'); setTimeout(() => this.copied = false, 2000); }); }
|
||
}">
|
||
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.share') }}</div>
|
||
<div class="grid grid-cols-2 gap-1.5">
|
||
<a :href="`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`" @click="track('linkedin')" target="_blank" rel="noopener" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M3 6h2.5v8H3V6zm1.25-3a1.25 1.25 0 110 2.5 1.25 1.25 0 010-2.5zM7 6h2.4v1.1c.4-.7 1.3-1.3 2.6-1.3 2.7 0 3 1.7 3 3.9V14h-2.5v-3.7c0-.9 0-2-1.3-2s-1.5 1-1.5 2V14H7V6z" fill="currentColor" /></svg>
|
||
LinkedIn
|
||
</a>
|
||
<a :href="`https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`" @click="track('x')" target="_blank" rel="noopener" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M2 2l5.5 7.2L2 14h1.6l4.7-3.7L11.5 14H14L8.2 6.4 13.6 2H12L7.5 5.5 4.5 2H2z" fill="currentColor" /></svg>
|
||
X
|
||
</a>
|
||
<a :href="`mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`" @click="track('email')" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="10" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M2 4l6 4 6-4" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
|
||
{{ __('web.detail.email') }}
|
||
</a>
|
||
<button type="button" @click="copy()" class="inline-flex items-center gap-[7px] px-2.5 py-2 text-[11.5px] font-medium bg-white border border-bg-rule-strong text-ink hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 9l4-4M5 4h2a3 3 0 010 6h-1M11 12H9a3 3 0 010-6h1" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linecap="round" /></svg>
|
||
<span x-text="copied ? @js(__('web.detail.copied')) : @js(__('web.detail.copy_link'))">{{ __('web.detail.copy_link') }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Schlagwörter --}}
|
||
@if ($keywords->isNotEmpty())
|
||
<div>
|
||
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.keywords') }}</div>
|
||
<div class="flex flex-wrap gap-[5px]">
|
||
@foreach ($keywords as $keyword)
|
||
<a href="{{ route('suche', ['q' => $keyword]) }}" class="px-2.5 py-1 text-[11px] font-medium bg-white border border-bg-rule-strong text-ink-2 hover:text-ink transition-colors cursor-pointer">{{ $keyword }}</a>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
@endif
|
||
|
||
{{-- Newsletter-Box bewusst entfernt: kommt nachgelagert mit echtem
|
||
Double-Opt-in (siehe dev/release/entwicklungsplan-release.md). --}}
|
||
|
||
{{-- Weiterführend --}}
|
||
<div>
|
||
<div class="eyebrow muted text-[9.5px] mb-2.5">{{ __('web.detail.further') }}</div>
|
||
<div class="flex flex-col border border-bg-rule-strong bg-white">
|
||
<button type="button" onclick="window.print()" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 hover:bg-bg-elev transition-colors cursor-pointer text-left">
|
||
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M2 4h10v8H2zM2 4V2h10v2" stroke="currentColor" stroke-width="1.3" fill="none" /></svg>
|
||
<span class="flex-1">{{ __('web.detail.print') }}</span>
|
||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||
</button>
|
||
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']) }}" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 border-t border-bg-rule hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><circle cx="7" cy="7" r="5.5" stroke="currentColor" stroke-width="1.3" fill="none" /><path d="M7 4v3.5M7 9.5v.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" /></svg>
|
||
<span class="flex-1">{{ __('web.detail.report_release') }}</span>
|
||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||
</a>
|
||
<a href="{{ route('legal-request.create', ['slug' => $release->slug, 'type' => 'dsgvo']) }}" class="flex items-center gap-2.5 px-3 py-2.5 text-[12px] text-ink-2 border-t border-bg-rule hover:bg-bg-elev transition-colors cursor-pointer">
|
||
<svg width="13" height="13" viewBox="0 0 14 14" class="text-ink-3 flex-shrink-0" aria-hidden="true"><path d="M7 1l5 2v4c0 3-2.2 5.2-5 6-2.8-.8-5-3-5-6V3z" stroke="currentColor" stroke-width="1.3" fill="none" stroke-linejoin="round" /></svg>
|
||
<span class="flex-1">{{ __('web.detail.privacy_rights') }}</span>
|
||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" class="text-ink-4" aria-hidden="true"><path d="M4 2l4 4-4 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Verwaltungszugang in der Seitenleiste (Roadmap 1b.4): derselbe
|
||
Hinweis wie in der Veröffentlichungsmail — wer nur die Leiste
|
||
liest, soll ihn nicht verpassen. --}}
|
||
<div class="border border-bg-rule-strong bg-bg-elev p-4">
|
||
<div class="eyebrow muted text-[9.5px] mb-2">{{ __('web.detail.manage_eyebrow') }}</div>
|
||
<p class="text-[12px] leading-[1.5] text-ink-2 m-0 mb-3">{{ __('web.detail.manage_sidebar_text') }}</p>
|
||
@if ($claimRecipients->isNotEmpty())
|
||
<a href="{{ route('newsroom.claim', ['slug' => $company->slug]) }}" rel="nofollow" class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-[12px] font-semibold bg-white border border-bg-rule-strong text-ink hover:border-brand hover:text-brand transition-colors">
|
||
{{ __('web.detail.manage_cta') }} →
|
||
</a>
|
||
@else
|
||
<a href="{{ $portalUrl }}/anmeldelink" class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-[12px] font-semibold bg-white border border-bg-rule-strong text-ink hover:border-brand hover:text-brand transition-colors">
|
||
{{ __('web.detail.manage_link_short') }} →
|
||
</a>
|
||
@endif
|
||
</div>
|
||
</aside>
|
||
</section>
|
||
|
||
{{-- Empfehlungen: zweispaltig nur, wenn beide Listen gefüllt sind —
|
||
eine einzelne Liste nutzt sonst die volle Layout-Breite. --}}
|
||
@if ($companyReleases->isNotEmpty() || $relatedReleases->isNotEmpty())
|
||
<section class="max-w-layout mx-auto px-4 sm:px-6 lg:px-8 mt-14">
|
||
<div @class([
|
||
'grid grid-cols-1 gap-10',
|
||
'md:grid-cols-2' => $companyReleases->isNotEmpty() && $relatedReleases->isNotEmpty(),
|
||
])>
|
||
@if ($companyReleases->isNotEmpty())
|
||
<div>
|
||
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">{{ __('web.detail.more_from', ['company' => $companyName]) }}</h3>
|
||
<div class="text-[11.5px] text-ink-3 mb-3.5">{{ __('web.detail.more_from_sub') }}</div>
|
||
<hr class="rule-strong mb-1" />
|
||
@foreach ($companyReleases as $companyRelease)
|
||
<x-web.feed-item :release="$companyRelease" />
|
||
@endforeach
|
||
<a href="{{ $companyHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">{{ __('web.detail.all_from', ['company' => $companyName]) }} →</a>
|
||
</div>
|
||
@endif
|
||
|
||
@if ($relatedReleases->isNotEmpty())
|
||
<div>
|
||
<h3 class="font-serif text-[20px] font-semibold m-0 mb-1 tracking-[-0.2px]">{{ __('web.detail.related') }}</h3>
|
||
<div class="text-[11.5px] text-ink-3 mb-3.5">{{ __('web.detail.related_sub') }}</div>
|
||
<hr class="rule-strong mb-1" />
|
||
@foreach ($relatedReleases as $relatedRelease)
|
||
<x-web.feed-item :release="$relatedRelease" />
|
||
@endforeach
|
||
@if ($categoryName)
|
||
<a href="{{ $categoryHref }}" class="block mt-3.5 text-[12.5px] font-semibold text-brand hover:text-brand-deep transition-colors cursor-pointer">{{ __('web.detail.all_from_category', ['category' => $categoryName]) }} →</a>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</div>
|
||
</section>
|
||
@endif
|
||
</article>
|
||
</main>
|
||
|
||
<x-web.site-footer />
|
||
@endsection
|