presseportale/routes/web.php
Kevin Adametz 253141c6dc 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>
2026-06-16 16:39:28 +00:00

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');