diff --git a/app/Services/Web/CategoryPageData.php b/app/Services/Web/CategoryPageData.php index 2d641832..3d45690a 100644 --- a/app/Services/Web/CategoryPageData.php +++ b/app/Services/Web/CategoryPageData.php @@ -95,18 +95,36 @@ class CategoryPageData 'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1), ]; - $leadRelease = $base()->with($with)->orderByDesc('published_at')->first(); + // Gebuchte Boosts (Decision-Update §2.2): geboostete PMs der Branche + // besetzen Lead- und Top-Plätze vor der Chronologie; die Views + // markieren sie über `boostedIds`. + $boostedReleases = $base() + ->boosted() + ->with($with) + ->orderByDesc('published_at') + ->limit(3) + ->get(); + $boostedIds = $boostedReleases->modelKeys(); + + $leadRelease = $boostedReleases->first() + ?? $base()->with($with)->orderByDesc('published_at')->first(); // Ohne mediumText-Spalte `text`: Top-/Feed-Listen rendern nur // Titel/Meta, der Volltext wäre unnötiger I/O. $listColumns = ['id', 'portal', 'language', 'slug', 'title', 'category_id', 'company_id', 'published_at', 'content_score']; - $topReleases = $base() - ->with($with) - ->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey())) - ->orderByDesc('published_at') - ->limit(2) - ->get($listColumns); + $topBoosted = $boostedReleases->slice(1)->take(2)->values(); + $topChronoSlots = 2 - $topBoosted->count(); + $topReleases = $topChronoSlots > 0 + ? $base() + ->with($with) + ->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey())) + ->when($boostedIds !== [], fn (Builder $query) => $query->whereNotIn('id', $boostedIds)) + ->orderByDesc('published_at') + ->limit($topChronoSlots) + ->get($listColumns) + : collect(); + $topReleases = $topBoosted->concat($topReleases)->values(); $excludeIds = collect([$leadRelease?->getKey()])->merge($topReleases->modelKeys())->filter()->all(); @@ -249,6 +267,7 @@ class CategoryPageData 'leadRelease' => $leadRelease, 'topReleases' => $topReleases, 'feedReleases' => $feedReleases, + 'boostedIds' => $boostedIds, 'mostReadReleases' => $mostReadReleases, 'categoryStats' => $stats, 'topNewsrooms' => $topNewsrooms, diff --git a/app/Services/Web/HomePageData.php b/app/Services/Web/HomePageData.php index 24d76b54..ba9af307 100644 --- a/app/Services/Web/HomePageData.php +++ b/app/Services/Web/HomePageData.php @@ -72,19 +72,37 @@ class HomePageData ->oldest('published_at') ->value('published_at'); - $leadRelease = $publishedQuery() + // Gebuchte Boosts (Decision-Update §2.2): geboostete PMs besetzen die + // Top-Positionen vor der Chronologie — die jüngste als Lead, weitere + // in der Seitenleiste. Die Views markieren sie über `boostedIds`. + $boostedReleases = $publishedQuery() + ->boosted() ->with($with) ->orderByDesc('published_at') - ->first(); + ->limit(5) + ->get(); + $boostedIds = $boostedReleases->modelKeys(); + + $leadRelease = $boostedReleases->first() + ?? $publishedQuery() + ->with($with) + ->orderByDesc('published_at') + ->first(); // Ohne mediumText-Spalte `text`: die Seitenleisten-Teaser rendern nur // Titel/Meta, der Volltext wäre unnötiger I/O. - $sideReleases = $publishedQuery() - ->with($with) - ->when($leadRelease, fn (Builder $query) => $query->where('id', '!=', $leadRelease->id)) - ->orderByDesc('published_at') - ->limit(4) - ->get(['id', 'portal', 'language', 'slug', 'title', 'category_id', 'company_id', 'published_at', 'content_score']); + $sideBoosted = $boostedReleases->slice(1)->take(4)->values(); + $sideChronoSlots = 4 - $sideBoosted->count(); + $sideReleases = $sideChronoSlots > 0 + ? $publishedQuery() + ->with($with) + ->when($leadRelease, fn (Builder $query) => $query->where('id', '!=', $leadRelease->id)) + ->when($boostedIds !== [], fn (Builder $query) => $query->whereNotIn('id', $boostedIds)) + ->orderByDesc('published_at') + ->limit($sideChronoSlots) + ->get(['id', 'portal', 'language', 'slug', 'title', 'category_id', 'company_id', 'published_at', 'content_score']) + : collect(); + $sideReleases = $sideBoosted->concat($sideReleases)->values(); $mostReadReleases = $publishedQuery() ->orderByDesc('hits') @@ -244,6 +262,7 @@ class HomePageData return [ 'leadRelease' => $leadRelease, 'sideReleases' => $sideReleases, + 'boostedIds' => $boostedIds, 'mostReadReleases' => $mostReadReleases, 'activeNewsrooms' => $activeNewsrooms, 'industryIndex' => $industryIndex, diff --git a/lang/de/web.php b/lang/de/web.php index 503bcf07..05574812 100644 --- a/lang/de/web.php +++ b/lang/de/web.php @@ -23,6 +23,7 @@ return [ 'read_time' => ':minutes Min. Lesezeit', 'minutes_short' => ':minutes Min.', 'recommendation' => 'Empfehlung', + 'boosted_placement' => 'Top-Platzierung', ], 'countries' => [ diff --git a/lang/en/web.php b/lang/en/web.php index bf1c78a8..87305edd 100644 --- a/lang/en/web.php +++ b/lang/en/web.php @@ -18,6 +18,7 @@ return [ 'read_time' => ':minutes min read', 'minutes_short' => ':minutes min', 'recommendation' => 'Recommended', + 'boosted_placement' => 'Featured', ], 'countries' => [ diff --git a/resources/views/components/web/focus-hero.blade.php b/resources/views/components/web/focus-hero.blade.php index 6edd48b2..6e204c76 100644 --- a/resources/views/components/web/focus-hero.blade.php +++ b/resources/views/components/web/focus-hero.blade.php @@ -1,10 +1,12 @@ @props([ 'leadRelease' => null, 'sideReleases' => [], + 'boostedIds' => [], ]) @php $sideReleases = collect($sideReleases)->take(4)->values(); + $boostedIds = collect($boostedIds)->all(); if ($leadRelease) { $leadCategoryName = $leadRelease->localizedCategoryName(); @@ -50,6 +52,9 @@