Multi-Domain-Asset-Infrastruktur: geteilte Vite-Konfiguration und DomainAssetContext

- vite.shared.js als gemeinsame Quelle fuer Ports, Hot-Files, HMR-Hosts
  und CORS-Origins der beiden Vite-Builds (Portal/Web)
- App\Support\DomainAssetContext kapselt die Vite-Build-Directory-
  Konfiguration pro Domain (ThemeServiceProvider + Auth-Layout nutzen ihn)
- Tailwind-Portal-Content-Globs auf die tatsaechliche View-Struktur gezogen
- Dev-Beispiel-Routen + Tests (DomainAssetContextTest, DevExampleRoutesTest)
- Aufraeumen: versehentliche Leerdatei dev:web entfernt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 08:16:09 +00:00
parent 4bb9094207
commit 0efabaf446
15 changed files with 485 additions and 109 deletions

View file

@ -16,4 +16,23 @@ enum Portal: string
self::Both => 'Beide Portale',
};
}
public function abbreviation(): string
{
return match ($this) {
self::Presseecho => 'PE',
self::Businessportal24 => 'B24',
self::Both => 'PE+B24',
};
}
public static function stripTrailingAbbreviation(string $value): string
{
$abbreviations = implode('|', array_map(
fn (self $portal): string => preg_quote($portal->abbreviation(), '/'),
self::cases(),
));
return trim((string) preg_replace('/\s*\(('.$abbreviations.')\)\s*$/u', '', $value));
}
}

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Support\DomainAssetContext;
use Illuminate\Routing\UrlGenerator;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\URL;
@ -16,7 +17,6 @@ class ThemeServiceProvider extends ServiceProvider
*/
public function register(): void
{
// Registriere die domains.php Konfigurationsdatei
$this->mergeConfigFrom(
base_path('config/domains.php'),
'domains'
@ -28,11 +28,10 @@ class ThemeServiceProvider extends ServiceProvider
*/
public function boot(): void
{
$host = Request::getHost(); // is domain_name
$themeOverride = Request::get('theme'); // Allow theme override via URL parameter
$host = Request::getHost();
$themeOverride = Request::get('theme');
// Standard-Werte für Domain, die nicht in der Konfiguration sind
$domainConfig = [
$defaults = [
'name' => config('app.name'),
'theme' => 'b2in',
'view_prefix' => 'b2in',
@ -41,68 +40,47 @@ class ThemeServiceProvider extends ServiceProvider
'domain_name' => config('app.domain_name'),
];
// Lade die Domain-Konfiguration
$confiDomains = config('domains.domains');
$domainConfig = DomainAssetContext::resolve(
$host,
$defaults,
config('domains.domains', []),
is_string($themeOverride) ? $themeOverride : null,
);
// Suche nach der aktuellen Domain in der Konfiguration
foreach ($confiDomains as $name => $config) {
if (is_array($config) && isset($config['domain_name']) && $config['domain_name'] === $host) {
$domainConfig = array_merge($domainConfig, $config);
break;
}
}
$staticAssetOrigin = DomainAssetContext::staticAssetOrigin($domainConfig);
$viteDevServerUrl = DomainAssetContext::viteDevServerUrl($domainConfig);
// Allow theme override via URL parameter (for testing)
if ($themeOverride && isset($confiDomains[$themeOverride])) {
$domainConfig = array_merge($domainConfig, $confiDomains[$themeOverride]);
}
// Dynamische ASSET_URL basierend auf der aktuellen Domain setzen
// Verhindert CORS-Probleme, da Assets immer von derselben Domain geladen werden
$assetUrl = $domainConfig['url'];
// Grundlegende Konfiguration im Anwendungskontext verfügbar machen
config([
'app.theme' => $domainConfig['theme'],
'app.view_prefix' => $domainConfig['view_prefix'],
'app.domain_name' => $domainConfig['domain_name'],
'app.url' => $domainConfig['url'],
'app.asset_url' => $assetUrl, // Dynamische Asset-URL für die aktuelle Domain
'app.asset_url' => $staticAssetOrigin,
]);
// URL-Generator für die aktuelle Domain konfigurieren
// Dies ist wichtig, damit asset() und url() die richtige Domain verwenden
URL::forceRootUrl($domainConfig['url']);
URL::forceScheme(parse_url($domainConfig['url'], PHP_URL_SCHEME) ?: 'https');
// WICHTIG: Asset-Root direkt im UrlGenerator setzen
// Der asset() Helper verwendet einen separaten Asset-Root
/** @var UrlGenerator $urlGenerator */
$urlGenerator = app('url');
$urlGenerator->useAssetOrigin($assetUrl);
$urlGenerator->useAssetOrigin($staticAssetOrigin);
// Spezifischere Daten für die Views verfügbar machen
View::share('theme', $domainConfig['theme']);
View::share('viewPrefix', $domainConfig['view_prefix']);
View::share('domainName', $domainConfig['domain_name']);
View::share('domainConfig', $domainConfig);
View::share('domainUrl', $domainConfig['url']);
View::share('assetUrl', $assetUrl);
View::share('assetUrl', $staticAssetOrigin);
View::share('viteDevServerUrl', $viteDevServerUrl);
// Vite-Assets-Konfiguration für die aktuelle Domain
if (! app()->runningInConsole()) {
if (isset($domainConfig['assets_dir'])) {
Vite::useBuildDirectory($domainConfig['assets_dir']);
DomainAssetContext::configureVite($domainConfig);
}
if (app()->environment('local')) {
// Entwicklung: Vite Dev Server mit HMR
$viteDevServerUrl = env('VITE_DEV_SERVER_URL', 'https://assets.pressekonto.test');
Vite::useHotFile(public_path('hot'));
config(['app.vite_dev_server_url' => $viteDevServerUrl]);
View::share('viteDevServerUrl', $viteDevServerUrl);
} else {
// Produktion: Assets von der aktuellen Domain laden (kein CORS nötig)
Vite::useScriptTagAttributes(['crossorigin' => false]);
Vite::useStyleTagAttributes(['crossorigin' => false]);
}

View file

@ -0,0 +1,145 @@
<?php
namespace App\Support;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Vite;
class DomainAssetContext
{
/**
* @param array<string, mixed> $defaults
* @param array<string, array<string, mixed>> $configuredDomains
* @return array<string, mixed>
*/
public static function resolve(
string $host,
array $defaults,
array $configuredDomains,
?string $themeOverride = null,
?Request $request = null,
): array {
if ($themeOverride !== null && isset($configuredDomains[$themeOverride])) {
return array_merge($defaults, $configuredDomains[$themeOverride]);
}
$portalHost = (string) config('domains.domain_portal');
if ($host === $portalHost) {
$request ??= request();
if (self::isBackendRequest($request)) {
return array_merge($defaults, $configuredDomains['portal'] ?? $defaults);
}
return array_merge($defaults, $configuredDomains['pressekonto'] ?? $defaults);
}
foreach ($configuredDomains as $config) {
if (! is_array($config)) {
continue;
}
if (($config['domain_name'] ?? null) === $host) {
return array_merge($defaults, $config);
}
}
return $defaults;
}
public static function isBackendRequest(Request $request): bool
{
if ($request->routeIs(
'dashboard',
'login',
'register',
'password.*',
'verification.*',
'two-factor.*',
'magic-links.*',
'me.*',
'settings.*',
'press-releases.preview',
)) {
return true;
}
$backendPrefixes = [
'admin',
'customer',
'dashboard',
'login',
'register',
'forgot-password',
'reset-password',
'magic-login',
'user',
'settings',
'flux',
'livewire',
];
$path = trim($request->path(), '/');
foreach ($backendPrefixes as $prefix) {
if ($path === $prefix || str_starts_with($path, "{$prefix}/")) {
return true;
}
}
return false;
}
public static function usesWebAssets(string $assetsDir): bool
{
return $assetsDir === 'build/web';
}
public static function hotFilePath(string $assetsDir): string
{
return self::usesWebAssets($assetsDir)
? public_path('hot-web')
: public_path('hot-portal');
}
/**
* @param array<string, mixed> $domainConfig
*/
public static function configureVite(array $domainConfig): void
{
$assetsDir = (string) ($domainConfig['assets_dir'] ?? 'build/portal');
Vite::useBuildDirectory($assetsDir);
Vite::useHotFile(self::hotFilePath($assetsDir));
}
/**
* @param array<string, mixed> $domainConfig
*/
public static function staticAssetOrigin(array $domainConfig): string
{
return (string) ($domainConfig['url'] ?? config('app.url'));
}
/**
* @param array<string, mixed> $domainConfig
*/
public static function viteDevServerUrl(array $domainConfig): string
{
if (isset($domainConfig['asset_url'])) {
return (string) $domainConfig['asset_url'];
}
$assetsDir = (string) ($domainConfig['assets_dir'] ?? 'build/portal');
return self::usesWebAssets($assetsDir)
? 'https://assets.pressekonto.test'
: 'https://assets.pressekonto.test';
}
public static function isViteDevServerRunning(string $assetsDir): bool
{
return is_file(self::hotFilePath($assetsDir));
}
}