web: E9 und E10 entschieden — Zuordnung aktiv, eine Adresse je statischer Seite, Hub-Portalseiten leiten weiter

E9: Kategorie-Zuordnung wie vorgeschlagen übernommen, Zuordnung ist
standardmäßig an (CANONICAL_ASSIGNMENT_ENABLED). Auf der Dev-Kopie
verifiziert: Kopie trägt rel=canonical auf die Erstquelle und fällt aus der
Sitemap ihres Portals.

E10: faq, hilfe, gute-pressemitteilung, die vier Transparenzseiten und die
API-Doku lagen auf allen drei Hosts mit identischem Text und je eigenem
Self-Canonical. Jetzt führt presseecho die Informationsseiten, der Hub die
API-Doku; Sitemap und llms.txt nennen nur die führende Adresse.
Rechtstexte (Impressum, Datenschutz, AGB, Richtlinien, Meldeweg,
Kündigung) bleiben bewusst je Host — jede Domain ist ein eigenes Angebot
mit eigener Anbieterkennzeichnung.

Bei der Erreichbarkeitsprüfung des Hubs gefunden und behoben:
/de/preise, /de/veroeffentlichen, /de/newsrooms und /de/suche wurden unter
pressekonto.com mit dem Branding von businessportal24 ausgeliefert (der
Portal-Kontext fällt dort darauf zurück) — jetzt 301 auf presseecho, die
Transparenz-Navigation verlinkt direkt dorthin. /de/kontakt und
/de/ueber-uns sprangen über /de auf die Startseite, jetzt in einem Schritt.

Der Self-Canonical kommt jetzt aus dem aufgerufenen Host; Portal-Inhalte,
die über den Hub laufen, behalten ihr Portal (webPortalContextApplied).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 07:52:09 +00:00
parent ee267c622e
commit 4c17a17254
18 changed files with 434 additions and 17 deletions

View file

@ -0,0 +1,116 @@
<?php
namespace App\Support;
/**
* Kanonischer Host statischer Seiten (Roadmap Anhang 2, E10).
*
* faq, hilfe, Transparenzseiten und die API-Doku werden auf Hub und beiden
* Portalen mit identischem Text ausgeliefert. Damit nicht drei Adressen um
* denselben Text konkurrieren, führt je Seite ein Host; die beiden anderen
* verweisen per rel=canonical dorthin. Rechtstexte sind bewusst
* ausgenommen — sie gehören zu ihrer Domain (siehe config/canonical.php).
*/
class StaticPageCanonical
{
/**
* Kanonische Adresse der aktuellen Seite oder null, wenn die Seite
* self-canonical bleibt.
*/
public static function urlFor(?string $routeName, string $path): ?string
{
$base = self::baseFor($routeName);
return $base === null ? null : $base.$path;
}
/**
* Basis-URL des führenden Hosts einer Route, oder null.
*/
public static function baseFor(?string $routeName): ?string
{
if ($routeName === null) {
return null;
}
$domainKey = config('canonical.static_page_host.'.$routeName);
if (! is_string($domainKey)) {
return null;
}
$url = config('domains.domains.'.$domainKey.'.url');
return is_string($url) && $url !== '' ? rtrim($url, '/') : null;
}
/**
* Basis der Selbst-Referenz: die konfigurierte URL des Hosts, unter dem
* die Seite tatsächlich aufgerufen wurde.
*
* Der geteilte `$domainUrl` aus dem ThemeServiceProvider taugt dafür
* nicht immer — er wird beim Booten aus dem damaligen Host abgeleitet
* und von `WebPortalContext::fromRequest()` auf ein Portal umgestellt,
* auf dem Hub also auf businessportal24. Maßgeblich ist der Host der
* Anfrage — außer bei Portal-Inhalten, die über den Hub aufgerufen
* wurden ($portalContextApplied).
*/
public static function selfBase(?string $domainUrl = null, bool $portalContextApplied = false): string
{
$host = request()->getHost();
// Portal-Inhalte (Kategorie, Archiv, Meldung, Newsroom) sind auch
// über den Hub erreichbar und rendern dort das Portal. Ihr Canonical
// gehört auf das Portal, nicht auf pressekonto.
if ($portalContextApplied && is_string($domainUrl) && $domainUrl !== '') {
return rtrim($domainUrl, '/');
}
if (is_string($domainUrl) && $domainUrl !== '' && parse_url($domainUrl, PHP_URL_HOST) === $host) {
return rtrim($domainUrl, '/');
}
foreach ((array) config('domains.domains', []) as $domain) {
$url = $domain['url'] ?? null;
if (is_string($url) && parse_url($url, PHP_URL_HOST) === $host) {
return rtrim($url, '/');
}
}
return rtrim(request()->getSchemeAndHttpHost(), '/');
}
/**
* Adresse für einen Link auf eine benannte Seite: normalerweise die des
* aufgerufenen Hosts — bei Portalseiten auf dem Hub direkt die des
* führenden Portals, damit der Besucher nicht über eine Weiterleitung
* läuft.
*/
public static function publicUrlFor(string $routeName): string
{
$url = route($routeName);
$portalBase = self::portalOnlyRedirect($routeName);
if ($portalBase !== null && str_contains(request()->getHost(), 'pressekonto')) {
return $portalBase.(string) parse_url($url, PHP_URL_PATH);
}
return $url;
}
/**
* Seiten, die es nur auf den Portalen gibt — auf dem Hub führen sie auf
* das führende Portal statt auf eine Fassung mit fremdem Branding.
*/
public static function portalOnlyRedirect(?string $routeName): ?string
{
if ($routeName === null || ! in_array($routeName, (array) config('canonical.portal_only_pages', []), true)) {
return null;
}
$url = config('domains.domains.'.config('canonical.portal_only_host', 'presseecho').'.url');
return is_string($url) && $url !== '' ? rtrim($url, '/') : null;
}
}

View file

@ -3,6 +3,7 @@
namespace App\Support;
use App\Enums\Portal;
use Illuminate\Support\Facades\View;
/**
* Öffentlicher Portal-Kontext (Presseecho/Businessportal24) einer Web-Landingpage,
@ -39,6 +40,12 @@ class WebPortalContext
$portal = self::portalFromHost();
$domainKey = $portal === Portal::Presseecho ? 'presseecho' : 'businessportal24';
// Merker fürs Layout: Diese Seite rendert Portal-Inhalte, die
// Domain-Config zeigt also auf ein Portal — auch wenn der Aufruf über
// den Hub kam. Der Self-Canonical muss dann dem Portal folgen, nicht
// dem aufgerufenen Host (Anhang 2, E10).
View::share('webPortalContextApplied', true);
return new self(
domainKey: $domainKey,
portal: $portal,