presseportale/routes/web.php
Kevin Adametz 1a612e04b2 R0.1: tote Dev-Varianten-Routen entfernt (fehlende Views -> 500 auf BP24)
/variant-1 + /variant-2 referenzierten nicht existierende Views
web.businessportal24-variant-float-glow/-glass-gradient; Fragmente der
alten Frontend-Version, nirgends verlinkt. Regressionstest ersetzt den
Stub in DevExampleRoutesTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:50:37 +00:00

215 lines
9.4 KiB
PHP

<?php
use App\Enums\Portal;
use App\Http\Controllers\PressReleasePreviewController;
use App\Http\Controllers\Web\CategoryController;
use App\Http\Controllers\Web\HomeController;
use App\Http\Controllers\Web\NewsroomDetailController;
use App\Http\Controllers\Web\NewsroomsController;
use App\Http\Controllers\Web\PreiseController;
use App\Http\Controllers\Web\ReleaseDetailController;
use App\Http\Controllers\Web\SucheController;
use App\Http\Controllers\Web\VeroeffentlichenController;
use App\Http\Middleware\SetEdition;
use App\Models\Category;
use App\Models\Company;
use App\Models\Plan;
use App\Scopes\PortalScope;
use App\Support\DomainAssetContext;
use Illuminate\Support\Facades\Route;
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
// Dünner Delegations-Wrapper auf die Single Source of Truth (DomainAssetContext),
// damit Bare-Root- und Edition-Home-Route unverändert per Closure aufrufen können.
$applyWebDomainConfig = static fn (string $domainKey): array => DomainAssetContext::applyWebDomainConfig($domainKey);
// ---------------------------------------------------------------------------
// 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) {
$base = request()->getSchemeAndHttpHost();
$clean = trim((string) preg_replace('/\.html$/', '', $path), '/');
// Account/Anmeldung lebt im Publisher-Hub (pressekonto), nicht am Portal.
// Die öffentlichen Spokes haben kein eigenes Login-System (Hub-and-Spoke).
$hubAliases = [
'registration' => '/register',
'login' => '/login',
];
if (array_key_exists($clean, $hubAliases)) {
$hubUrl = rtrim((string) config('domains.domain_portal_url'), '/');
return redirect($hubUrl.$hubAliases[$clean], 301);
}
// Entfallene Rubriken-Übersicht -> Startseite der Ausgabe.
if ($clean === 'kategorien') {
return redirect($base.'/'.$edition, 301);
}
// Ratgeber-Strecke der Altseite (/de/presse/*.html) -> konsolidierte FAQ.
// Hier steckt der SEO-Wert; alle Einzelseiten bündeln wir 301 auf /faq.
if ($clean === 'presse' || str_starts_with($clean, 'presse/')) {
return redirect($base.'/'.$edition.'/faq', 301);
}
// Alte Firmen-Pressemappe (presskit/{id}/{slug}) entspricht dem Newsroom.
// Feinmapping über die Legacy-ID: companies.(legacy_portal, legacy_id) ist
// unique-indiziert, ein Lookup liefert den heutigen Newsroom-Slug. Bei
// gültigem öffentlichem Newsroom 301 direkt dorthin (ein Hop), sonst sauber
// aufs Verzeichnis statt in einen 404.
if (preg_match('#^presskit/(\d+)#', $clean, $presskitMatch) === 1) {
$legacyPortal = str_contains(request()->getHost(), 'presseecho')
? Portal::Presseecho->value
: Portal::Businessportal24->value;
$company = Company::query()
->withoutGlobalScope(PortalScope::class)
->where('legacy_portal', $legacyPortal)
->where('legacy_id', (int) $presskitMatch[1])
->first();
$hasPublicNewsroom = $company
&& $company->is_active
&& $company->pressReleases()
->withoutGlobalScope(PortalScope::class)
->visibleInArchive()
->exists();
return redirect(
$hasPublicNewsroom
? $base.'/'.$edition.'/newsroom/'.$company->slug
: $base.'/'.$edition.'/newsrooms',
301
);
}
// Pressemappen-Pfade ohne numerische ID -> Newsrooms-Verzeichnis.
if ($clean === 'presskit' || str_starts_with($clean, 'presskit/')) {
return redirect($base.'/'.$edition.'/newsrooms', 301);
}
// Legacy-Slugs des Altsystems (meist englisch) -> neue deutsche Pfade.
$staticAliases = [
'pricelist' => 'preise',
'service' => 'veroeffentlichen',
'contact' => 'kontakt',
'imprint' => 'impressum',
'terms' => 'agb',
'privacypolicy' => 'datenschutz',
];
// Bereits saubere Zielpfade der neuen Struktur (Pfad bleibt erhalten).
$staticPages = [
'preise', 'faq', 'kontakt', 'ueber-uns', 'veroeffentlichen',
'suche', 'newsrooms', 'api', 'team', 'partner', 'karriere',
'hilfe', 'impressum', 'datenschutz', 'agb', 'cookies',
];
if (isset($staticAliases[$clean])) {
$target = $staticAliases[$clean];
} elseif (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(rtrim($base.'/'.$edition.'/'.$target, '/'), 301);
};
// ---------------------------------------------------------------------------
// Entfallene Sprach-/Regionsvarianten (de-ch, de-at) -> deutsche Ausgabe.
// Die echten Regionen-Übersichten (/region/{slug}) sind für einen späteren
// Schritt geplant; bis dahin verhindern diese 301 tote Bestands-Links.
// ---------------------------------------------------------------------------
$legacyRegions = ['de-ch', 'de-at'];
Route::get('{region}', function (string $region) {
return redirect(request()->getSchemeAndHttpHost().'/'.SetEdition::DEFAULT_EDITION, 301);
})->whereIn('region', $legacyRegions);
Route::get('{region}/{path}', function (string $region, string $path) {
return redirect(request()->getSchemeAndHttpHost().'/'.SetEdition::DEFAULT_EDITION, 301);
})->whereIn('region', $legacyRegions)->where('path', '.*');
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 () {
// Startseite je Ausgabe
Route::get('/', HomeController::class)->name('web.home');
// Kategorie / Branchenseite (z.B. /de/category/energie-klima)
Route::get('category/{slug}', CategoryController::class)->name('kategorie');
// Detailseite einer Pressemitteilung (z.B. /de/press-release/ki-revolution)
Route::get('press-release/{slug}', ReleaseDetailController::class)->name('release.detail');
// Preise / Tarife (datengetrieben: Plan + config/billing + config/credits)
Route::get('preise', PreiseController::class)->name('preise');
// Statische Seiten
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', VeroeffentlichenController::class)->name('veroeffentlichen');
Route::get('suche', SucheController::class)->name('suche');
Route::get('newsrooms', NewsroomsController::class)->name('newsrooms');
Route::get('newsroom/{slug}', NewsroomDetailController::class)->name('newsroom');
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 (API-Doku).
// ---------------------------------------------------------------------------
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');