R2.2: HubPageData - Plan-Katalog + echte Plattform-Kennzahlen fuer die Hub-Landing

Liefert Plan::active(), Einzel-PM-Preis und stuendlich gecachte echte
Zahlen (Archiv-Mitteilungen gesamt/je Portal, aktive Newsrooms,
Branchen) an web.pressekonto. Tests: Katalog-Preise statt Fake-Liste,
keine toten Links/erfundenen Referenzen, Rechtsseiten auf der
Hub-Domain erreichbar. (Gehoert zum Web-Commit der Hub-Ueberarbeitung.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-07-03 09:24:04 +00:00
parent 5226f5de2b
commit f6edb4aec5
3 changed files with 104 additions and 10 deletions

View file

@ -0,0 +1,49 @@
<?php
namespace App\Services\Web;
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\Plan;
use App\Models\PressRelease;
use App\Scopes\PortalScope;
use Illuminate\Support\Facades\Cache;
/**
* Datenaufbereitung der Hub-Landing (pressekonto.de): aktiver Tarif-Katalog
* plus echte Plattform-Kennzahlen statt statischer Marketing-Zahlen.
*/
class HubPageData
{
private const StatsCacheKey = 'web.hub.landing-stats';
private const StatsTtlSeconds = 3600;
/**
* @return array<string, mixed>
*/
public function build(): array
{
return [
'plans' => Plan::query()->active()->get(),
'singlePmPriceCents' => (int) config('billing.single_pm_price_cents'),
'hubStats' => Cache::remember(self::StatsCacheKey, self::StatsTtlSeconds, function (): array {
$published = PressRelease::query()
->withoutGlobalScope(PortalScope::class)
->visibleInArchive();
return [
'releases_total' => (clone $published)->count(),
'releases_presseecho' => (clone $published)->forPortalValues([Portal::Presseecho->value, Portal::Both->value])->count(),
'releases_businessportal' => (clone $published)->forPortalValues([Portal::Businessportal24->value, Portal::Both->value])->count(),
'newsrooms' => Company::query()
->withoutGlobalScope(PortalScope::class)
->whereHas('pressReleases', fn ($query) => $query->withoutGlobalScope(PortalScope::class)->visibleInArchive())
->count(),
'categories' => Category::query()->withoutGlobalScope(PortalScope::class)->count(),
];
}),
];
}
}

View file

@ -16,6 +16,7 @@ use App\Models\Category;
use App\Models\Company;
use App\Models\Plan;
use App\Scopes\PortalScope;
use App\Services\Web\HubPageData;
use App\Support\DomainAssetContext;
use Illuminate\Support\Facades\Route;
@ -39,7 +40,7 @@ Route::get('/', function () use ($applyWebDomainConfig) {
if (str_contains($domain, 'pressekonto')) {
$applyWebDomainConfig('pressekonto');
return view('web.pressekonto');
return view('web.pressekonto', app(HubPageData::class)->build());
}
return redirect(request()->getSchemeAndHttpHost().'/'.SetEdition::DEFAULT_EDITION, 301);

View file

@ -1,9 +1,12 @@
<?php
use Database\Seeders\PlanSeeder;
use Tests\TestCase;
test('pressekonto hub homepage renders the publisher landing shell', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$this->get('https://pressekonto.test/')
->assertSuccessful()
->assertSee('presse', false)
@ -14,15 +17,57 @@ test('pressekonto hub homepage renders the publisher landing shell', function ()
->assertSeeText('So funktioniert es')
->assertSeeText('Vier Schritte')
->assertSeeText('Tarife')
->assertSeeText('Starter')
->assertSeeText('Standard')
->assertSeeText('Pro')
->assertSeeText('Enterprise')
->assertSeeText('Hinter pressekonto')
->assertSeeText('Die Plattform in Zahlen')
->assertSeeText('Plattform im Überblick')
->assertSeeText('Häufige Fragen')
->assertSeeText('Loslegen')
->assertSeeText('Alle Systeme betriebsbereit');
->assertSeeText('Loslegen');
});
test('pressekonto hub pricing is driven by the plan catalog', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$response = $this->get('https://pressekonto.test/')->assertSuccessful();
// Alle aktiven Tarife aus dem Katalog inkl. Kontingent; die frühere
// hartcodierte Fake-Preisliste (0/49/149 €, "Standard") ist raus.
$response
->assertSeeText('Starter')
->assertSeeText('Business')
->assertSeeText('Pro')
->assertSeeText('Agency')
->assertSee('29&nbsp;€', false)
->assertSee('199&nbsp;€', false)
->assertDontSeeText('Standard')
->assertDontSee('149&nbsp;€', false);
});
test('pressekonto hub homepage has no dead links or fabricated references', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$response = $this->get('https://pressekonto.test/')->assertSuccessful();
$content = $response->getContent();
expect($content)
->not->toContain('href="#"')
->not->toContain('Siemens')
->not->toContain('Alle Systeme betriebsbereit')
->not->toContain('14 Tage')
->not->toContain('ISO 27001');
// Rechtsseiten sind auf der Hub-Domain verlinkt und erreichbar.
expect($content)->toContain('/de/impressum')->toContain('/de/datenschutz');
});
test('legal pages are reachable on the hub domain', function () {
/** @var TestCase $this */
$this->get('https://pressekonto.test/de/impressum')->assertSuccessful();
$this->get('https://pressekonto.test/de/datenschutz')->assertSuccessful();
$this->get('https://pressekonto.test/de/agb')->assertSuccessful();
$this->get('https://pressekonto.test/de/cookies')->assertSuccessful();
});
test('pressekonto hub homepage uses the brand-mark splitting without TLDs', function () {
@ -42,10 +87,9 @@ test('pressekonto hub homepage uses the brand-mark splitting without TLDs', func
->toContain('>echo<')
->toContain('>konto<');
// pressekonto darf ausschliesslich in der Vertriebs-Mailto-Adresse als Domain auftauchen,
// niemals als Marken-Schreibweise im Lese-Text.
expect(substr_count($content, 'pressekonto.de'))
->toBe(substr_count($content, 'vertrieb@pressekonto.de'));
// pressekonto darf als Domain nirgends im Lese-Text auftauchen — nur die
// gesplittete Marken-Schreibweise.
expect($content)->not->toContain('pressekonto.de');
});
test('pressekonto hub homepage loads the hub theme assets, not portal admin', function () {