web: Strukturierte Daten (JSON-LD) und SEO-Metas fürs öffentliche Frontend
Ergänzt die bislang fehlenden strukturierten Daten auf den Presse-Seiten: - PM-Detailseite: NewsArticle + BreadcrumbList als JSON-LD sowie article:published_time/-section/-tag-Metas und og:image:alt — nur auf der öffentlichen Seite, die noindex-Vorschau bleibt ohne Markup. - Kategorie-Seiten: BreadcrumbList plus hreflang de/en über die lokalisierten Slugs (CategoryPageData liefert dafür neu categoryAlternateSlugs, mit View-Fallback für gecachte Alt-Daten). - Newsroom: Organization-Markup (Logo, Website als sameAs, Boilerplate). - Portal-Homepages: WebSite mit SearchAction (Sitelinks-Searchbox) und Organization. - Layout: og:locale und twitter:image ergänzt; neue Sections meta_extra und structured_data. Neue Komponente x-web.structured-data rendert JSON-LD mit JSON_HEX_TAG, damit "</script>" in Titeln/Teasern das Script-Tag nicht aufbrechen kann. JSON-LD-URLs werden wie der Canonical aus der Portal-Basis gebaut (unabhängig von forceRootUrl), damit Host und Canonical übereinstimmen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
c70e1754e0
commit
379688f4ff
9 changed files with 346 additions and 0 deletions
|
|
@ -83,6 +83,63 @@
|
|||
// 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';
|
||||
|
||||
// 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 = rtrim($domainUrl ?? config('app.url'), '/');
|
||||
|
||||
// dateModified darf laut Google nicht vor datePublished liegen
|
||||
// (updated_at kann bei terminierten PMs älter sein als published_at).
|
||||
$modifiedAt = $release->updated_at?->greaterThan($publishedAt ?? $release->updated_at)
|
||||
? $release->updated_at
|
||||
: $publishedAt;
|
||||
|
||||
// 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' => $shareUrl],
|
||||
'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' => $companyName ? array_filter([
|
||||
'@type' => 'Organization',
|
||||
'name' => $companyName,
|
||||
'url' => $company?->slug ? $portalBase.'/'.app()->getLocale().'/newsroom/'.$company->slug : null,
|
||||
]) : null,
|
||||
'publisher' => ['@type' => 'Organization', 'name' => $brandLabel, 'url' => $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)
|
||||
|
|
@ -95,6 +152,28 @@
|
|||
@section('robots', 'noindex,nofollow')
|
||||
@else
|
||||
@section('canonical', $shareUrl)
|
||||
|
||||
@section('meta_extra')
|
||||
@if ($publishedAt)
|
||||
<meta property="article:published_time" content="{{ $publishedAt->toIso8601String() }}">
|
||||
@endif
|
||||
@if ($release->updated_at?->greaterThan($publishedAt ?? $release->updated_at))
|
||||
<meta property="article:modified_time" content="{{ $release->updated_at->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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue