b2in/routes/web.php
Kevin Adametz 6d53243ea6 B2in Interior: Relaunch der Website, Kernmarken-CMS und Terminanfrage
Umsetzung von AP0-AP6 aus dev/2026-08-28/entwicklungsplan.md sowie der
vorab gefundenen Befunde T1 und B1-B13.

Befunde
- CmsContentSeeder überschrieb bei jedem Lauf alle redaktionellen Inhalte;
  jetzt Redaktionsschutz plus gezielter Relaunch-Seeder (B1)
- Magazin-CMS war nicht ans Frontend angebunden, Artikel jetzt über Slugs
  mit 301 von den alten numerischen URLs (B2)
- Vier veröffentlichte Display-Playlists zeigten auf b2in.test und wären
  live nicht ladbar gewesen (B13)
- Info-Tablet speicherte geleerte Öffnungszeiten nicht (T1)
- /service und /portfolio lieferten seit Februar 500 (B12)

Website
- Startseite neu strukturiert und umgetextet, eigene Seite /marken mit
  Detailseite je Kernmarke, /netzwerk und Altrouten leiten dorthin
- Galerie je Marke, getrennt nach realisierten Projekten und
  Visualisierungen; ohne gesetzte Kategorie gilt ein Bild als Entwurf
- Seite /referenzen, gespeist aus den Markengalerien
- Showroom-Display im 9:16-Rahmen über eine schreibgeschützte Embed-Route
  ohne Preview-Token
- Terminanfrage-Widget auf allen Seiten, Markenauswahl aus dem CMS,
  WhatsApp per Klick-zum-Chat-Link mit vorbereitetem Text
- FAQ, Über-uns, Kontakt und Magazin inhaltlich auf Interior umgestellt

Sonstiges
- Vorschau-Instanzen liefern noindex und verlinken sich bei unbekanntem
  Host auf sich selbst statt auf eine fremde Domain
- Seeder-Zugangsdaten aus dem Code in die Konfiguration (B8)
- Ablauf für die Vorschau: dev/2026-08-28/preview-deployment.md

833 Tests grün (3084 Assertions), Pint sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:31:19 +00:00

