From 241d67c62a6fa3c53e5b910e3b3d938ba8c9ab43 Mon Sep 17 00:00:00 2001 From: Kevin Adametz Date: Fri, 19 Jun 2026 12:14:38 +0000 Subject: [PATCH] Optimierung 4.3/4.4 (Home): Page-Data-Service + Invokable Controller Startseiten-Datenaufbereitung ($webHomeData) in HomePageData; die Edition-Home- Route nutzt Web\HomeController (pressekonto-Redirect + Portal->View-Mapping). Damit ist routes/web.php von ~1080 auf ~230 Zeilen geschrumpft; alle datengetriebenen Seiten laufen jetzt ueber schlanke Invokable Controller + Page-Data-Services. Verhalten unveraendert, 92 Web-Tests gruen. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Http/Controllers/Web/HomeController.php | 29 +++ app/Services/Web/HomePageData.php | 245 +++++++++++++++++++ routes/web.php | 246 +------------------- 3 files changed, 277 insertions(+), 243 deletions(-) create mode 100644 app/Http/Controllers/Web/HomeController.php create mode 100644 app/Services/Web/HomePageData.php diff --git a/app/Http/Controllers/Web/HomeController.php b/app/Http/Controllers/Web/HomeController.php new file mode 100644 index 00000000..195b85b4 --- /dev/null +++ b/app/Http/Controllers/Web/HomeController.php @@ -0,0 +1,29 @@ +getHost(), 'pressekonto')) { + return redirect(request()->getSchemeAndHttpHost().'/', 301); + } + + $context = WebPortalContext::fromRequest(); + + $view = $context->portal === Portal::Presseecho + ? 'web.presseecho' + : 'web.businessportal24'; + + return view($view, $pageData->build($context->portal, $context->language)); + } +} diff --git a/app/Services/Web/HomePageData.php b/app/Services/Web/HomePageData.php new file mode 100644 index 00000000..2f0c1376 --- /dev/null +++ b/app/Services/Web/HomePageData.php @@ -0,0 +1,245 @@ + + */ + public function build(Portal $portal, string $language = 'de'): array + { + $portalValues = [$portal->value, Portal::Both->value]; + + // Aktualitäts-Stichtag an den jüngsten Datensatz koppeln (historisches Archiv). + $referenceNow = EditorialClock::reference($portalValues, $language); + $recentSince = $referenceNow->copy()->subDays(7); + $previousSince = $referenceNow->copy()->subDays(14); + + $publishedQuery = static fn (): Builder => PressRelease::query() + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive(); + + $with = [ + 'company', + 'category.translations' => fn ($query) => $query->where('locale', $language), + 'images' => fn ($query) => $query + ->orderByDesc('is_preview') + ->orderBy('sort_order') + ->limit(1), + ]; + + $oldestPublishedAt = $publishedQuery() + ->oldest('published_at') + ->value('published_at'); + + $leadRelease = $publishedQuery() + ->with($with) + ->orderByDesc('published_at') + ->first(); + + $sideReleases = $publishedQuery() + ->with($with) + ->when($leadRelease, fn (Builder $query) => $query->where('id', '!=', $leadRelease->id)) + ->orderByDesc('published_at') + ->limit(4) + ->get(); + + $mostReadReleases = $publishedQuery() + ->orderByDesc('hits') + ->orderByDesc('published_at') + ->limit(4) + ->get(['id', 'slug', 'title', 'hits', 'portal', 'language']); + + $activeNewsrooms = Company::query() + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereHas('pressReleases', function (Builder $query) use ($portalValues, $recentSince, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $recentSince); + }) + ->withCount([ + 'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $recentSince); + }, + 'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->whereDate('published_at', $referenceNow->toDateString()); + }, + ]) + ->orderByDesc('today_releases_count') + ->orderByDesc('recent_releases_count') + ->limit(6) + ->get() + ->map(fn (Company $company): array => [ + 'name' => $company->name, + 'slug' => $company->slug, + 'initial' => mb_strtoupper(mb_substr((string) $company->name, 0, 1)), + 'count' => (int) $company->recent_releases_count, + 'today' => (int) $company->today_releases_count > 0, + ]); + + $industryIndex = Category::query() + ->with(['translations' => fn ($query) => $query->where('locale', $language)]) + ->withCount([ + 'pressReleases as recent_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $recentSince); + }, + 'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $previousSince) + ->where('published_at', '<', $recentSince); + }, + ]) + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereNull('parent_id') + ->orderByDesc('recent_count') + ->limit(7) + ->get() + ->map(function (Category $category): ?array { + $translation = $category->translations->first(); + + if (! $translation) { + return null; + } + + return [ + 'name' => $translation->name, + 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), + 'count' => (int) ($category->recent_count ?? 0), + 'delta' => (int) ($category->recent_count ?? 0) - (int) ($category->previous_count ?? 0), + ]; + }) + ->filter() + ->values(); + + // "Heute im Fokus · Branche": aktivste Top-Rubrik im Stichtag-Fenster mit echten Meldungen. + $spotlightCategory = Category::query() + ->with(['translations' => fn ($query) => $query->where('locale', $language)]) + ->withCount([ + 'pressReleases as window_count' => function (Builder $query) use ($portalValues, $recentSince, $referenceNow, $language): void { + $query + ->forPortalValues($portalValues) + ->forLanguage($language) + ->published() + ->where('published_at', '>=', $recentSince) + ->where('published_at', '<=', $referenceNow); + }, + ]) + ->whereIn('portal', $portalValues) + ->where('is_active', true) + ->whereNull('parent_id') + ->orderByDesc('window_count') + ->first(); + + $spotlight = null; + + if ($spotlightCategory && $spotlightTranslation = $spotlightCategory->translations->first()) { + $spotlightBase = static fn (): Builder => PressRelease::query() + ->forPortalValues($portalValues) + ->forLanguage($language) + ->visibleInArchive() + ->where('category_id', $spotlightCategory->id); + + $spotlightReleases = $spotlightBase() + ->with(['company']) + ->orderByDesc('published_at') + ->limit(3) + ->get() + ->map(static function (PressRelease $release) use ($spotlightTranslation): array { + $words = str_word_count(strip_tags((string) $release->text)); + $publishedAt = $release->published_at; + + return [ + 'time' => $publishedAt?->format('H:i') ?? '', + 'date' => $publishedAt?->translatedFormat('j. M') ?? '', + 'category' => $spotlightTranslation->name, + 'title' => $release->title, + 'company' => $release->company?->name ?? '', + 'city' => null, + 'minutes' => max(1, (int) ceil($words / 200)), + 'href' => $release->slug ? route('release.detail', ['slug' => $release->slug]) : '#', + ]; + }); + + if ($spotlightReleases->isNotEmpty()) { + $windowCount = (int) ($spotlightCategory->window_count ?? 0); + $archiveCount = $spotlightBase()->count(); + $activeCompanies = (clone $spotlightBase()) + ->where('published_at', '>=', $recentSince) + ->where('published_at', '<=', $referenceNow) + ->distinct() + ->count('company_id'); + + $spotlight = [ + 'industry' => $spotlightTranslation->name, + 'stats' => [ + ['label' => 'Meldungen (7 Tage)', 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => 'Stand '.$referenceNow->translatedFormat('j. M Y')], + ['label' => 'Aktive Unternehmen', 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => 'in dieser Branche'], + ['label' => 'Im Archiv', 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => 'Meldungen gesamt'], + ], + 'releases' => $spotlightReleases->all(), + ]; + } + } + + // Ad-Hoc-Ticker aus den jüngsten echten Pressemitteilungen. + $tickerItems = $publishedQuery() + ->orderByDesc('published_at') + ->limit(8) + ->get(['id', 'title', 'published_at']) + ->map(static fn (PressRelease $release): array => [ + 'time' => $release->published_at?->format('H:i') ?? '', + 'text' => Str::limit((string) $release->title, 70), + ]) + ->all(); + + return [ + 'leadRelease' => $leadRelease, + 'sideReleases' => $sideReleases, + 'mostReadReleases' => $mostReadReleases, + 'activeNewsrooms' => $activeNewsrooms, + 'industryIndex' => $industryIndex, + 'spotlight' => $spotlight, + 'tickerItems' => $tickerItems, + 'homeStats' => [ + 'publishedCount' => $publishedQuery()->count(), + 'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(), + 'archiveSince' => $oldestPublishedAt ? (int) Carbon::parse($oldestPublishedAt)->format('Y') : null, + ], + ]; + } +} diff --git a/routes/web.php b/routes/web.php index e9e149be..4b6f3892 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Enums\Portal; use App\Http\Controllers\PressReleasePreviewController; use App\Http\Controllers\Web\CategoryController; +use App\Http\Controllers\Web\HomeController; use App\Http\Controllers\Web\NewsroomDetailController; use App\Http\Controllers\Web\NewsroomsController; use App\Http\Controllers\Web\PreiseController; @@ -13,14 +14,9 @@ use App\Http\Middleware\SetEdition; use App\Models\Category; use App\Models\Company; use App\Models\Plan; -use App\Models\PressRelease; use App\Scopes\PortalScope; use App\Support\DomainAssetContext; -use App\Support\EditorialClock; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Route; -use Illuminate\Support\Str; Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class) ->where('token', '[A-Za-z0-9]{40,}') @@ -33,227 +29,6 @@ Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class) // damit Bare-Root- und Edition-Home-Route unverändert per Closure aufrufen können. $applyWebDomainConfig = static fn (string $domainKey): array => DomainAssetContext::applyWebDomainConfig($domainKey); -$webHomeData = static function (Portal $primaryPortal, string $language = 'de'): array { - $portalValues = [$primaryPortal->value, Portal::Both->value]; - - // Aktualitäts-Stichtag an den jüngsten Datensatz koppeln (historisches Archiv). - $referenceNow = EditorialClock::reference($portalValues, $language); - $recentSince = $referenceNow->copy()->subDays(7); - $previousSince = $referenceNow->copy()->subDays(14); - - $publishedQuery = static fn (): Builder => PressRelease::query() - ->forPortalValues($portalValues) - ->forLanguage($language) - ->visibleInArchive(); - - $with = [ - 'company', - 'category.translations' => fn ($query) => $query->where('locale', $language), - 'images' => fn ($query) => $query - ->orderByDesc('is_preview') - ->orderBy('sort_order') - ->limit(1), - ]; - - $oldestPublishedAt = $publishedQuery() - ->oldest('published_at') - ->value('published_at'); - - $leadRelease = $publishedQuery() - ->with($with) - ->orderByDesc('published_at') - ->first(); - - $sideReleases = $publishedQuery() - ->with($with) - ->when($leadRelease, fn (Builder $query) => $query->where('id', '!=', $leadRelease->id)) - ->orderByDesc('published_at') - ->limit(4) - ->get(); - - $mostReadReleases = $publishedQuery() - ->orderByDesc('hits') - ->orderByDesc('published_at') - ->limit(4) - ->get(['id', 'slug', 'title', 'hits', 'portal', 'language']); - - $activeNewsrooms = Company::query() - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereHas('pressReleases', function (Builder $query) use ($portalValues, $recentSince, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->where('published_at', '>=', $recentSince); - }) - ->withCount([ - 'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->where('published_at', '>=', $recentSince); - }, - 'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->whereDate('published_at', $referenceNow->toDateString()); - }, - ]) - ->orderByDesc('today_releases_count') - ->orderByDesc('recent_releases_count') - ->limit(6) - ->get() - ->map(fn (Company $company): array => [ - 'name' => $company->name, - 'slug' => $company->slug, - 'initial' => mb_strtoupper(mb_substr((string) $company->name, 0, 1)), - 'count' => (int) $company->recent_releases_count, - 'today' => (int) $company->today_releases_count > 0, - ]); - - $industryIndex = Category::query() - ->with(['translations' => fn ($query) => $query->where('locale', $language)]) - ->withCount([ - 'pressReleases as recent_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->where('published_at', '>=', $recentSince); - }, - 'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->where('published_at', '>=', $previousSince) - ->where('published_at', '<', $recentSince); - }, - ]) - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereNull('parent_id') - ->orderByDesc('recent_count') - ->limit(7) - ->get() - ->map(function (Category $category): ?array { - $translation = $category->translations->first(); - - if (! $translation) { - return null; - } - - return [ - 'name' => $translation->name, - 'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'), - 'count' => (int) ($category->recent_count ?? 0), - 'delta' => (int) ($category->recent_count ?? 0) - (int) ($category->previous_count ?? 0), - ]; - }) - ->filter() - ->values(); - - // "Heute im Fokus · Branche": aktivste Top-Rubrik im Stichtag-Fenster mit echten Meldungen. - $spotlightCategory = Category::query() - ->with(['translations' => fn ($query) => $query->where('locale', $language)]) - ->withCount([ - 'pressReleases as window_count' => function (Builder $query) use ($portalValues, $recentSince, $referenceNow, $language): void { - $query - ->forPortalValues($portalValues) - ->forLanguage($language) - ->published() - ->where('published_at', '>=', $recentSince) - ->where('published_at', '<=', $referenceNow); - }, - ]) - ->whereIn('portal', $portalValues) - ->where('is_active', true) - ->whereNull('parent_id') - ->orderByDesc('window_count') - ->first(); - - $spotlight = null; - - if ($spotlightCategory && $spotlightTranslation = $spotlightCategory->translations->first()) { - $spotlightBase = static fn (): Builder => PressRelease::query() - ->forPortalValues($portalValues) - ->forLanguage($language) - ->visibleInArchive() - ->where('category_id', $spotlightCategory->id); - - $spotlightReleases = $spotlightBase() - ->with(['company']) - ->orderByDesc('published_at') - ->limit(3) - ->get() - ->map(static function (PressRelease $release) use ($spotlightTranslation): array { - $words = str_word_count(strip_tags((string) $release->text)); - $publishedAt = $release->published_at; - - return [ - 'time' => $publishedAt?->format('H:i') ?? '', - 'date' => $publishedAt?->translatedFormat('j. M') ?? '', - 'category' => $spotlightTranslation->name, - 'title' => $release->title, - 'company' => $release->company?->name ?? '', - 'city' => null, - 'minutes' => max(1, (int) ceil($words / 200)), - 'href' => $release->slug ? route('release.detail', ['slug' => $release->slug]) : '#', - ]; - }); - - if ($spotlightReleases->isNotEmpty()) { - $windowCount = (int) ($spotlightCategory->window_count ?? 0); - $archiveCount = $spotlightBase()->count(); - $activeCompanies = (clone $spotlightBase()) - ->where('published_at', '>=', $recentSince) - ->where('published_at', '<=', $referenceNow) - ->distinct() - ->count('company_id'); - - $spotlight = [ - 'industry' => $spotlightTranslation->name, - 'stats' => [ - ['label' => 'Meldungen (7 Tage)', 'value' => number_format($windowCount, 0, ',', '.'), 'sub' => 'Stand '.$referenceNow->translatedFormat('j. M Y')], - ['label' => 'Aktive Unternehmen', 'value' => number_format($activeCompanies, 0, ',', '.'), 'sub' => 'in dieser Branche'], - ['label' => 'Im Archiv', 'value' => number_format($archiveCount, 0, ',', '.'), 'sub' => 'Meldungen gesamt'], - ], - 'releases' => $spotlightReleases->all(), - ]; - } - } - - // Ad-Hoc-Ticker aus den jüngsten echten Pressemitteilungen. - $tickerItems = $publishedQuery() - ->orderByDesc('published_at') - ->limit(8) - ->get(['id', 'title', 'published_at']) - ->map(static fn (PressRelease $release): array => [ - 'time' => $release->published_at?->format('H:i') ?? '', - 'text' => Str::limit((string) $release->title, 70), - ]) - ->all(); - - return [ - 'leadRelease' => $leadRelease, - 'sideReleases' => $sideReleases, - 'mostReadReleases' => $mostReadReleases, - 'activeNewsrooms' => $activeNewsrooms, - 'industryIndex' => $industryIndex, - 'spotlight' => $spotlight, - 'tickerItems' => $tickerItems, - 'homeStats' => [ - 'publishedCount' => $publishedQuery()->count(), - 'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(), - 'archiveSince' => $oldestPublishedAt ? (int) Carbon::parse($oldestPublishedAt)->format('Y') : null, - ], - ]; -}; - // --------------------------------------------------------------------------- // Bare Root: Publisher-Hub (pressekonto) oder Redirect auf die Default-Ausgabe. // --------------------------------------------------------------------------- @@ -397,24 +172,9 @@ Route::get('{path}', function (string $path) use ($htmlRedirect) { // --------------------------------------------------------------------------- // Öffentliche Inhalte je Ausgabe (Sprache): /de/... und /en/... // --------------------------------------------------------------------------- -Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () use ($applyWebDomainConfig, $webHomeData) { +Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () { // Startseite je Ausgabe - Route::get('/', function (string $edition) use ($applyWebDomainConfig, $webHomeData) { - $domain = request()->getHost(); - - if (str_contains($domain, 'pressekonto')) { - return redirect(request()->getSchemeAndHttpHost().'/', 301); - } - - [$domainKey, $portal, $view] = match (true) { - str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho, 'web.presseecho'], - default => ['businessportal24', Portal::Businessportal24, 'web.businessportal24'], - }; - - $applyWebDomainConfig($domainKey); - - return view($view, $webHomeData($portal, app()->getLocale())); - })->name('web.home'); + Route::get('/', HomeController::class)->name('web.home'); // Kategorie / Branchenseite (z.B. /de/category/energie-klima) Route::get('category/{slug}', CategoryController::class)->name('kategorie');