Öffentliche Seiten auf gemeinsames Editorial-Design (x-web.site-header/-footer,
Design-Tokens) und Ausgaben-Präfix /{edition}/ (de|en) umgestellt.
- Routing: neue Middleware SetEdition (Locale + URL::defaults), /{edition}-Gruppe
in routes/web.php, Root-Redirect auf /de, 301 für Legacy-.html-URLs,
Baseline-Default in AppServiceProvider.
- Neue URL-Schemata: /{edition}/press-release/{slug}, /{edition}/category/{slug}.
- Ausgabe = Sprache: DE/EN-Umschalter (Region/CH/AT entfernt); EditorialClock
und Livewire-Komponenten sprachdynamisch.
- Detail-, Kategorie- und Veröffentlichen-Seite mit echten Daten neu aufgebaut.
- Suche aktiviert: Volt-Komponente livewire/web/search (Titel/Text/Keywords +
Firma + Rubrik, Filter, Sortierung, Pagination, URL-Parameter q/category/sort).
- Rubriken-Navigation statt Übersichtsseite: Helper CategoryNavigation;
web/kategorien.blade.php + Route entfernt (Legacy-301).
- Tests: Edition-Routing, Kategorie-Seite/-Navigation, Detail, Veröffentlichen,
Suche, EditorialClock. Doku in "Echte öffentliche Unterseiten.md" aktualisiert.
Co-authored-by: Cursor <cursoragent@cursor.com>
808 lines
33 KiB
PHP
808 lines
33 KiB
PHP
<?php
|
|
|
|
use App\Enums\Portal;
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Http\Controllers\PressReleasePreviewController;
|
|
use App\Http\Middleware\SetEdition;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
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\Facades\View;
|
|
use Illuminate\Support\Str;
|
|
|
|
Route::get('/pm-vorschau/{token}', PressReleasePreviewController::class)
|
|
->where('token', '[A-Za-z0-9]{40,}')
|
|
->name('press-releases.preview');
|
|
|
|
// Gemeinsame Web-Routes für alle Landingpages
|
|
// Jede Landing-Page hat das gleiche Gerüst, aber unterschiedliches Styling
|
|
|
|
$applyWebDomainConfig = static function (string $domainKey): array {
|
|
$domainConfig = config("domains.domains.{$domainKey}", []);
|
|
|
|
config([
|
|
'app.theme' => $domainConfig['theme'] ?? $domainKey,
|
|
'app.view_prefix' => $domainConfig['view_prefix'] ?? 'web',
|
|
'app.domain_name' => $domainConfig['domain_name'] ?? request()->getHost(),
|
|
'app.url' => $domainConfig['url'] ?? config('app.url'),
|
|
]);
|
|
|
|
View::share('theme', $domainConfig['theme'] ?? $domainKey);
|
|
View::share('viewPrefix', $domainConfig['view_prefix'] ?? 'web');
|
|
View::share('domainName', $domainConfig['domain_name'] ?? request()->getHost());
|
|
View::share('domainConfig', $domainConfig);
|
|
View::share('domainUrl', $domainConfig['url'] ?? config('app.url'));
|
|
View::share('assetUrl', DomainAssetContext::staticAssetOrigin($domainConfig));
|
|
|
|
DomainAssetContext::configureVite($domainConfig);
|
|
|
|
return $domainConfig;
|
|
};
|
|
|
|
$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()
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now());
|
|
|
|
$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
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '>=', $recentSince);
|
|
})
|
|
->withCount([
|
|
'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void {
|
|
$query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '>=', $recentSince);
|
|
},
|
|
'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void {
|
|
$query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->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
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->where('published_at', '>=', $recentSince)
|
|
->whereNotNull('published_at');
|
|
},
|
|
'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void {
|
|
$query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->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
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->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()
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now())
|
|
->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,
|
|
],
|
|
];
|
|
};
|
|
|
|
// Kategorie-/Branchenseite je Ausgabe.
|
|
$categoryPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) {
|
|
$domain = request()->getHost();
|
|
$language = app()->getLocale();
|
|
|
|
[$domainKey, $portal] = match (true) {
|
|
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
|
|
default => ['businessportal24', Portal::Businessportal24],
|
|
};
|
|
|
|
$applyWebDomainConfig($domainKey);
|
|
|
|
$portalValues = [$portal->value, Portal::Both->value];
|
|
|
|
$category = Category::query()
|
|
->whereIn('portal', $portalValues)
|
|
->where('is_active', true)
|
|
->whereHas('translations', fn (Builder $query) => $query->where('locale', $language)->where('slug', $slug))
|
|
->with([
|
|
'translations' => fn ($query) => $query->where('locale', $language),
|
|
'parent.translations' => fn ($query) => $query->where('locale', $language),
|
|
])
|
|
->first();
|
|
|
|
if (! $category) {
|
|
abort(404);
|
|
}
|
|
|
|
$childCategories = Category::query()
|
|
->where('parent_id', $category->id)
|
|
->where('is_active', true)
|
|
->with(['translations' => fn ($query) => $query->where('locale', $language)])
|
|
->get();
|
|
|
|
$categoryIds = collect([$category->id])->merge($childCategories->pluck('id'))->unique()->values()->all();
|
|
|
|
$base = static fn (): Builder => PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now())
|
|
->whereIn('category_id', $categoryIds);
|
|
|
|
// Kategoriespezifischer Stichtag: jüngste Meldung dieser Branche.
|
|
$latest = $base()->max('published_at');
|
|
$referenceNow = $latest ? Carbon::parse($latest) : now();
|
|
if ($referenceNow->gt(now())) {
|
|
$referenceNow = now();
|
|
}
|
|
$recentSince = $referenceNow->copy()->subDays(7);
|
|
$previousSince = $referenceNow->copy()->subDays(14);
|
|
$monthSince = $referenceNow->copy()->subDays(30);
|
|
|
|
$with = [
|
|
'company',
|
|
'category.translations' => fn ($query) => $query->where('locale', $language),
|
|
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1),
|
|
];
|
|
|
|
$leadRelease = $base()->with($with)->orderByDesc('published_at')->first();
|
|
|
|
$topReleases = $base()
|
|
->with($with)
|
|
->when($leadRelease, fn (Builder $query) => $query->whereKeyNot($leadRelease->getKey()))
|
|
->orderByDesc('published_at')
|
|
->limit(2)
|
|
->get();
|
|
|
|
$excludeIds = collect([$leadRelease?->getKey()])->merge($topReleases->modelKeys())->filter()->all();
|
|
|
|
$feedReleases = $base()
|
|
->with($with)
|
|
->when($excludeIds !== [], fn (Builder $query) => $query->whereNotIn('id', $excludeIds))
|
|
->orderByDesc('published_at')
|
|
->limit(12)
|
|
->get();
|
|
|
|
$mostReadReleases = $base()
|
|
->orderByDesc('hits')
|
|
->orderByDesc('published_at')
|
|
->limit(5)
|
|
->get(['id', 'slug', 'title', 'hits', 'portal', 'language']);
|
|
|
|
$stats = [
|
|
'today' => $base()->whereDate('published_at', $referenceNow->toDateString())->count(),
|
|
'week' => $base()->where('published_at', '>=', $recentSince)->count(),
|
|
'previousWeek' => $base()->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count(),
|
|
'month' => $base()->where('published_at', '>=', $monthSince)->count(),
|
|
'total' => $base()->count(),
|
|
'activeCompanies' => $base()->where('published_at', '>=', $recentSince)->distinct()->count('company_id'),
|
|
'totalCompanies' => $base()->distinct()->count('company_id'),
|
|
];
|
|
|
|
$newsroomFilter = static function (Builder $query) use ($portalValues, $categoryIds, $language): void {
|
|
$query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->whereIn('category_id', $categoryIds);
|
|
};
|
|
|
|
$topNewsrooms = Company::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('is_active', true)
|
|
->whereHas('pressReleases', function (Builder $query) use ($newsroomFilter, $recentSince): void {
|
|
$newsroomFilter($query);
|
|
$query->where('published_at', '>=', $recentSince);
|
|
})
|
|
->withCount([
|
|
'pressReleases as recent_releases_count' => function (Builder $query) use ($newsroomFilter, $recentSince): void {
|
|
$newsroomFilter($query);
|
|
$query->where('published_at', '>=', $recentSince);
|
|
},
|
|
'pressReleases as today_releases_count' => function (Builder $query) use ($newsroomFilter, $referenceNow): void {
|
|
$newsroomFilter($query);
|
|
$query->whereDate('published_at', $referenceNow->toDateString());
|
|
},
|
|
])
|
|
->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,
|
|
]);
|
|
|
|
$publishedCount = static fn (Builder $query) => $query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at');
|
|
|
|
$subCategories = $childCategories
|
|
->map(function (Category $child) use ($recentSince, $previousSince, $publishedCount): ?array {
|
|
$translation = $child->translations->first();
|
|
|
|
if (! $translation) {
|
|
return null;
|
|
}
|
|
|
|
$total = PressRelease::query()->withoutGlobalScope(PortalScope::class)
|
|
->where('category_id', $child->id)->tap($publishedCount)->count();
|
|
$recent = PressRelease::query()->withoutGlobalScope(PortalScope::class)
|
|
->where('category_id', $child->id)->tap($publishedCount)->where('published_at', '>=', $recentSince)->count();
|
|
$previous = PressRelease::query()->withoutGlobalScope(PortalScope::class)
|
|
->where('category_id', $child->id)->tap($publishedCount)
|
|
->where('published_at', '>=', $previousSince)->where('published_at', '<', $recentSince)->count();
|
|
|
|
return [
|
|
'name' => $translation->name,
|
|
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'),
|
|
'total' => $total,
|
|
'recent' => $recent,
|
|
'delta' => $recent - $previous,
|
|
];
|
|
})
|
|
->filter()
|
|
->sortByDesc('total')
|
|
->values();
|
|
|
|
$relatedCategories = Category::query()
|
|
->whereIn('portal', $portalValues)
|
|
->where('is_active', true)
|
|
->whereNull('parent_id')
|
|
->whereKeyNot($category->id)
|
|
->when($category->parent_id, fn (Builder $query) => $query->whereKeyNot($category->parent_id))
|
|
->with(['translations' => fn ($query) => $query->where('locale', $language)])
|
|
->withCount([
|
|
'pressReleases as total_count' => fn (Builder $query) => $publishedCount($query),
|
|
])
|
|
->orderByDesc('total_count')
|
|
->limit(8)
|
|
->get()
|
|
->map(function (Category $related): ?array {
|
|
$translation = $related->translations->first();
|
|
|
|
if (! $translation) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'name' => $translation->name,
|
|
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('web.home'),
|
|
'count' => (int) ($related->total_count ?? 0),
|
|
];
|
|
})
|
|
->filter()
|
|
->values();
|
|
|
|
return view('web.kategorie', [
|
|
'category' => $category,
|
|
'referenceNow' => $referenceNow,
|
|
'leadRelease' => $leadRelease,
|
|
'topReleases' => $topReleases,
|
|
'feedReleases' => $feedReleases,
|
|
'mostReadReleases' => $mostReadReleases,
|
|
'categoryStats' => $stats,
|
|
'topNewsrooms' => $topNewsrooms,
|
|
'subCategories' => $subCategories,
|
|
'relatedCategories' => $relatedCategories,
|
|
]);
|
|
};
|
|
|
|
// Detailseite einer Pressemitteilung je Ausgabe.
|
|
$releaseDetailPage = static function (string $edition, string $slug) use ($applyWebDomainConfig) {
|
|
$domain = request()->getHost();
|
|
$language = app()->getLocale();
|
|
|
|
[$domainKey, $portal] = match (true) {
|
|
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
|
|
default => ['businessportal24', Portal::Businessportal24],
|
|
};
|
|
|
|
$applyWebDomainConfig($domainKey);
|
|
|
|
$portalValues = [$portal->value, Portal::Both->value];
|
|
|
|
$publishedScope = static fn (Builder $query): Builder => $query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now());
|
|
|
|
$release = PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->tap($publishedScope)
|
|
->where('slug', $slug)
|
|
->with([
|
|
'company',
|
|
'category.translations' => fn ($query) => $query->where('locale', $language),
|
|
'category.parent.translations' => fn ($query) => $query->where('locale', $language),
|
|
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order'),
|
|
'contacts',
|
|
])
|
|
->first();
|
|
|
|
if (! $release) {
|
|
abort(404);
|
|
}
|
|
|
|
PressRelease::withoutGlobalScope(PortalScope::class)
|
|
->whereKey($release->getKey())
|
|
->update(['hits' => ($release->hits ?? 0) + 1]);
|
|
|
|
$with = [
|
|
'company',
|
|
'category.translations' => fn ($query) => $query->where('locale', $language),
|
|
'images' => fn ($query) => $query->orderByDesc('is_preview')->orderBy('sort_order')->limit(1),
|
|
];
|
|
|
|
$companyReleases = $release->company_id
|
|
? PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->tap($publishedScope)
|
|
->where('company_id', $release->company_id)
|
|
->whereKeyNot($release->getKey())
|
|
->with($with)
|
|
->orderByDesc('published_at')
|
|
->limit(3)
|
|
->get()
|
|
: collect();
|
|
|
|
$relatedReleases = $release->category_id
|
|
? PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->tap($publishedScope)
|
|
->where('category_id', $release->category_id)
|
|
->whereKeyNot($release->getKey())
|
|
->when($companyReleases->isNotEmpty(), fn (Builder $query) => $query->whereNotIn('id', $companyReleases->modelKeys()))
|
|
->with($with)
|
|
->orderByDesc('published_at')
|
|
->limit(3)
|
|
->get()
|
|
: collect();
|
|
|
|
return view('web.release-detail', [
|
|
'release' => $release,
|
|
'companyReleases' => $companyReleases,
|
|
'relatedReleases' => $relatedReleases,
|
|
]);
|
|
};
|
|
|
|
// Veröffentlichen-Marketingseite je Ausgabe (Einreichung läuft im Publisher-Hub).
|
|
$veroeffentlichenPage = static function (string $edition) use ($applyWebDomainConfig) {
|
|
$domain = request()->getHost();
|
|
$language = app()->getLocale();
|
|
|
|
[$domainKey, $portal] = match (true) {
|
|
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
|
|
default => ['businessportal24', Portal::Businessportal24],
|
|
};
|
|
|
|
$applyWebDomainConfig($domainKey);
|
|
|
|
$portalValues = [$portal->value, Portal::Both->value];
|
|
$referenceNow = EditorialClock::reference($portalValues, $language);
|
|
$recentSince = $referenceNow->copy()->subDays(30);
|
|
|
|
$publishedBase = static fn (): Builder => PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now());
|
|
|
|
$archiveTotal = $publishedBase()->count();
|
|
$publishedToday = $publishedBase()->whereDate('published_at', $referenceNow->toDateString())->count();
|
|
$oldest = $publishedBase()->min('published_at');
|
|
$oldestYear = $oldest ? (int) Carbon::parse($oldest)->format('Y') : null;
|
|
|
|
$inReview = PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Review)
|
|
->where('language', $language)
|
|
->count();
|
|
|
|
$exampleRelease = $publishedBase()
|
|
->with([
|
|
'company',
|
|
'category.translations' => fn ($query) => $query->where('locale', $language),
|
|
])
|
|
->orderByDesc('published_at')
|
|
->first();
|
|
|
|
$newsroomFilter = static function (Builder $query) use ($portalValues, $language, $recentSince): void {
|
|
$query
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '>=', $recentSince);
|
|
};
|
|
|
|
$newsroomQuery = Company::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('is_active', true)
|
|
->whereHas('pressReleases', fn (Builder $query) => $newsroomFilter($query));
|
|
|
|
$activeNewsroomsTotal = (clone $newsroomQuery)->count();
|
|
$activeNewsroomNames = (clone $newsroomQuery)
|
|
->withCount(['pressReleases as recent_count' => fn (Builder $query) => $newsroomFilter($query)])
|
|
->orderByDesc('recent_count')
|
|
->limit(10)
|
|
->pluck('name')
|
|
->all();
|
|
|
|
return view('web.veroeffentlichen', [
|
|
'referenceNow' => $referenceNow,
|
|
'archiveTotal' => $archiveTotal,
|
|
'publishedToday' => $publishedToday,
|
|
'inReview' => $inReview,
|
|
'oldestYear' => $oldestYear,
|
|
'exampleRelease' => $exampleRelease,
|
|
'activeNewsroomNames' => $activeNewsroomNames,
|
|
'activeNewsroomsTotal' => $activeNewsroomsTotal,
|
|
]);
|
|
};
|
|
|
|
// Archiv-Suchseite je Ausgabe (interaktive Suche läuft in der Volt-Komponente).
|
|
$suchePage = static function (string $edition) use ($applyWebDomainConfig) {
|
|
$domain = request()->getHost();
|
|
$language = app()->getLocale();
|
|
|
|
[$domainKey, $portal] = match (true) {
|
|
str_contains($domain, 'presseecho') => ['presseecho', Portal::Presseecho],
|
|
default => ['businessportal24', Portal::Businessportal24],
|
|
};
|
|
|
|
$applyWebDomainConfig($domainKey);
|
|
|
|
$portalValues = [$portal->value, Portal::Both->value];
|
|
|
|
$mostReadReleases = PressRelease::query()
|
|
->withoutGlobalScope(PortalScope::class)
|
|
->whereIn('portal', $portalValues)
|
|
->where('status', PressReleaseStatus::Published)
|
|
->where('language', $language)
|
|
->whereNotNull('published_at')
|
|
->where('published_at', '<=', now())
|
|
->orderByDesc('hits')
|
|
->orderByDesc('published_at')
|
|
->limit(5)
|
|
->get(['id', 'slug', 'title', 'hits', 'portal', 'language']);
|
|
|
|
return view('web.suche', [
|
|
'portal' => $portal->value,
|
|
'language' => $language,
|
|
'mostReadReleases' => $mostReadReleases,
|
|
]);
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bare Root: Publisher-Hub (pressekonto) oder Redirect auf die Default-Ausgabe.
|
|
// ---------------------------------------------------------------------------
|
|
Route::get('/', function () use ($applyWebDomainConfig) {
|
|
$domain = request()->getHost();
|
|
|
|
if (str_contains($domain, 'pressekonto')) {
|
|
$applyWebDomainConfig('pressekonto');
|
|
|
|
return view('web.pressekonto');
|
|
}
|
|
|
|
return redirect(request()->getSchemeAndHttpHost().'/'.SetEdition::DEFAULT_EDITION, 301);
|
|
})->name('home');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Legacy .html-URLs -> saubere 301-Weiterleitung (vor der Edition-Gruppe, damit
|
|
// `category/x.html` nicht als Slug `x.html` in die Kategorie-Route läuft).
|
|
// ---------------------------------------------------------------------------
|
|
$htmlRedirect = static function (string $edition, string $path) {
|
|
$clean = trim((string) preg_replace('/\.html$/', '', $path), '/');
|
|
|
|
$staticPages = [
|
|
'preise', 'faq', 'kontakt', 'ueber-uns', 'veroeffentlichen',
|
|
'suche', 'newsrooms', 'api', 'team', 'partner', 'karriere', 'presse',
|
|
'hilfe', 'impressum', 'datenschutz', 'agb', 'cookies',
|
|
];
|
|
|
|
// Entfallene Rubriken-Übersicht -> Startseite der Ausgabe.
|
|
if ($clean === 'kategorien') {
|
|
return redirect(request()->getSchemeAndHttpHost().'/'.$edition, 301);
|
|
}
|
|
|
|
if (str_starts_with($clean, 'category/') || str_starts_with($clean, 'press-release/')) {
|
|
$target = $clean;
|
|
} elseif ($clean === '' || str_contains($clean, '/')) {
|
|
$target = $clean;
|
|
} elseif (in_array($clean, $staticPages, true)) {
|
|
$target = $clean;
|
|
} else {
|
|
// Flache Detail-URL der Altseite: /{edition}/{slug}.html
|
|
$target = 'press-release/'.$clean;
|
|
}
|
|
|
|
return redirect(request()->getSchemeAndHttpHost().rtrim('/'.$edition.'/'.$target, '/'), 301);
|
|
};
|
|
|
|
Route::get('{edition}/{path}', function (string $edition, string $path) use ($htmlRedirect) {
|
|
return $htmlRedirect($edition, $path);
|
|
})->where('path', '.*\.html')->whereIn('edition', SetEdition::EDITIONS);
|
|
|
|
// Top-Level Legacy ohne Ausgabe-Präfix -> Default-Ausgabe.
|
|
Route::get('{path}', function (string $path) use ($htmlRedirect) {
|
|
return $htmlRedirect(SetEdition::DEFAULT_EDITION, $path);
|
|
})->where('path', '.*\.html');
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Öffentliche Inhalte je Ausgabe (Sprache): /de/... und /en/...
|
|
// ---------------------------------------------------------------------------
|
|
Route::prefix('{edition}')->whereIn('edition', SetEdition::EDITIONS)->group(function () use ($applyWebDomainConfig, $webHomeData, $categoryPage, $releaseDetailPage, $veroeffentlichenPage, $suchePage) {
|
|
// 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');
|
|
|
|
// Kategorie / Branchenseite (z.B. /de/category/energie-klima)
|
|
Route::get('category/{slug}', $categoryPage)->name('kategorie');
|
|
|
|
// Detailseite einer Pressemitteilung (z.B. /de/press-release/ki-revolution)
|
|
Route::get('press-release/{slug}', $releaseDetailPage)->name('release.detail');
|
|
|
|
// Statische Seiten
|
|
Route::view('preise', 'web.preise')->name('preise');
|
|
Route::view('faq', 'web.faq')->name('faq');
|
|
Route::view('kontakt', 'web.kontakt')->name('kontakt');
|
|
Route::view('ueber-uns', 'web.ueber-uns')->name('ueber-uns');
|
|
Route::get('veroeffentlichen', $veroeffentlichenPage)->name('veroeffentlichen');
|
|
Route::get('suche', $suchePage)->name('suche');
|
|
Route::view('newsrooms', 'web.newsrooms')->name('newsrooms');
|
|
Route::view('api', 'web.api')->name('api');
|
|
Route::view('team', 'web.team')->name('team');
|
|
Route::view('partner', 'web.partner')->name('partner');
|
|
Route::view('karriere', 'web.karriere')->name('karriere');
|
|
Route::view('presse', 'web.presse')->name('presse');
|
|
Route::view('hilfe', 'web.hilfe')->name('hilfe');
|
|
Route::view('impressum', 'web.impressum')->name('impressum');
|
|
Route::view('datenschutz', 'web.datenschutz')->name('datenschutz');
|
|
Route::view('agb', 'web.agb')->name('agb');
|
|
Route::view('cookies', 'web.cookies')->name('cookies');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sprach-/Edition-unabhängige Routen (Dev-Varianten, API-Doku).
|
|
// ---------------------------------------------------------------------------
|
|
Route::get('/variant-1', function () {
|
|
$domain = request()->getHost();
|
|
if (str_contains($domain, 'presseecho')) {
|
|
return view('web.presseecho');
|
|
}
|
|
|
|
return view('web.businessportal24-variant-float-glow');
|
|
})->name('variant-1');
|
|
|
|
Route::get('/variant-2', function () {
|
|
$domain = request()->getHost();
|
|
if (str_contains($domain, 'presseecho')) {
|
|
return view('web.presseecho');
|
|
}
|
|
|
|
return view('web.businessportal24-variant-glass-gradient');
|
|
});
|
|
|
|
Route::get('/docs/api/v1', function () {
|
|
return response((string) file_get_contents(base_path('docs/api/v1.yml')), 200, [
|
|
'Content-Type' => 'application/yaml; charset=UTF-8',
|
|
]);
|
|
})->name('docs.api.v1');
|