Frontend: Editorial-Relaunch der öffentlichen Strecke + Ausgaben-Routing
Ö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>
This commit is contained in:
parent
57afcab3b9
commit
3a1d2e6751
64 changed files with 4457 additions and 2971 deletions
753
routes/web.php
753
routes/web.php
|
|
@ -3,14 +3,18 @@
|
|||
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,}')
|
||||
|
|
@ -41,19 +45,24 @@ $applyWebDomainConfig = static function (string $domainKey): array {
|
|||
return $domainConfig;
|
||||
};
|
||||
|
||||
$webHomeData = static function (Portal $primaryPortal): array {
|
||||
$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', 'de')
|
||||
->where('language', $language)
|
||||
->whereNotNull('published_at')
|
||||
->where('published_at', '<=', now());
|
||||
|
||||
$with = [
|
||||
'company',
|
||||
'category.translations' => fn ($query) => $query->where('locale', 'de'),
|
||||
'category.translations' => fn ($query) => $query->where('locale', $language),
|
||||
'images' => fn ($query) => $query
|
||||
->orderByDesc('is_preview')
|
||||
->orderBy('sort_order')
|
||||
|
|
@ -85,29 +94,29 @@ $webHomeData = static function (Portal $primaryPortal): array {
|
|||
$activeNewsrooms = Company::query()
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('is_active', true)
|
||||
->whereHas('pressReleases', function (Builder $query) use ($portalValues): void {
|
||||
->whereHas('pressReleases', function (Builder $query) use ($portalValues, $recentSince, $language): void {
|
||||
$query
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('status', PressReleaseStatus::Published)
|
||||
->where('language', 'de')
|
||||
->where('language', $language)
|
||||
->whereNotNull('published_at')
|
||||
->where('published_at', '>=', now()->subDays(7));
|
||||
->where('published_at', '>=', $recentSince);
|
||||
})
|
||||
->withCount([
|
||||
'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues): void {
|
||||
'pressReleases as recent_releases_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void {
|
||||
$query
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('status', PressReleaseStatus::Published)
|
||||
->where('language', 'de')
|
||||
->where('language', $language)
|
||||
->whereNotNull('published_at')
|
||||
->where('published_at', '>=', now()->subDays(7));
|
||||
->where('published_at', '>=', $recentSince);
|
||||
},
|
||||
'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues): void {
|
||||
'pressReleases as today_releases_count' => function (Builder $query) use ($portalValues, $referenceNow, $language): void {
|
||||
$query
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('status', PressReleaseStatus::Published)
|
||||
->where('language', 'de')
|
||||
->whereDate('published_at', today());
|
||||
->where('language', $language)
|
||||
->whereDate('published_at', $referenceNow->toDateString());
|
||||
},
|
||||
])
|
||||
->orderByDesc('today_releases_count')
|
||||
|
|
@ -123,23 +132,23 @@ $webHomeData = static function (Portal $primaryPortal): array {
|
|||
]);
|
||||
|
||||
$industryIndex = Category::query()
|
||||
->with(['translations' => fn ($query) => $query->where('locale', 'de')])
|
||||
->with(['translations' => fn ($query) => $query->where('locale', $language)])
|
||||
->withCount([
|
||||
'pressReleases as recent_count' => function (Builder $query) use ($portalValues): void {
|
||||
'pressReleases as recent_count' => function (Builder $query) use ($portalValues, $recentSince, $language): void {
|
||||
$query
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('status', PressReleaseStatus::Published)
|
||||
->where('language', 'de')
|
||||
->where('published_at', '>=', now()->subDays(7))
|
||||
->where('language', $language)
|
||||
->where('published_at', '>=', $recentSince)
|
||||
->whereNotNull('published_at');
|
||||
},
|
||||
'pressReleases as previous_count' => function (Builder $query) use ($portalValues): void {
|
||||
'pressReleases as previous_count' => function (Builder $query) use ($portalValues, $recentSince, $previousSince, $language): void {
|
||||
$query
|
||||
->whereIn('portal', $portalValues)
|
||||
->where('status', PressReleaseStatus::Published)
|
||||
->where('language', 'de')
|
||||
->where('published_at', '>=', now()->subDays(14))
|
||||
->where('published_at', '<', now()->subDays(7));
|
||||
->where('language', $language)
|
||||
->where('published_at', '>=', $previousSince)
|
||||
->where('published_at', '<', $recentSince);
|
||||
},
|
||||
])
|
||||
->whereIn('portal', $portalValues)
|
||||
|
|
@ -157,7 +166,7 @@ $webHomeData = static function (Portal $primaryPortal): array {
|
|||
|
||||
return [
|
||||
'name' => $translation->name,
|
||||
'href' => $translation->slug ? route('kategorie', ['slug' => $translation->slug]) : route('kategorien'),
|
||||
'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),
|
||||
];
|
||||
|
|
@ -165,50 +174,619 @@ $webHomeData = static function (Portal $primaryPortal): array {
|
|||
->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', today())->count(),
|
||||
'publishedToday' => $publishedQuery()->whereDate('published_at', $referenceNow->toDateString())->count(),
|
||||
'archiveSince' => $oldestPublishedAt ? (int) Carbon::parse($oldestPublishedAt)->format('Y') : null,
|
||||
],
|
||||
];
|
||||
};
|
||||
|
||||
// Hauptseite - dynamisch basierend auf Domain
|
||||
Route::get('/', function () use ($applyWebDomainConfig, $webHomeData) {
|
||||
// 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, 'presseecho')) {
|
||||
$applyWebDomainConfig('presseecho');
|
||||
|
||||
return view('web.presseecho', $webHomeData(Portal::Presseecho));
|
||||
} elseif (str_contains($domain, 'businessportal24')) {
|
||||
$applyWebDomainConfig('businessportal24');
|
||||
|
||||
return view('web.businessportal24', $webHomeData(Portal::Businessportal24));
|
||||
} elseif (str_contains($domain, 'pressekonto')) {
|
||||
if (str_contains($domain, 'pressekonto')) {
|
||||
$applyWebDomainConfig('pressekonto');
|
||||
|
||||
return view('web.pressekonto');
|
||||
}
|
||||
|
||||
$applyWebDomainConfig('businessportal24');
|
||||
|
||||
return view('web.businessportal24', $webHomeData(Portal::Businessportal24));
|
||||
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();
|
||||
// Domain-basierte View-Auswahl
|
||||
if (str_contains($domain, 'presseecho')) {
|
||||
return view('web.presseecho');
|
||||
} elseif (str_contains($domain, 'businessportal24')) {
|
||||
return view('web.businessportal24-variant-float-glow');
|
||||
}
|
||||
|
||||
return view('web.businessportal24-variant-float-glow');
|
||||
|
|
@ -216,108 +794,15 @@ Route::get('/variant-1', function () {
|
|||
|
||||
Route::get('/variant-2', function () {
|
||||
$domain = request()->getHost();
|
||||
// Domain-basierte View-Auswahl
|
||||
if (str_contains($domain, 'presseecho')) {
|
||||
return view('web.presseecho');
|
||||
} elseif (str_contains($domain, 'businessportal24')) {
|
||||
return view('web.businessportal24-variant-glass-gradient');
|
||||
}
|
||||
|
||||
return view('web.businessportal24-variant-glass-gradient');
|
||||
});
|
||||
// Preise & Leistungen
|
||||
Route::get('/preise', function () {
|
||||
return view('web.preise');
|
||||
})->name('preise');
|
||||
|
||||
// FAQ - Häufig gestellte Fragen
|
||||
Route::get('/faq', function () {
|
||||
return view('web.faq');
|
||||
})->name('faq');
|
||||
|
||||
// Kontakt
|
||||
Route::get('/kontakt', function () {
|
||||
return view('web.kontakt');
|
||||
})->name('kontakt');
|
||||
|
||||
// Über uns
|
||||
Route::get('/ueber-uns', function () {
|
||||
return view('web.ueber-uns');
|
||||
})->name('ueber-uns');
|
||||
|
||||
// Veröffentlichen
|
||||
Route::get('/veroeffentlichen', function () {
|
||||
return view('web.veroeffentlichen');
|
||||
})->name('veroeffentlichen');
|
||||
|
||||
// Kategorien
|
||||
Route::get('/kategorien', function () {
|
||||
return view('web.kategorien');
|
||||
})->name('kategorien');
|
||||
|
||||
// Suche
|
||||
Route::get('/suche', function () {
|
||||
return view('web.suche');
|
||||
})->name('suche');
|
||||
|
||||
// Services
|
||||
Route::get('/newsrooms', function () {
|
||||
return view('web.newsrooms');
|
||||
})->name('newsrooms');
|
||||
|
||||
Route::get('/api', function () {
|
||||
return view('web.api');
|
||||
})->name('api');
|
||||
|
||||
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');
|
||||
|
||||
// Über uns - Unterseiten
|
||||
Route::get('/team', function () {
|
||||
return view('web.team');
|
||||
})->name('team');
|
||||
|
||||
Route::get('/partner', function () {
|
||||
return view('web.partner');
|
||||
})->name('partner');
|
||||
|
||||
Route::get('/karriere', function () {
|
||||
return view('web.karriere');
|
||||
})->name('karriere');
|
||||
|
||||
Route::get('/presse', function () {
|
||||
return view('web.presse');
|
||||
})->name('presse');
|
||||
|
||||
// Hilfe & Support
|
||||
Route::get('/hilfe', function () {
|
||||
return view('web.hilfe');
|
||||
})->name('hilfe');
|
||||
|
||||
// Rechtliches
|
||||
Route::get('/impressum', function () {
|
||||
return view('web.impressum');
|
||||
})->name('impressum');
|
||||
|
||||
Route::get('/datenschutz', function () {
|
||||
return view('web.datenschutz');
|
||||
})->name('datenschutz');
|
||||
|
||||
Route::get('/agb', function () {
|
||||
return view('web.agb');
|
||||
})->name('agb');
|
||||
|
||||
Route::get('/cookies', function () {
|
||||
return view('web.cookies');
|
||||
})->name('cookies');
|
||||
|
||||
// Kategorie Seite (z.B. IT & Software)
|
||||
Route::get('/kategorie/{slug}', function ($slug) {
|
||||
return view('web.kategorie', ['categorySlug' => $slug]);
|
||||
})->name('kategorie');
|
||||
|
||||
// Release Detail Seite
|
||||
Route::get('/release/{slug}', function ($slug) {
|
||||
return view('web.release-detail', ['releaseSlug' => $slug]);
|
||||
})->name('release.detail');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue