Editorial-Frontend: oeffentliche Unterseiten, Preise & Linkpflege

Oeffentliche Strecke durchgaengig auf das Editorial-Design umgestellt und
datengetrieben gemacht:

- Newsrooms-Verzeichnis + Newsroom-Detailseiten (Suche, Pagination)
- Preise-Seite neu (Abos via Plan, Einzel-PM via config/billing, Add-ons via
  config/credits); Texte ausgelagert nach lang/{de,en}/pricing.php
- Pricing-Teaser-Komponente auf den Startseiten + Preise-Link in Header/Footer
- Statische Seiten im Editorial-Design ueber neue Komponente x-web.static-page:
  impressum, datenschutz, agb, cookies, faq (Akkordeon), hilfe, kontakt,
  ueber-uns, api (altes Theme/gradient-hero entfernt)
- Header/Footer-Linkpflege: tote #-Anker raus, FAQ/Hilfe/Cookies verlinkt,
  "Anmelden" fuehrt in den Hub
- Legacy-URL-Redirects (.html, presskit->newsroom, Regionen)
- Datums-Bugfix in Feed-Komponenten

Tests: NewsroomPage, PreisePage, PricingTeaser, StaticPages, NavigationLinks,
LegacyRedirect. Doku in docs/frontend aktualisiert.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-06-17 16:08:46 +00:00
parent 0618080e02
commit 326ff9b2a0
34 changed files with 2842 additions and 3102 deletions

View file

@ -0,0 +1,114 @@
<?php
use App\Enums\Portal;
use App\Models\Company;
use App\Models\PressRelease;
use Tests\TestCase;
/**
* Bestandsschutz für die URL-Struktur der Altseite: jede Alt-URL muss per 301
* sauber auf ihr neues Pendant zeigen, damit kein SEO-Wert verloren geht.
*
* @see docs/frontend/Struktur Frontend.md
*/
test('legacy static slugs of the old site redirect to the new german paths', function (string $legacy, string $target) {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/'.$legacy)
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/'.$target);
})->with([
'pricelist -> preise' => ['pricelist.html', 'preise'],
'service -> veroeffentlichen' => ['service.html', 'veroeffentlichen'],
'contact -> kontakt' => ['contact.html', 'kontakt'],
'imprint -> impressum' => ['imprint.html', 'impressum'],
'terms -> agb' => ['terms.html', 'agb'],
'privacypolicy -> datenschutz' => ['privacypolicy.html', 'datenschutz'],
]);
test('legacy account urls redirect to the publisher hub', function (string $legacy, string $hubPath) {
/** @var TestCase $this */
$hubUrl = rtrim((string) config('domains.domain_portal_url'), '/');
$this->get('https://businessportal24.test/de/'.$legacy)
->assertStatus(301)
->assertRedirect($hubUrl.$hubPath);
})->with([
'registration -> register' => ['registration.html', '/register'],
'login -> login' => ['login.html', '/login'],
]);
test('legacy press guide pages collapse into the faq', function (string $legacy) {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/'.$legacy)
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/faq');
})->with([
'overview' => ['presse.html'],
'pressemitteilung-schreiben' => ['presse/pressemitteilung-schreiben.html'],
'pressemitteilung-aufbau' => ['presse/pressemitteilung-aufbau.html'],
'presseausweis' => ['presse/presseausweis.html'],
]);
test('legacy presskit urls map to the new newsroom via the legacy id', function () {
/** @var TestCase $this */
$company = Company::factory()->businessportal24()->create([
'name' => 'Acme GmbH',
'slug' => 'acme-gmbh-neu',
'is_active' => true,
'legacy_portal' => Portal::Businessportal24->value,
'legacy_id' => 512,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($company)
->create(['published_at' => now()->subDay()]);
// Alt-Slug ist egal – die Legacy-ID bestimmt das Ziel (heutiger Slug).
$this->get('https://businessportal24.test/de/presskit/512/irgendein-alt-slug.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/newsroom/acme-gmbh-neu');
});
test('legacy presskit urls fall back to the directory without a public newsroom', function () {
/** @var TestCase $this */
// Unbekannte ID.
$this->get('https://businessportal24.test/de/presskit/99999/unbekannt.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/newsrooms');
// Firma ohne veröffentlichte Meldung -> kein öffentlicher Newsroom.
Company::factory()->businessportal24()->create([
'is_active' => true,
'legacy_portal' => Portal::Businessportal24->value,
'legacy_id' => 777,
]);
$this->get('https://businessportal24.test/de/presskit/777/leer.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/newsrooms');
});
test('dropped language/region variants redirect to the default edition', function (string $path) {
/** @var TestCase $this */
$this->get('https://businessportal24.test/'.$path)
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de');
})->with([
'de-ch root' => ['de-ch'],
'de-at root' => ['de-at'],
'de-ch subpath' => ['de-ch/category/finanzen.html'],
'de-at subpath' => ['de-at/login.html'],
]);
test('legacy aliases also work without an edition prefix', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/contact.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/kontakt');
$this->get('https://businessportal24.test/imprint.html')
->assertStatus(301)
->assertRedirect('https://businessportal24.test/de/impressum');
});

View file

@ -0,0 +1,22 @@
<?php
use Tests\TestCase;
test('header and footer link to faq, hilfe and cookies and drop dead links', function () {
/** @var TestCase $this */
$response = $this->get('https://businessportal24.test/de');
$response->assertSuccessful()
// Neu verlinkt (FAQ im Header + Footer, Hilfe + Cookies im Footer).
->assertSee('/de/faq', false)
->assertSee('/de/hilfe', false)
->assertSee('/de/cookies', false)
// „Anmelden" führt jetzt in den Hub statt auf einen toten Anker.
->assertSee('/login', false)
->assertDontSee('href="#newsletter"', false)
->assertDontSee('href="#rss"', false)
->assertDontSee('href="#anmelden"', false)
// Nicht verlinkte Platzhalter-Punkte wurden entfernt.
->assertDontSee('Mediendaten')
->assertDontSee('Verifizierter Newsroom');
});

View file

@ -0,0 +1,116 @@
<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Illuminate\Support\Str;
use Tests\TestCase;
function makeNewsroom(string $name, Portal $portal = Portal::Businessportal24, array $attributes = []): Company
{
return Company::factory()
->state(['portal' => $portal->value])
->create(array_merge([
'name' => $name,
'slug' => Str::slug($name).'-'.fake()->unique()->numberBetween(1000, 9999),
'is_active' => true,
], $attributes));
}
function publishForCompany(Company $company, string $title, array $attributes = [], ?Category $category = null): PressRelease
{
$category ??= Category::factory()->create(['portal' => Portal::Both]);
return PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for($company)
->create(array_merge([
'title' => $title,
'slug' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
'language' => 'de',
'published_at' => now()->subDay(),
], $attributes));
}
test('the newsrooms index renders the editorial shell and lists active newsrooms', function () {
/** @var TestCase $this */
$company = makeNewsroom('Nordwind Energie AG');
publishForCompany($company, 'Nordwind nimmt neuen Windpark in Betrieb');
$this->get('https://businessportal24.test/de/newsrooms')
->assertSuccessful()
->assertSee('businessportal', false)
->assertSeeText('Newsrooms')
->assertSeeText('Nordwind Energie AG');
});
test('the newsrooms index filters by company name', function () {
/** @var TestCase $this */
$match = makeNewsroom('Solartec Industries GmbH');
publishForCompany($match, 'Solartec meldet Rekordquartal');
$other = makeNewsroom('Hafenlogistik Bremen AG');
publishForCompany($other, 'Hafenlogistik baut Terminal aus');
$this->get('https://businessportal24.test/de/newsrooms?q=Solartec')
->assertSuccessful()
->assertSeeText('Solartec Industries GmbH')
->assertDontSeeText('Hafenlogistik Bremen AG');
});
test('the newsrooms index excludes companies without published releases', function () {
/** @var TestCase $this */
makeNewsroom('Stilles Unternehmen AG');
$this->get('https://businessportal24.test/de/newsrooms')
->assertSuccessful()
->assertDontSeeText('Stilles Unternehmen AG');
});
test('the newsroom detail page renders company data and its releases', function () {
/** @var TestCase $this */
$company = makeNewsroom('Bavaria Maschinenbau AG', attributes: [
'boilerplate' => 'Bavaria Maschinenbau ist ein führender Anbieter von Industrielösungen.',
]);
publishForCompany($company, 'Bavaria präsentiert neue Produktionslinie');
$this->get('https://businessportal24.test/de/newsroom/'.$company->slug)
->assertSuccessful()
->assertSeeText('Bavaria Maschinenbau AG')
->assertSeeText('Bavaria präsentiert neue Produktionslinie')
->assertSeeText('Alle Meldungen');
});
test('the newsroom detail page returns 404 for an unknown slug', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/newsroom/gibt-es-nicht')
->assertNotFound();
});
test('the newsroom detail page returns 404 when the company has no published releases', function () {
/** @var TestCase $this */
$company = makeNewsroom('Leeres Newsroom AG');
$this->get('https://businessportal24.test/de/newsroom/'.$company->slug)
->assertNotFound();
});
test('the newsrooms pages are available per edition and portal', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/en/newsrooms')->assertSuccessful();
$this->get('https://presseecho.test/de/newsrooms')
->assertSuccessful()
->assertSee('presse', false);
});
test('legacy newsroom html urls redirect to the clean edition path', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/newsrooms.html')
->assertRedirect('https://businessportal24.test/de/newsrooms');
$this->get('https://businessportal24.test/de/newsroom/acme.html')
->assertRedirect('https://businessportal24.test/de/newsroom/acme');
});

View file