250 lines
9.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Livewire\Cabinet\QuickStatus;
use App\Models\CmsProject;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Livewire\Volt\Volt;
// Gemeinsame Web-Routes für alle Pages)
// Jede Landing-Page hat das gleiche Gerüst, aber unterschiedliches Styling
// Hauptseite - lädt automatisch das richtige Theme basierend auf der Domain
Route::get('/', function () {
$theme = config('app.theme', 'b2in');
// Use theme-specific view if it exists (e.g., web.b2a, web.stileigentum)
// The view name for b2in theme is 'web.b2in' and not 'web.home'
if ($theme === 'b2in') {
return view('web.home');
}
if (view()->exists('web.'.$theme)) {
return view('web.'.$theme);
}
// Fallback to the default home view
return view('web.home');
})->name('home');
// Willkommensseite
Route::get('/welcome', function () {
return view('web.welcome');
})->name('welcome');
// Weitere gemeinsame Webseiten hier...
Route::get('/about', function () {
return view('web.about');
})->name('about');
Route::get('/ecosystem', function () {
return redirect('/partner', 301);
})->name('ecosystem');
$loadAziziContent = fn (): array => json_decode(
file_get_contents(resource_path('lang/de/immobilien-azizi.json')),
true,
512,
JSON_THROW_ON_ERROR,
);
$isAziziSourcePreview = function (Request $request): bool {
if (! $request->query->has('dev')) {
return false;
}
return ! in_array(strtolower((string) $request->query('dev')), ['0', 'false', 'off', 'no'], true);
};
Route::get('/immobilien', function (Request $request) use ($isAziziSourcePreview, $loadAziziContent) {
$content = $loadAziziContent();
$dev = $isAziziSourcePreview($request);
$devQuery = $dev;
$overviewRouteName = 'immobilien';
$detailRouteName = 'immobilien.show';
return view('web.dev.immobilien-azizi', compact('content', 'dev', 'devQuery', 'overviewRouteName', 'detailRouteName'));
})->name('immobilien');
Route::get('/immobilien/{slug}', function (Request $request, string $slug) use ($isAziziSourcePreview, $loadAziziContent) {
$content = $loadAziziContent();
$dev = $isAziziSourcePreview($request);
$devQuery = $dev;
$overviewRouteName = 'immobilien';
$project = collect($content['projects'] ?? [])->firstWhere('slug', $slug);
abort_unless($project, 404);
return view('web.dev.immobilien-azizi-show', compact('content', 'project', 'dev', 'devQuery', 'overviewRouteName'));
})->name('immobilien.show');
// Die Netzwerk-Seite geht im Abschnitt "Mehr als Marken: unser Netzwerk" auf /marken auf.
// Alte Links und Rankings laufen per 301 dorthin; die View bleibt als Archiv liegen.
Route::get('/netzwerk', function () {
return redirect('/marken', 301);
})->name('netzwerk');
Route::get('/interior', function () {
return redirect('/marken', 301);
})->name('interior');
/*Route::get('/immobilien/{slug}', function (string $slug) {
$cmsProject = CmsProject::query()->published()->where('slug', $slug)->first();
if ($cmsProject) {
$project = $cmsProject->toFrontendArray();
return view('web.immobilien-show', compact('project'));
}
$theme = config('app.theme', 'b2in');
$immobilienProjects = cms_theme_section('immobilien_projects', $theme);
$project = is_array($immobilienProjects) ? data_get($immobilienProjects, "projects.{$slug}") : null;
abort_unless($project, 404);
return view('web.immobilien-show', compact('project'));
})->name('immobilien.show');
*/
Route::get('/partner', function () {
return redirect('/marken', 301);
})->name('partner');
Route::get('/magazin', function () {
return view('web.magazin');
})->name('magazin');
// Alte numerische Magazin-URLs (/magazin/1 … /magazin/5) dauerhaft auf den Slug umleiten.
// Muss vor der Slug-Route stehen, sonst wuerde diese die Ziffern selbst schlucken.
Route::get('/magazin/{id}', function (string $id) {
$slug = \App\Models\CmsArticle::slugForLegacyId($id);
abort_unless($slug !== null, 404);
return redirect()->route('magazin.detail', ['slug' => $slug], 301);
})->whereNumber('id')->name('magazin.detail.legacy');
Route::get('/magazin/{slug}', function (string $slug) {
return view('web.magazin-detail', compact('slug'));
})->name('magazin.detail');
// Kernmarken: Uebersichtsseite und Detailseite je Marke.
Route::get('/marken', function () {
return view('web.marken');
})->name('marken');
Route::get('/marken/{slug}', function (string $slug) {
$brands = \App\Models\CmsBrand::forFrontend();
abort_unless(isset($brands[$slug]), 404);
$brand = $brands[$slug];
$brandTitle = $brand['name'].' – '.$brand['tagline'].' | B2in Interior';
$brandDescription = trim(strip_tags((string) $brand['teaser']));
return view('web.marken-show', compact('slug', 'brandTitle', 'brandDescription'));
})->name('marken.show');
// Referenzen: realisierte Projekte aus den Markengalerien plus der Showroom-Feed.
Route::get('/referenzen', function () {
return view('web.referenzen');
})->name('referenzen');
// Schreibgeschuetzte Einbettung eines Displays (nur veroeffentlichte Playlists, kein Token).
Route::get('/embed/display/{display}', [\App\Http\Controllers\DisplayEmbedController::class, 'show'])
->name('display.embed');
Route::get('/contact', function () {
return view('web.contact');
})->name('contact');
// Verwaiste Seiten: ihre Content-Sektionen (hero_image, content_section_left/right, portfolio)
// existieren in keiner Lang-Datei mehr und die Seiten liefern seitdem 500. Sie werden wie die
// anderen stillgelegten Seiten dauerhaft umgeleitet; die Views bleiben als Archiv liegen.
Route::get('/service', function () {
return redirect('/marken', 301);
})->name('service');
Route::get('/portfolio', function () {
return redirect('/immobilien', 301);
})->name('portfolio');
Route::get('/faq', function () {
return view('web.faq');
})->name('faq');
// Rechtliche Seiten
Route::get('/impressum', function () {
return view('web.impressum');
})->name('impressum');
Route::get('/privacy', function () {
return view('web.privacy');
})->name('privacy');
Route::get('/terms', function () {
return view('web.terms');
})->name('terms');
Route::get('/cookie-policy', function () {
return view('web.cookie-policy');
})->name('cookie-policy');
// Theme Demo Route
Route::get('/theme-demo', function () {
return view('web.theme-demo');
})->name('theme-demo');
// Dev: Sitemap & Archiv-Versionen
Route::prefix('dev')->group(function () use ($loadAziziContent) {
Route::get('/sitemap', fn () => view('web.dev.sitemap'))->name('dev.sitemap');
Route::get('/immobilien-v1', fn () => view('web.dev.immobilien-v1'))->name('dev.immobilien-v1');
Route::get('/immobilien-azizi', function () use ($loadAziziContent) {
$content = $loadAziziContent();
$dev = true;
$devQuery = false;
$overviewRouteName = 'dev.immobilien-azizi';
$detailRouteName = 'dev.immobilien-azizi.show';
return view('web.dev.immobilien-azizi', compact('content', 'dev', 'devQuery', 'overviewRouteName', 'detailRouteName'));
})->name('dev.immobilien-azizi');
Route::get('/immobilien-azizi/{slug}', function (string $slug) use ($loadAziziContent) {
$content = $loadAziziContent();
$project = collect($content['projects'] ?? [])->firstWhere('slug', $slug);
abort_unless($project, 404);
$dev = true;
$devQuery = false;
$overviewRouteName = 'dev.immobilien-azizi';
return view('web.dev.immobilien-azizi-show', compact('content', 'project', 'dev', 'devQuery', 'overviewRouteName'));
})->name('dev.immobilien-azizi.show');
Route::get('/interior-v1', fn () => view('web.dev.interior-v1'))->name('dev.interior-v1');
Route::get('/partner-v1', fn () => view('web.dev.partner-v1'))->name('dev.partner-v1');
});
// Pfad-basierte Theme-Routen für lokale Entwicklung wurden entfernt
// Die Themensauswahl wird nun über den ThemeServiceProvider gesteuert (Domain oder ?theme=... GET-Parameter)
Route::get('/partner/invitation/expired/{token}', function (string $token) {
$invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
return view('partner.invitation-expired', compact('invitation'));
})->name('partner.invitation.expired');
Route::get('/partner/invitation/used/{token}', function (string $token) {
$invitation = \App\Models\PartnerInvitation::with('role')->where('token', $token)->firstOrFail();
return view('partner.invitation-used', compact('invitation'));
})->name('partner.invitation.used');
Volt::route('/partner/invitation/{token}', 'partner.invitation-accept')
->name('partner.invitation.accept');
Volt::route('/partner/create-account', 'partner.create-account')
->name('partner.create.account');
// Öffentliche Registrierung per QR-/Code (Landing, code-check)
Volt::route('/reg/{role}', 'reg.landing')
->name('registration.landing');
Volt::route('/registration/thank-you', 'reg.thank-you')
->name('registration.thank-you');
// Partner Setup Wizard & Daten
Route::middleware('auth')->group(function () {
Volt::route('/partner/setup', 'partner.setup-wizard')
->name('partner.setup.wizard');
});
// Cabinet Quick-Status – key-geschützte Mini-App für schnellen Statuswechsel
Route::get('/info/status', QuickStatus::class)->name('cabinet.quick-status');
// Authentifizierungs-Routen werden in domains.php eingebunden
// require __DIR__ . '/auth.php';