web: Portal-Inhalte sind auf dem Hub nicht mehr erreichbar (Anhang 2, E10)

Kategorien, Jahresarchive, Meldungen und Newsrooms liefen auch unter
pressekonto.com — die Ausgabe-Routen gelten für alle Hosts, und der
Portal-Kontext fällt dort auf businessportal24 zurück. Jetzt 301 auf das
Portal.

Meldung und Newsroom folgen ihrem eigenen Portal, nicht einer festen
Vorgabe; bei Doppelveröffentlichung (gleicher Slug auf beiden Portalen)
gewinnt die Erstquelle, sonst endete die Weiterleitung auf der Kopie.
Kategorie, Archiv und Suche gibt es auf beiden Portalen — dort entscheidet
canonical.portal_only_host.

Bewusst nicht weitergeleitet wird ein unbekannter Slug (sonst 301 in ein
404) und alles, was kein GET ist (ein 301 auf POST verliert die Anfrage —
Newsroom beanspruchen und Share-Zählung laufen weiter). Query-Parameter
bleiben erhalten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-18 09:15:04 +00:00
parent 4c17a17254
commit 24c1bbf500
5 changed files with 222 additions and 28 deletions

View file

@ -2,33 +2,82 @@
namespace App\Http\Middleware;
use App\Models\Company;
use App\Models\PressRelease;
use App\Support\StaticPageCanonical;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Portalseiten auf dem Hub (Anhang 2, E10): Preise, Veröffentlichen,
* Newsrooms und Suche gibt es nur auf den Portalen. Unter pressekonto.com
* wurden sie bisher trotzdem ausgeliefert — mit dem Branding von
* businessportal24, weil der Portal-Kontext dort darauf zurückfällt. Statt
* einer Seite mit fremdem Namen unter fremder Domain: 301 auf das führende
* Portal.
* Portalseiten und Portal-Inhalte auf dem Hub (Anhang 2, E10).
*
* Die Ausgabe-Routen (`/{edition}/…`) gelten für alle Hosts, deshalb waren
* Preise, Veröffentlichen, Newsrooms, Suche, Kategorien, Jahresarchive,
* Meldungen und Newsrooms auch unter pressekonto.com erreichbar — mit dem
* Branding von businessportal24, weil der Portal-Kontext auf dem Hub darauf
* zurückfällt. Der Hub ist der Publisher-Bereich und hat diese Inhalte
* nicht; sie gehören auf das Portal.
*
* Ziel ist bei Meldung und Newsroom das Portal des Eintrags selbst (bei
* `both` und bei den übrigen Seiten das Standard-Portal). Ist der Eintrag
* unbekannt, wird NICHT weitergeleitet — dann läuft wie bisher die 404-
* bzw. Tombstone-Behandlung, statt den Besucher auf eine Adresse zu
* schicken, die es auch dort nicht gibt.
*/
class RedirectPortalOnlyPagesFromHub
{
public function handle(Request $request, Closure $next): Response
{
if (! str_contains($request->getHost(), 'pressekonto')) {
// Nur lesende Aufrufe: ein 301 auf ein POST (Newsroom beanspruchen,
// Share-Zählung) würde die Anfrage verlieren.
if (! str_contains($request->getHost(), 'pressekonto') || ! $request->isMethod('GET')) {
return $next($request);
}
$base = StaticPageCanonical::portalOnlyRedirect($request->route()?->getName());
$routeName = $request->route()?->getName();
$base = StaticPageCanonical::portalOnlyRedirect($routeName)
?? $this->contentPortalBase($request, $routeName);
if ($base === null) {
return $next($request);
}
return redirect($base.$request->getPathInfo(), 301);
return redirect($base.$request->getRequestUri(), 301);
}
/**
* Portal-Basis für eine Inhaltsadresse, oder null, wenn die Route keine
* Inhaltsroute ist oder der Eintrag nicht existiert.
*/
private function contentPortalBase(Request $request, ?string $routeName): ?string
{
if ($routeName === null || ! in_array($routeName, (array) config('canonical.portal_only_content_routes', []), true)) {
return null;
}
$slug = $request->route()?->parameter('slug');
// Kategorie und Jahresarchiv gibt es auf beiden Portalen — dort
// entscheidet das Standard-Portal.
if (! is_string($slug) || ! in_array($routeName, ['release.detail', 'newsroom', 'newsroom.claim'], true)) {
return StaticPageCanonical::defaultPortalBase();
}
// Entfernte und depublizierte Einträge zählen mit: ihre Adresse
// liefert auf dem Portal einen Tombstone, und der gehört dorthin.
//
// Bei Doppelveröffentlichung tragen beide Fassungen denselben Slug —
// dann gewinnt die Erstquelle (ohne `canonical_press_release_id`),
// damit die Weiterleitung nicht auf der Kopie endet.
$portal = $routeName === 'release.detail'
? PressRelease::query()->withoutGlobalScopes()
->where('slug', $slug)
->orderByRaw('canonical_press_release_id is null desc')
->orderBy('id')
->value('portal')
: Company::query()->withoutGlobalScopes()->where('slug', $slug)->orderBy('id')->value('portal');
return $portal === null ? null : StaticPageCanonical::portalBaseFor((string) ($portal->value ?? $portal));
}
}