@ -0,0 +1,57 @@
<?php
use App\Models\Plan;
use Database\Seeders\PlanSeeder;
use Tests\TestCase;
test('the pricing page renders the editorial shell with real plans, single pm and add-ons', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$this->get('https://businessportal24.test/de/preise')
->assertSuccessful()
// Editorial-Masthead (Inhalte aus lang/de/pricing.php)
->assertSeeText('Transparente Preise für jede Reichweite')
// 1) Abo-Tarife (echte Pläne aus dem System)
->assertSeeText('Abo-Tarife mit Monatskontingent')
->assertSeeText('Starter')
->assertSeeText('Business')
->assertSeeText('Pro')
->assertSeeText('Agency')
// 2) Einzel-Pressemitteilung (config/billing)
->assertSeeText('Einzel-Pressemitteilung')
->assertSeeText('pro Veröffentlichung')
// 3) Add-ons (config/credits)
->assertSeeText('Add-ons & Einzelkosten')
->assertSeeText('Extra-Pressemitteilung')
->assertSeeText('Boost-Platzierung')
->assertSeeText('Veröffentlichungsnachweis (PDF)')
->assertSeeText('Credit-Pakete')
->assertSeeText('7 Tage') // boost 7d aus config
->assertSeeText('10 Credits') // Credit-Paket p10 aus config
->assertSeeText('Depublizieren')
->assertSeeText('25 Credits'); // depublish aus config
});
test('the pricing page serves english content on the english edition', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$this->get('https://businessportal24.test/en/preise')
->assertSuccessful()
->assertSeeText('Transparent pricing for every reach')
->assertSeeText('Subscriptions with a monthly quota')
->assertSeeText('Single press release')
->assertSeeText('Add-ons & individual costs');
});
test('the pricing page still works and shows an empty hint without plans', function () {
/** @var TestCase $this */
expect(Plan::query()->count())->toBe(0);
$this->get('https://businessportal24.test/de/preise')
->assertSuccessful()
->assertSeeText('Tarife werden in Kürze veröffentlicht.')
// Add-ons sind config-basiert und unabhängig von Tarifen sichtbar.
->assertSeeText('Add-ons & Einzelkosten');
});

View file

@ -0,0 +1,54 @@
<?php
use App\Models\Plan;
use Database\Seeders\PlanSeeder;
use Tests\TestCase;
test('the homepage renders active plans from the system in the pricing teaser', function () {
/** @var TestCase $this */
$this->seed(PlanSeeder::class);
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('Tarife & Pakete')
->assertSeeText('Starter')
->assertSeeText('Business')
->assertSeeText('Pro')
->assertSeeText('Agency')
// Jahrespreis-Kommunikation und Live-Preis (Agency 199 €).
->assertSeeText('2 Monate gratis')
->assertSee('199')
->assertSeeText('Auswählen')
->assertSeeText('Beliebt');
});
test('the pricing teaser hides inactive plans', function () {
/** @var TestCase $this */
Plan::factory()->create(['name' => 'Sichtbarer Tarif', 'is_active' => true]);
Plan::factory()->inactive()->create(['name' => 'Versteckter Tarif']);
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSeeText('Sichtbarer Tarif')
->assertDontSeeText('Versteckter Tarif');
});
test('the pricing teaser is omitted when no plans exist but the page still works', function () {
/** @var TestCase $this */
expect(Plan::query()->count())->toBe(0);
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertDontSeeText('Auswählen');
});
test('the header links to the pricing page on every portal page', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de')
->assertSuccessful()
->assertSee('/de/preise');
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSee('/de/preise');
});

View file

@ -0,0 +1,45 @@
<?php
use Tests\TestCase;
dataset('static pages', [
'impressum' => ['de/impressum', 'Impressum'],
'datenschutz' => ['de/datenschutz', 'Datenschutzerklärung'],
'agb' => ['de/agb', 'Allgemeine Geschäftsbedingungen'],
'cookies' => ['de/cookies', 'Cookie-Richtlinien'],
'faq' => ['de/faq', 'Häufig gestellte Fragen'],
'hilfe' => ['de/hilfe', 'Hilfe-Center'],
'kontakt' => ['de/kontakt', 'Kontakt'],
'ueber-uns' => ['de/ueber-uns', 'Über uns'],
'api' => ['de/api', 'API & Integrationen'],
]);
test('static pages render in the editorial design', function (string $path, string $heading) {
/** @var TestCase $this */
$response = $this->get("https://businessportal24.test/{$path}");
$response->assertSuccessful()
->assertSeeText($heading)
// Editorial-Shell (Token aus den neuen Komponenten) statt altem Theme.
->assertSee('bg-bg', false)
// Altes Theme darf nicht mehr gerendert werden.
->assertDontSee('gradient-hero');
})->with('static pages');
test('the faq page lists questions and links to support', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/faq')
->assertSuccessful()
->assertSeeText('Wie veröffentliche ich meine erste Pressemitteilung?')
->assertSeeText('Frage nicht gefunden?')
->assertSee(route('kontakt'), false);
});
test('the help center shows quick links and topics', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/hilfe')
->assertSuccessful()
->assertSeeText('Hilfe-Themen')
->assertSee(route('faq'), false)
->assertSee(route('veroeffentlichen'), false);
});