PM-Vorschau: echtes Portal-Design, Direkt-Button und Portal-Switch

Der Vorschau-Link (/pm-vorschau/{token}) renderte eine schlichte
Standalone-Seite, die dem spaeteren Erscheinungsbild nicht entsprach.
Jetzt rendert er die echte Detailseite (web.release-detail) im Theme
des Zielportals:

- Controller wendet Domain-/Theme-Konfig des Zielportals an (Sprache
  der PM, Links zeigen auf die Portal-Domain), kein Hit-Zaehler
- Gelbes Vorschau-Banner mit Status und Link-Gueltigkeit; noindex per
  Meta-Tag + X-Robots-Tag-Header, Canonical entfaellt bei noindex
  (neue robots-Section im Web-Layout)
- Teilen-Buttons teilen den Vorschau-Link selbst (oeffentliche URL
  existiert vor Veroeffentlichung noch nicht)
- PMs fuer beide Portale: ?portal=... waehlt die Ansicht, Umschalter
  im Banner; im Kundenpanel je Portal ein Vorschau-Button, sonst ein
  einzelner "Vorschau oeffnen"-Button (Link-Feld jetzt kopierbar)
- Alte press-release-preview-View entfernt

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-07-07 13:11:57 +00:00
parent 9a95a6937d
commit 090a893cc6
8 changed files with 306 additions and 149 deletions

View file

@ -362,8 +362,26 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<span class="section-eyebrow">{{ __('Öffentlicher Vorschau-Link erstellt') }}</span>
<span class="badge ok dot">{{ __('gültig bis :date', ['date' => $shareExpiresAt]) }}</span>
</div>
<div class="p-5">
<flux:input readonly :value="$shareUrl" />
<div class="p-5 space-y-3">
<flux:input readonly copyable :value="$shareUrl" />
<div class="flex flex-wrap items-center gap-2">
@if ($pr->portal === \App\Enums\Portal::Both)
{{-- PM erscheint auf beiden Portalen: Vorschau je Portal öffnen --}}
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}?portal=presseecho" target="_blank" rel="noopener">
{{ __('Vorschau presseecho') }}
</flux:button>
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}?portal=businessportal24" target="_blank" rel="noopener">
{{ __('Vorschau businessportal24') }}
</flux:button>
@else
<flux:button size="sm" variant="primary" icon="arrow-top-right-on-square"
href="{{ $shareUrl }}" target="_blank" rel="noopener">
{{ __('Vorschau öffnen') }}
</flux:button>
@endif
</div>
</div>
</article>
@endif

View file

@ -1,136 +0,0 @@
<!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="robots" content="noindex,nofollow">
<title>{{ $pressRelease->title }} – Vorschau</title>
<style>
:root {
--bg: #f8fafc;
--surface: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--accent: #2563eb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #f1f5f9;
--muted: #94a3b8;
--accent: #60a5fa;
}
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.preview-banner {
background: #fef3c7;
color: #78350f;
padding: 12px 16px;
text-align: center;
font-size: 14px;
border-bottom: 1px solid #fde68a;
}
@media (prefers-color-scheme: dark) {
.preview-banner {
background: #78350f;
color: #fef3c7;
border-bottom-color: #fcd34d;
}
}
.container {
max-width: 800px;
margin: 32px auto;
padding: 32px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
h1 { margin: 0 0 8px; font-size: 28px; line-height: 1.25; }
.meta {
color: var(--muted);
font-size: 14px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.body { white-space: pre-wrap; }
.images {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 12px;
margin: 24px 0;
}
.images img {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--border);
}
.footer {
margin-top: 32px;
padding-top: 16px;
border-top: 1px solid var(--border);
color: var(--muted);
font-size: 13px;
}
.keywords { font-size: 13px; color: var(--muted); margin-top: 16px; }
a { color: var(--accent); }
</style>
</head>
<body>
<div class="preview-banner">
{{ __('Private Vorschau – Link gültig bis :date', ['date' => $expiresAt?->format('d.m.Y H:i')]) }}
</div>
<main class="container">
<h1>{{ $pressRelease->title }}</h1>
<div class="meta">
@if($pressRelease->company)
<strong>{{ $pressRelease->company->name }}</strong>
·
@endif
@if($pressRelease->category)
{{ $pressRelease->category->translations->firstWhere('locale', $pressRelease->language)?->name ?? '–' }}
·
@endif
{{ $pressRelease->created_at?->format('d.m.Y') }}
</div>
@if($pressRelease->images->isNotEmpty())
<div class="images">
@foreach($pressRelease->images as $image)
@if($image->variantUrl('medium') ?? $image->url())
<img src="{{ $image->variantUrl('medium') ?? $image->url() }}" alt="{{ $image->title ?? $pressRelease->title }}" loading="lazy">
@endif
@endforeach
</div>
@endif
<div class="body">{!! $pressRelease->renderedText() !!}</div>
@if($pressRelease->keywords)
<p class="keywords"><strong>{{ __('Stichwörter') }}:</strong> {{ $pressRelease->keywords }}</p>
@endif
<div class="footer">
{{ __('Status') }}: {{ $pressRelease->status->label() }}
@if($pressRelease->backlink_url)
· <a href="{{ $pressRelease->backlink_url }}" rel="nofollow noopener" target="_blank">{{ $pressRelease->backlink_url }}</a>
@endif
</div>
</main>
</body>
</html>

View file

@ -3,6 +3,12 @@
@php
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())
@ -17,7 +23,9 @@
$parentSlug = $parentTranslation?->slug;
$parentHref = $parentSlug ? route('kategorie', ['slug' => $parentSlug]) : route('web.home');
$publishedAt = $release->published_at;
// 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();
@ -55,7 +63,9 @@
$publisherVerified = (bool) ($company?->is_active && filled($company?->website));
$isRecommended = $release->content_score !== null && $release->content_score >= 80;
$shareUrl = route('release.detail', ['slug' => $release->slug]);
// 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';
@endphp
@ -65,7 +75,11 @@
@if ($imageUrl)
@section('og_image', $imageUrl)
@endif
@section('canonical', $shareUrl)
@if ($isPreview)
@section('robots', 'noindex,nofollow')
@else
@section('canonical', $shareUrl)
@endif
@section('content')
{{-- Lese-Fortschrittsbalken --}}
@ -78,6 +92,44 @@
<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 --}}